Skip to main content

Overview

SuperTokens Core provides a high-performance bulk import system for migrating users from external authentication systems. It supports batched processing, multiple authentication methods per user, and preserves user metadata, roles, and verification status.

Key Features

High Volume

Import up to 10,000 users per API request

Multi-Method

Support for email/password, social, and passwordless

Account Linking

Automatically link multiple auth methods per user

Async Processing

Background processing with status tracking

Error Handling

Detailed error reporting per user

Password Migration

Import existing password hashes

Bulk Import Configuration

From io/supertokens/bulkimport/BulkImport.java:91-101:
MAX_USERS_TO_ADD
number
Maximum users per POST request
GET_USERS_PAGINATION_MAX_LIMIT
number
Maximum users returned per GET request
GET_USERS_DEFAULT_LIMIT
number
Default pagination limit
DELETE_USERS_MAX_LIMIT
number
Maximum users deleted per request
PROCESS_USERS_INTERVAL_SECONDS
number
Interval between processing jobs (5 minutes)

User Import Structure

BulkImportUser Schema

public class BulkImportUser {
    public String id;                        // Auto-generated UUID
    public String externalUserId;            // Your system's user ID
    public JsonObject userMetadata;          // Custom metadata
    public List<UserRole> userRoles;         // Roles and permissions
    public List<TotpDevice> totpDevices;     // TOTP devices
    public List<LoginMethod> loginMethods;   // Authentication methods
}

public class LoginMethod {
    public String recipeId;                  // "emailpassword", "thirdparty", "passwordless"
    public String superTokensUserId;         // Generated per method
    public String externalUserId;            // Optional external ID
    public String email;
    public String passwordHash;              // Existing hash to import
    public String plainTextPassword;         // Or plain text (will be hashed)
    public String phoneNumber;
    public String thirdPartyId;              // "google", "github", etc.
    public String thirdPartyUserId;          // ID from provider
    public boolean isPrimary;                // Primary login method flag
    public boolean isVerified;               // Email/phone verified
    public List<String> tenantIds;           // Associated tenants
    public long timeJoinedInMSSinceEpoch;
}

public class UserRole {
    public String role;
    public List<String> tenantIds;
}

public class TotpDevice {
    public String secretKey;
    public String deviceName;
    public int period;
    public int skew;
}

Adding Users for Import

From io/supertokens/bulkimport/BulkImport.java:106-123:
public static void addUsers(
    AppIdentifier appIdentifier,
    Storage storage,
    List<BulkImportUser> users
) {
    while (true) {
        try {
            StorageUtils.getBulkImportStorage(storage)
                .addBulkImportUsers(appIdentifier, users);
            break;
        } catch (DuplicateUserIdException e) {
            // Regenerate IDs on conflict
            for (BulkImportUser user : users) {
                user.id = Utils.getUUID();
            }
        }
    }
}

Import Process

Processing Pipeline

From io/supertokens/bulkimport/BulkImport.java:209-239:
1

Process Login Methods

Create users for each authentication method (email/password, social, passwordless)
2

Link Accounts

Create primary users and link multiple login methods
3

Create User ID Mappings

Map SuperTokens IDs to external system IDs
4

Verify Emails

Mark emails as verified based on import data
5

Create TOTP Devices

Import 2FA devices
6

Import Metadata

Store custom user metadata
7

Assign Roles

