Overview
SuperTokens Core provides enterprise-grade multi-tenancy support, allowing you to serve multiple customers (tenants) from a single SuperTokens instance. Each tenant can have isolated data, custom configurations, and independent authentication settings.
Architecture
Hierarchy
SuperTokens uses a three-level hierarchy:
Connection URI Domain
Top-level identifier, typically maps to a database connection
App
Application within a connection URI domain, shares a user pool
Tenant
Individual tenant within an app, can have custom configuration
Tenant Identifier
Every operation is scoped to a TenantIdentifier:
public class TenantIdentifier {
private String connectionUriDomain ; // Default: ""
private String appId ; // Default: "public"
private String tenantId ; // Default: "public"
}
The default tenant is represented as TenantIdentifier(null, null, null) which resolves to ("", "public", "public").
Creating Tenants
Tenant Configuration
From io/supertokens/multitenancy/Multitenancy.java:231-318 :
public static boolean addNewOrUpdateAppOrTenant (
Main main,
TenantConfig newTenant,
boolean shouldPreventProtectedConfigUpdate,
boolean skipThirdPartyConfigValidation,
boolean forceReloadResources
) {
// Validate tenant configuration
validateTenantConfig (main, newTenant,
shouldPreventProtectedConfigUpdate,
skipThirdPartyConfigValidation);
// Create in shared database
StorageLayer . getMultitenancyStorage (main)
. createTenant (newTenant);
// Refresh tenant resources
MultitenancyHelper . getInstance (main)
. refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged ( false );
// Add tenant ID in target storage
storage . addTenantIdInTargetStorage ( newTenant . tenantIdentifier );
return true ;
}
Tenant Config Structure
Unique identifier for the tenant
Tenant-specific core configuration overrides
OAuth/OIDC provider configurations for this tenant
Email/password specific settings
Passwordless authentication settings
Permission Model
From io/supertokens/multitenancy/Multitenancy.java:63-126 :
Create/Update Permissions
Tenant Level Use public tenant or same tenant
App Level Use public tenant of the app
CUD Level Use base tenant or same CUD
public static void checkPermissionsForCreateOrUpdate (
Main main,
TenantIdentifier sourceTenant,
TenantIdentifier targetTenant
) throws BadPermissionException {
// Creating/updating a tenant
if ( ! targetTenant . getTenantId (). equals (DEFAULT_TENANT_ID)) {
if ( ! sourceTenant . getTenantId (). equals (DEFAULT_TENANT_ID)
&& ! sourceTenant . getTenantId (). equals ( targetTenant . getTenantId ())) {
throw new BadPermissionException (
"You must use the public or same tenant to add/update a tenant"
);
}
}
// Creating/updating an app
if ( ! targetTenant . getAppId (). equals (DEFAULT_APP_ID)) {
if ( ! sourceTenant . getTenantId (). equals (DEFAULT_TENANT_ID)
|| ( ! sourceTenant . getAppId (). equals (DEFAULT_APP_ID)
&& ! sourceTenant . getAppId (). equals ( targetTenant . getAppId ()))) {
throw new BadPermissionException (
"You must use the public or same app to add/update an app"
);
}
}
}
Delete Permissions
Only parent entities can delete children:
Parent app can delete tenants
Parent CUD can delete apps
Base tenant can delete connection URI domains
User Management
User-Tenant Association
Users can be associated with multiple tenants:
public static boolean addUserIdToTenant (
Main main,
TenantIdentifier tenantIdentifier,
Storage storage,
String userId
) throws DuplicateEmailException,
DuplicatePhoneNumberException,
DuplicateThirdPartyUserException {
// Get user to associate
AuthRecipeUserInfo user = authRecipeStorage
. getPrimaryUserById_Transaction (appIdentifier, con, userId);
// Check for conflicts in target tenant
if ( user . isPrimaryUser ) {
// Check email conflicts
for ( String email : user . emails ) {
AuthRecipeUserInfo [] usersWithSameEmail =
authRecipeStorage . listPrimaryUsersByEmail_Transaction (
appIdentifier, con, email
);
// Validate no conflicts exist
}
// Check phone conflicts
// Check third-party conflicts
}
// Add user to tenant
return storage . addUserIdToTenant_Transaction (
tenantIdentifier, con, userId
);
}
Conflict Prevention
From io/supertokens/multitenancy/Multitenancy.java:396-555 :
When associating a primary user with a tenant, SuperTokens checks:
Email Uniqueness
No other primary user in the tenant has the same email for the same recipe
Phone Number Uniqueness
No other primary user in the tenant has the same phone number
Third-Party Uniqueness
No other primary user has the same third-party ID + third-party user ID
These checks only apply to primary users . Recipe users without account linking can have duplicate identifiers across tenants.
Disassociating Users
public static boolean removeUserIdFromTenant (
Main main,
TenantIdentifier tenantIdentifier,
Storage storage,
String userId,
String externalUserId
) throws UnknownUserIdException {
// Delete non-auth recipe data (sessions, metadata, etc.)
boolean didExist = AuthRecipe . deleteNonAuthRecipeUser (
tenantIdentifier, storage, externalUserId ?? userId
);
// Remove from tenant
didExist = storage . removeUserIdFromTenant (
tenantIdentifier, userId
) || didExist;
return didExist;
}
Storage Architecture
Database Isolation Models
SuperTokens supports multiple storage models:
Shared Database All tenants in one database with tenant_id columns
Separate Databases Each tenant has its own isolated database
Hybrid Mix of shared and isolated based on tier
Tenant Storage Mapping
Tenants within the same app share a user pool :
// All tenants in an app share the same user pool storage
String userPoolId = storage . getUserPoolId ();
// Each tenant can override configuration
TenantConfig tenant = Multitenancy . getTenantInfo (main, tenantIdentifier);
JsonObject tenantConfig = tenant . coreConfig ;
Configuration
Core Configuration Overrides
Tenants can override most core configurations:
{
"access_token_validity" : 7200 ,
"refresh_token_validity" : 144000 ,
"password_hashing_alg" : "BCRYPT" ,
"bcrypt_log_rounds" : 11
}
Protected configs cannot be changed after tenant creation:
Database connection parameters
Core service ports
Base paths
From io/supertokens/multitenancy/Multitenancy.java:158-180 :
if (shouldPreventProtecterdConfigUpdate) {
for ( String protectedConfig : CoreConfig . PROTECTED_CONFIGS ) {
if ( targetTenantConfig . coreConfig . has (protectedConfig) &&
! targetTenantConfig . coreConfig . get (protectedConfig)
. equals ( currentConfig . get (protectedConfig))) {
throw new BadPermissionException (
"Not allowed to modify protected configs."
);
}
}
}
Third-Party Provider Configuration
Each tenant can have independent OAuth/OIDC providers:
public class ThirdPartyConfig {
public ProviderConfig [] providers ;
}
public class ProviderConfig {
public String thirdPartyId ; // "google", "github", etc.
public ClientConfig [] clients ;
public String authorizationEndpoint ;
public String tokenEndpoint ;
// ... other OIDC endpoints
}
Listing Tenants
All Tenants
TenantConfig [] allTenants = Multitenancy . getAllTenants (main);
Tenants for an App
TenantConfig [] appTenants = Multitenancy . getAllTenantsForApp (
appIdentifier, main
);
Tenants for Connection URI Domain
TenantConfig [] cudTenants =
Multitenancy . getAllAppsAndTenantsForConnectionUriDomain (
connectionUriDomain, main
);
Deleting Tenants
Delete a Tenant
From io/supertokens/multitenancy/Multitenancy.java:320-335 :
public static boolean deleteTenant (
TenantIdentifier tenantIdentifier,
Main main
) throws CannotDeleteNullTenantException {
if ( tenantIdentifier . getTenantId (). equals (DEFAULT_TENANT_ID)) {
throw new CannotDeleteNullTenantException ();
}
// Delete from tenant-specific storage
storage . deleteTenantIdInTargetStorage (tenantIdentifier);
// Delete from base storage
boolean didExist = StorageLayer . getMultitenancyStorage (main)
. deleteTenantInfoInBaseStorage (tenantIdentifier);
// Refresh resources
MultitenancyHelper . getInstance (main)
. refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged ( true );
return didExist;
}
Deleting a tenant does not delete user data. You must explicitly delete users before deleting the tenant.
Delete an App
From io/supertokens/multitenancy/Multitenancy.java:337-357 :
public static boolean deleteApp (
AppIdentifier appIdentifier,
Main main
) throws BadPermissionException {
if ( appIdentifier . getAppId (). equals (DEFAULT_APP_ID)) {
throw new CannotDeleteNullAppIdException ();
}
// Must delete all tenants except public first
if ( getAllTenantsForApp (appIdentifier, main). length > 1 ) {
throw new BadPermissionException (
"Please delete all tenants except the public tenant first"
);
}
// Delete app
return deleteAppImplementation (appIdentifier, main);
}
API Domain Configuration
Store per-app website and API domains:
Multitenancy . saveWebsiteAndAPIDomainForApp (
storage, appIdentifier,
"https://example.com" , // websiteDomain
"https://api.example.com" // apiDomain
);
// Retrieve domains
String websiteDomain = Multitenancy . getWebsiteDomain (storage, appIdentifier);
String apiDomain = Multitenancy . getAPIDomain (storage, appIdentifier);
Feature Flag Requirement
Multi-tenancy requires the MULTI_TENANCY feature flag to be enabled: if ( Arrays . stream ( FeatureFlag . getInstance (main, appIdentifier)
. getEnabledFeatures ())
. noneMatch (feature -> feature == EE_FEATURES . MULTI_TENANCY )) {
throw new FeatureNotEnabledException ( EE_FEATURES . MULTI_TENANCY );
}
Use Cases
B2B SaaS Each customer gets their own tenant with isolated data and branding
White-Label Apps Different apps for different brands using the same codebase
Regional Isolation Separate tenants for different geographic regions
Development Environments Separate tenants for dev, staging, and production
Best Practices
Use Meaningful IDs : Choose descriptive tenant IDs like customer-acme instead of UUIDs
Plan User Pool Boundaries : Users within an app share a pool - plan accordingly
Minimize Config Overrides : Only override what’s necessary per tenant
Handle Tenant Not Found : Always catch TenantOrAppNotFoundException gracefully
Use Public Tenant for Global Operations : Store app-wide settings in the public tenant