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 :
Maximum users per POST request
Maximum users returned per GET request
Maximum users deleted per request
PROCESS_USERS_INTERVAL_SECONDS
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 :
Process Login Methods
Create users for each authentication method (email/password, social, passwordless)
Link Accounts
Create primary users and link multiple login methods
Create User ID Mappings
Map SuperTokens IDs to external system IDs
Verify Emails
Mark emails as verified based on import data
Create TOTP Devices
Import 2FA devices
Import Metadata
Store custom user metadata
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 Code Description E001 Unknown recipe ID E002-E008 Recipe-specific errors (duplicate email, phone, etc.) E009-E017 Tenant association errors E018-E028 Account linking errors E030-E032 User ID mapping errors E033-E034 User role errors E036-E037 TOTP device errors E039-E044 General 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
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