Apply roles and permissions
public static void processUsersImportSteps(
    Main main,
    AppIdentifier appIdentifier,
    Storage bulkImportProxyStorage,
    List<BulkImportUser> users,
    Storage[] allStoragesForApp
) {
    // 1. Process login methods by recipe
    processUsersLoginMethods(main, appIdentifier, bulkImportProxyStorage, users);
    
    // 2. Create primary users and link accounts
    createPrimaryUsersAndLinkAccounts(main, appIdentifier, bulkImportProxyStorage, users);
    
    // 3. Create user ID mappings
    createMultipleUserIdMapping(appIdentifier, users, allStoragesForApp);
    
    // 4. Verify emails
    verifyMultipleEmailForAllLoginMethods(appIdentifier, bulkImportProxyStorage, users);
    
    // 5. Create TOTP devices
    createMultipleTotpDevices(main, appIdentifier, bulkImportProxyStorage, users);
    
    // 6. Import metadata
    createMultipleUserMetadata(appIdentifier, bulkImportProxyStorage, users);
    
    // 7. Assign roles
    createMultipleUserRoles(main, appIdentifier, bulkImportProxyStorage, users);
}

Email/Password Import

From io/supertokens/bulkimport/BulkImport.java:383-426:
private static List<? extends ImportUserBase> processEmailPasswordLoginMethods(
    Main main,
    Storage storage,
    List<LoginMethod> loginMethods,
    AppIdentifier appIdentifier
) {
    List<EmailPasswordImportUser> usersToImport = new ArrayList<>();
    
    for (LoginMethod lm : loginMethods) {
        TenantIdentifier tenantIdentifier = new TenantIdentifier(
            appIdentifier.getConnectionUriDomain(),
            appIdentifier.getAppId(),
            lm.tenantIds.get(0)
        );
        
        // Hash password if plain text provided
        String passwordHash = lm.passwordHash;
        if (passwordHash == null && lm.plainTextPassword != null) {
            passwordHash = PasswordHashing.getInstance(main)
                .createHashWithSalt(
                    tenantIdentifier.toAppIdentifier(),
                    lm.plainTextPassword
                );
        }
        
        usersToImport.add(new EmailPasswordImportUser(
            lm.superTokensUserId,
            lm.email,
            passwordHash,
            tenantIdentifier,
            lm.timeJoinedInMSSinceEpoch
        ));
    }
    
    // Batch insert
    EmailPassword.createMultipleUsersWithPasswordHash(
        storage, usersToImport
    );
    
    return usersToImport;
}
Password hashes can be imported directly (if you have them) or plain text passwords will be hashed automatically using your configured algorithm.

Third-Party Import

From io/supertokens/bulkimport/BulkImport.java:344-381:
private static List<? extends ImportUserBase> processThirdpartyLoginMethods(
    Main main,
    Storage storage,
    List<LoginMethod> loginMethods,
    AppIdentifier appIdentifier
) {
    List<ThirdPartyImportUser> usersToImport = new ArrayList<>();
    
    for (LoginMethod lm : loginMethods) {
        TenantIdentifier tenantIdentifier = new TenantIdentifier(
            appIdentifier.getConnectionUriDomain(),
            appIdentifier.getAppId(),
            lm.tenantIds.get(0)
        );
        
        usersToImport.add(new ThirdPartyImportUser(
            lm.email,
            lm.superTokensUserId,
            lm.thirdPartyId,
            lm.thirdPartyUserId,
            tenantIdentifier,
            lm.timeJoinedInMSSinceEpoch
        ));
    }
    
    ThirdParty.createMultipleThirdPartyUsers(storage, usersToImport);
    
    return usersToImport;
}

Passwordless Import

private static List<? extends ImportUserBase> processPasswordlessLoginMethods(
    Main main,
    AppIdentifier appIdentifier,
    Storage storage,
    List<LoginMethod> loginMethods
) {
    List<PasswordlessImportUser> usersToImport = new ArrayList<>();
    
    for (LoginMethod lm : loginMethods) {
        TenantIdentifier tenantIdentifier = new TenantIdentifier(
            appIdentifier.getConnectionUriDomain(),
            appIdentifier.getAppId(),
            lm.tenantIds.get(0)
        );
        
        usersToImport.add(new PasswordlessImportUser(
            lm.superTokensUserId,
            lm.phoneNumber,
            lm.email,
            tenantIdentifier,
            lm.timeJoinedInMSSinceEpoch
        ));
    }
    
    Passwordless.createPasswordlessUsers(storage, usersToImport);
    
    return usersToImport;
}

Account Linking During Import

From io/supertokens/bulkimport/BulkImport.java:479-527:
private static void createPrimaryUsersAndLinkAccounts(
    Main main,
    AppIdentifier appIdentifier,
    Storage storage,
    List<BulkImportUser> users
) {
    // Filter users that need account linking
    List<BulkImportUser> usersForLinking = users.stream()
        .filter(user -> 
            user.loginMethods.stream().anyMatch(lm -> lm.isPrimary) ||
            user.loginMethods.size() > 1
        )
        .collect(Collectors.toList());
    
    if (usersForLinking.isEmpty()) {
        return;
    }
    
    // Create primary users
    CreatePrimaryUsersResultHolder resultHolder = 
        AuthRecipe.createPrimaryUsersForBulkImport(
            main, appIdentifier, storage, usersForLinking
        );
    
    // Link accounts
    if (resultHolder.usersWithSameExtraData != null) {
        linkAccountsForMultipleUser(
            main, appIdentifier, storage,
            usersForLinking, resultHolder.usersWithSameExtraData
        );
    }
}
When importing users with multiple login methods, one method must be marked as isPrimary: true. All other methods will be linked to this primary account.

Tenant Association

From io/supertokens/bulkimport/BulkImport.java:428-477:
private static void associateUserToTenants(
    Main main,
    AppIdentifier appIdentifier,
    Storage storage,
    LoginMethod lm,
    String firstTenant
) {
    // First tenant is already associated during user creation
    for (String tenantId : lm.tenantIds) {
        if (tenantId.equals(firstTenant)) {
            continue;
        }
        
        TenantIdentifier tenantIdentifier = new TenantIdentifier(
            appIdentifier.getConnectionUriDomain(),
            appIdentifier.getAppId(),
            tenantId
        );
        
        Multitenancy.addUserIdToTenant(
            main,
            tenantIdentifier,
            storage,
            lm.superTokensUserId
        );
    }
}

Error Handling

Error Codes

From io/supertokens/bulkimport/BulkImport.java:86-87: All errors include unique error codes (E001-E046) for debugging:
Error CodeDescription
E001Unknown recipe ID
E002-E008Recipe-specific errors (duplicate email, phone, etc.)
E009-E017Tenant association errors
E018-E028Account linking errors
E030-E032User ID mapping errors
E033-E034User role errors
E036-E037TOTP device errors
E039-E044General processing errors

Error Response Structure

public class BulkImportBatchInsertException extends Exception {
    public Map<String, Exception> exceptionByUserId;
}
Errors are mapped by user ID:
{
  "user-123": {
    "error": "E003: A user with email [email protected] already exists"
  },
  "user-456": {
    "error": "E034: Role does not exist! You need to pre-create the role"
  }
}

Checking Import Status

Get Import Users

From io/supertokens/bulkimport/BulkImport.java:125-152:
public static BulkImportUserPaginationContainer getUsers(
    AppIdentifier appIdentifier,
    Storage storage,
    int limit,
    BULK_IMPORT_USER_STATUS status,  // NEW, PROCESSING, FAILED
    String paginationToken
) {
    List<BulkImportUser> users;
    
    if (paginationToken == null) {
        users = bulkImportStorage.getBulkImportUsers(
            appIdentifier, limit + 1, status, null, null
        );
    } else {
        BulkImportUserPaginationToken tokenInfo = 
            BulkImportUserPaginationToken.extractTokenInfo(paginationToken);
        users = bulkImportStorage.getBulkImportUsers(
            appIdentifier, limit + 1, status,
            tokenInfo.bulkImportUserId, tokenInfo.createdAt
        );
    }
    
    // Generate next pagination token if needed
    String nextPaginationToken = null;
    if (users.size() == limit + 1) {
        BulkImportUser lastUser = users.get(limit);
        nextPaginationToken = new BulkImportUserPaginationToken(
            lastUser.id, lastUser.createdAt
        ).generateToken();
        users = users.subList(0, limit);
    }
    
    return new BulkImportUserPaginationContainer(
        users, nextPaginationToken
    );
}

Import Status Types

NEW

User added to import queue

PROCESSING

Currently being imported

FAILED

Import failed with error

Count Users by Status

long count = BulkImport.getBulkImportUsersCount(
    appIdentifier,
    storage,
    BULK_IMPORT_USER_STATUS.FAILED  // or null for all
);

Deleting Import Users

From io/supertokens/bulkimport/BulkImport.java:154-157:
public static List<String> deleteUsers(
    AppIdentifier appIdentifier,
    Storage storage,
    String[] userIds  // Max 500
) {
    return StorageUtils.getBulkImportStorage(storage)
        .deleteBulkImportUsers(appIdentifier, userIds);
}
This deletes users from the import queue only. It does not affect successfully imported users.

Complete Import Example

// Prepare users for import
List<BulkImportUser> users = new ArrayList<>();

BulkImportUser user = new BulkImportUser();
user.externalUserId = "external-user-123";
user.userMetadata = new JsonObject();
user.userMetadata.addProperty("plan", "premium");

// Add email/password login method
LoginMethod emailMethod = new LoginMethod();
emailMethod.recipeId = "emailpassword";
emailMethod.superTokensUserId = Utils.getUUID();
emailMethod.email = "[email protected]";
emailMethod.passwordHash = existingPasswordHash;
emailMethod.isPrimary = true;
emailMethod.isVerified = true;
emailMethod.tenantIds = Arrays.asList("public");
emailMethod.timeJoinedInMSSinceEpoch = System.currentTimeMillis();

// Add Google login method
LoginMethod googleMethod = new LoginMethod();
googleMethod.recipeId = "thirdparty";
googleMethod.superTokensUserId = Utils.getUUID();
googleMethod.email = "[email protected]";
googleMethod.thirdPartyId = "google";
googleMethod.thirdPartyUserId = "google-user-id-123";
googleMethod.isPrimary = false;
googleMethod.isVerified = true;
googleMethod.tenantIds = Arrays.asList("public");
googleMethod.timeJoinedInMSSinceEpoch = System.currentTimeMillis();

user.loginMethods = Arrays.asList(emailMethod, googleMethod);

// Add roles
UserRole adminRole = new UserRole();
adminRole.role = "admin";
adminRole.tenantIds = Arrays.asList("public");
user.userRoles = Arrays.asList(adminRole);

// Add TOTP device
TotpDevice totp = new TotpDevice();
totp.secretKey = "JBSWY3DPEHPK3PXP";
totp.deviceName = "My Authenticator";
totp.period = 30;
totp.skew = 1;
user.totpDevices = Arrays.asList(totp);

users.add(user);

// Add users to import queue
BulkImport.addUsers(appIdentifier, storage, users);

// Check status
BulkImportUserPaginationContainer result = BulkImport.getUsers(
    appIdentifier, storage, 100, 
    BULK_IMPORT_USER_STATUS.FAILED, null
);

for (BulkImportUser failedUser : result.users) {
    System.out.println("Failed: " + failedUser.id);
}

Best Practices

Batch Processing

Import in batches of 1,000-5,000 users for optimal performance

Pre-Create Roles

Create all roles before importing users

Validate Data

Validate emails, phone numbers, and hashes before import

Monitor Status

Check import status and handle failures

Use External IDs

Map to your system IDs for easy integration

Test Small Batches

Test with 10-100 users before full migration

Performance Considerations

  • Transaction Batching: Users are processed in database transactions for consistency
  • Proxy Storage: Uses special proxy storage to avoid connection pool exhaustion
  • Background Processing: Cron job processes users every 5 minutes
  • Error Isolation: Errors in one user don’t affect others in the batch

Build docs developers (and LLMs) love