Paid Feature
This is a paid feature.
For self hosted users, Sign up to get a license key and follow the instructions sent to you by email. Creation of tenants is free on the dev license key.
This feature is already enabled for managed service users. Creation of additional tenant is free on the provided development environment.
Creating and configuring a tenant
#
Step 1: Create a tenant and enable third party login for themThe first step in setting up a multi tenant login system is to create a tenant in the SuperTokens core. Each tenant has a unique tenantId
(a string data type) mapped to that tenant's configuation. The tenantId
could be that tenant's sub domain, or a workspace URL, or anything else using which you can uniquely identify them.
The configuration mapped to each tenant contains information about which login methods are enabled for them. You can create a tenant via our backend SDK or via a cURL command to the core. This also allows you to dynamically create tenants without manual intervention:
- Dashboard
- NodeJS
- GoLang
- Python
- cURL
Important
import Multitenancy from "supertokens-node/recipe/multitenancy";
import { FactorIds } from "supertokens-node/recipe/multifactorauth";
async function createNewTenant() {
let resp = await Multitenancy.createOrUpdateTenant("customer1", {
firstFactors: [FactorIds.OTP_PHONE, FactorIds.OTP_EMAIL, FactorIds.LINK_PHONE, FactorIds.LINK_EMAIL, FactorIds.THIRDPARTY]
});
if (resp.createdNew) {
// new tenant was created
} else {
// existing tenant's config was modified.
}
}
In the example above, we have added all factor IDs related to the passwordless recipe. However, you can choose to use a subset of them. The factor IDs are described below:
- ThirdParty:
FactorIds.THIRDPARTY
- Passwordless:
- With email OTP:
FactorIds.OTP_EMAIL
- With SMS OTP:
FactorIds.OTP_PHONE
- With email magic link:
FactorIds.LINK_EMAIL
- With SMS magic link:
FactorIds.LINK_PHONE
- With email OTP:
import (
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
"github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels"
)
func main() {
tenantId := "customer1"
thirdPartyEnabled := true
passwordlessEnabled := true
resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{
ThirdPartyEnabled: &thirdPartyEnabled,
PasswordlessEnabled: &passwordlessEnabled,
})
if err != nil {
// handle error
}
if resp.OK.CreatedNew {
// new tenant was created
} else {
// existing tenant's config was modified.
}
}
- Asyncio
- Syncio
from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
from supertokens_python.recipe.multifactorauth.types import FactorIds
async def some_func():
response = await create_or_update_tenant(
"customer1",
TenantConfigCreateOrUpdate(
first_factors=[
FactorIds.OTP_PHONE,
FactorIds.OTP_EMAIL,
FactorIds.LINK_PHONE,
FactorIds.LINK_EMAIL,
FactorIds.THIRDPARTY,
]
),
)
if response.status != "OK":
print("Handle error")
elif response.created_new:
print("new tenant was created")
else:
print("existing tenant's config was modified.")
from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
from supertokens_python.recipe.multifactorauth.types import FactorIds
def some_func():
response = create_or_update_tenant(
"customer1",
TenantConfigCreateOrUpdate(
first_factors=[
FactorIds.OTP_PHONE,
FactorIds.OTP_EMAIL,
FactorIds.LINK_PHONE,
FactorIds.LINK_EMAIL,
FactorIds.THIRDPARTY,
]
),
)
if response.status != "OK":
print("Handle error")
elif response.created_new:
print("new tenant was created")
else:
print("existing tenant's config was modified.")
- Single app setup
- Multi app setup
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"firstFactors": ["otp-email", "otp-phone", "link-email", "link-phone", "thirdparty"]
}'
In the example above, we have added all factor IDs related to the passwordless recipe. However, you can choose to use a subset of them. The factor IDs are described below:
- ThirdParty:
thirdparty
- Passwordless:
- With email OTP:
otp-email
- With SMS OTP:
otp-phone
- With email magic link:
link-email
- With SMS magic link:
link-phone
- With email OTP:
curl --location --request PUT '/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"thirdPartyEnabled": true,
"passwordlessEnabled": true
}'
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"firstFactors": ["otp-email", "otp-phone", "link-email", "link-phone", "thirdparty"]
}'
In the example above, we have added all factor IDs related to the passwordless recipe. However, you can choose to use a subset of them. The factor IDs are described below:
- ThirdParty:
thirdparty
- Passwordless:
- With email OTP:
otp-email
- With SMS OTP:
otp-phone
- With email magic link:
link-email
- With SMS magic link:
link-phone
- With email OTP:
curl --location --request PUT '/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"thirdPartyEnabled": true,
"passwordlessEnabled": true
}'
#
Step 2: Configure the third party providers for the tenantOnce you have created a tenant with third party enabled, you need to configure which third party providers to enable for the tenant. We have several in built providers, but you can also configure a custom provider.
Once again, you can add / modify this config dynamically using our backend SDK or using a cURL command.
- Dashboard
- NodeJS
- GoLang
- Python
- cURL
Important
import Multiteancy from "supertokens-node/recipe/multitenancy";
async function createTenant() {
let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", {
thirdPartyId: "active-directory",
name: "Active Directory",
clients: [{
clientId: "...",
clientSecret: "...",
}],
oidcDiscoveryEndpoint: "https://login.microsoftonline.com/<directoryId>/v2.0/.well-known/openid-configuration",
});
if (resp.createdNew) {
// Active Directory added to customer1
} else {
// Existing active directory config overwritten for customer1
}
}
import (
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
"github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels"
)
func main() {
tenantId := "customer1"
resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{
ThirdPartyId: "active-directory",
Name: "Active Directory",
Clients: []tpmodels.ProviderClientConfig{
{
ClientID: "...",
ClientSecret: "...",
},
},
OIDCDiscoveryEndpoint: "https://login.microsoftonline.com/<directoryId>/v2.0/.well-known/openid-configuration",
}, nil, nil)
if err != nil {
// handle error
}
if resp.OK.CreatedNew {
// Active Directory added to customer1
} else {
// Existing active directory config overwritten for customer1
}
}
- Asyncio
- Syncio
from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config
from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig
async def update_tenant():
result = await create_or_update_third_party_config(
"customer1",
config=ProviderConfig(
third_party_id="active-directory",
name="Active Directory",
clients=[
ProviderClientConfig(
client_id="...",
client_secret="...",
)
],
oidc_discovery_endpoint="https://login.microsoftonline.com/<directoryId>/v2.0/.well-known/openid-configuration",
),
)
if result.status != "OK":
print("Error adding active directory to tenant")
elif result.created_new:
print("Active directory was added to the tenant")
else:
print("Existing tenant's active directory config was modified")
from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config
from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig
result = create_or_update_third_party_config(
"customer1",
config=ProviderConfig(
third_party_id="active-directory",
name="Active Directory",
clients=[
ProviderClientConfig(
client_id="...",
client_secret="...",
)
]
),
)
if result.status != "OK":
print("Error creating or updating tenant")
elif result.created_new:
print("New tenant was created")
else:
print("Existing tenant's config was modified")
- Single app setup
- Multi app setup
curl --location --request PUT '/<TENANT_ID>/recipe/multitenancy/config/thirdparty' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"config": {
"thirdPartyId": "active-directory",
"name": "Active Directory",
"clients": [
{
"clientId": "...",
"clientSecret": "..."
}
],
"oidcDiscoveryEndpoint": "https://login.microsoftonline.com/<directoryId>/v2.0/.well-known/openid-configuration"
}
}'
curl --location --request PUT '/<TENANT_ID>/recipe/multitenancy/config/thirdparty' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"config": {
"thirdPartyId": "active-directory",
"name": "Active Directory",
"clients": [
{
"clientId": "...",
"clientSecret": "..."
}
],
"oidcDiscoveryEndpoint": "https://login.microsoftonline.com/<directoryId>/v2.0/.well-known/openid-configuration"
}
}'
- The above code snippet shows how you can add an Active directory login to your tenant. The
clientId
,clientSecret
anddirectoryId
will be provided to you by your tenant. - You can see the required information for other providers on this page.
Next steps
You have now successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users, but we have two common UX flows documented in the next section.
#
Providing additional configuration per tenantYou can also configure a tenant to have different configurations as per the core's config.yaml
(or docker env) variabls. Below is how you can specify the config, when creating or modifying a tenant:
- Dashboard
- NodeJS
- GoLang
- Python
- cURL
Important
import Multitenancy from "supertokens-node/recipe/multitenancy";
async function createNewTenant() {
let resp = await Multitenancy.createOrUpdateTenant("customer1", {
coreConfig: {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2",
}
});
if (resp.createdNew) {
// new tenant was created
} else {
// existing tenant's config was modified.
}
}
In the above example, we are setting different values for certain configs for customer1
tenant. All other configs are inherited from the base config (config.yaml file or docker env vars).
We even specify a postgresql_connection_uri
config. This means that all the information related to this tenant (users, roles, metadata etc) will be saved in the db pointed to by the value of postgresql_connection_uri
(A similar config exists for MySQL as well). This can be used to achieve data isolation on a tenant level. This config is not necessary and if not provided, the tenant's information will be stored in the db as specified in the core's config.yaml or docker env vars (it will still be a different user pool though).
Here is the list of full core config variables that can be configured, and below are the lists of variables depending on the database you use:
important
Some configs cannot be different across tenants - they must be the same within an app. In the above links, if a config has a comment saying DIFFERENT_ACROSS_TENANTS
, then it can be changed for each tenant, else if it has DIFFERENT_ACROSS_APPS
, then it must be the same for all tenants within an app.
If a config has neither of these, then it can only be set per core instance.
import (
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
"github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels"
)
func main() {
tenantId := "customer1"
resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{
CoreConfig: map[string]interface{}{
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2",
},
})
if err != nil {
// handle error
}
if resp.OK.CreatedNew {
// new tenant was created
} else {
// existing tenant's config was modified.
}
}
In the above example, we are setting different values for certain configs for customer1
tenant. All other configs are inherited from the base config (config.yaml file or docker env vars).
We even specify a postgresql_connection_uri
config. This means that all the information related to this tenant (users, roles, metadata etc) will be saved in the db pointed to by the value of postgresql_connection_uri
(A similar config exists for MySQL as well). This can be used to achieve data isolation on a tenant level. This config is not necessary and if not provided, the tenant's information will be stored in the db as specified in the core's config.yaml or docker env vars (it will still be a different user pool though).
Here is the list of full core config variables that can be configured, and below are the lists of variables depending on the database you use:
important
Some configs cannot be different across tenants - they must be the same within an app. In the above links, if a config has a comment saying DIFFERENT_ACROSS_TENANTS
, then it can be changed for each tenant, else if it has DIFFERENT_ACROSS_APPS
, then it must be the same for all tenants within an app.
If a config has neither of these, then it can only be set per core instance.
- Asyncio
- Syncio
from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
async def some_func():
tenant_id = "customer1"
result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate(
core_config={
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2",
},
))
if result.status != "OK":
print("handle error")
elif result.created_new:
print("new tenant created")
else:
print("existing tenant's config was modified.")
from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
tenant_id = "customer1"
result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate(
core_config={
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2",
},
))
if result.status != "OK":
print("handle error")
elif result.created_new:
print("new tenant created")
else:
print("existing tenant's config was modified.")
In the above example, we are setting different values for certain configs for customer1
tenant. All other configs are inherited from the base config (config.yaml file or docker env vars).
We even specify a postgresql_connection_uri
config. This means that all the information related to this tenant (users, roles, metadata etc) will be saved in the db pointed to by the value of postgresql_connection_uri
(A similar config exists for MySQL as well). This can be used to achieve data isolation on a tenant level. This config is not necessary and if not provided, the tenant's information will be stored in the db as specified in the core's config.yaml or docker env vars (it will still be a different user pool though).
Here is the list of full core config variables that can be configured, and below are the lists of variables depending on the database you use:
important
Some configs cannot be different across tenants - they must be the same within an app. In the above links, if a config has a comment saying DIFFERENT_ACROSS_TENANTS
, then it can be changed for each tenant, else if it has DIFFERENT_ACROSS_APPS
, then it must be the same for all tenants within an app.
If a config has neither of these, then it can only be set per core instance.
- Single app setup
- Multi app setup
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
}
}'
curl --location --request PUT '/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
}
}'
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
}
}'
curl --location --request PUT '/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
}
}'
In the above example, we are setting different values for certain configs for customer1
tenant. All other configs are inherited from the base config (config.yaml file or docker env vars).
We even specify a postgresql_connection_uri
config. This means that all the information related to this tenant (users, roles, metadata etc) will be saved in the db pointed to by the value of postgresql_connection_uri
(A similar config exists for MySQL as well). This can be used to achieve data isolation on a tenant level. This config is not necessary and if not provided, the tenant's information will be stored in the db as specified in the core's config.yaml or docker env vars (it will still be a different user pool though).
Here is the list of full core config variables that can be configured, and below are the lists of variables depending on the database you use:
important
Some configs cannot be different across tenants - they must be the same within an app. In the above links, if a config has a comment saying DIFFERENT_ACROSS_TENANTS
, then it can be changed for each tenant, else if it has DIFFERENT_ACROSS_APPS
, then it must be the same for all tenants within an app.
If a config has neither of these, then it can only be set per core instance.
In the above example, we are setting different values for certain configs for customer1
tenant. All other configs are inherited from the base config (config.yaml file or docker env vars).
The values can be edited by clicking on the pencil icon and then specifying a new value.
caution
Database connection settings cannot be edited directly from the Dashboard and you may need to use the SDK or cURL to update them.
Once you have set the configs for a specific tenant, you can fetch the tenant info as shown below:
- NodeJS
- GoLang
- Python
- cURL
Important
import Multitenancy from "supertokens-node/recipe/multitenancy";
async function getTenant(tenantId: string) {
let resp = await Multitenancy.getTenant(tenantId);
if (resp === undefined) {
// tenant does not exist
} else {
let coreConfig = resp.coreConfig;
let firstFactors = resp.firstFactors;
let configuredThirdPartyProviders = resp.thirdParty.providers;
}
}
import (
"fmt"
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
)
func main() {
tenantId := "customer1"
tenant, err := multitenancy.GetTenant(tenantId)
if err != nil {
// handle error
}
if tenant == nil {
// tenant does not exist
} else {
isEmailPasswordLoginEnabled := tenant.EmailPassword.Enabled;
isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled;
isPasswordlessLoginEnabled := tenant.Passwordless.Enabled;
if (isEmailPasswordLoginEnabled) {
// Tenant support email password login
}
if (isThirdPartyLoginEnabled) {
// Tenant support third party login
configuredThirdPartyProviders := tenant.ThirdParty.Providers;
fmt.Println(configuredThirdPartyProviders);
}
if (isPasswordlessLoginEnabled) {
// Tenant support passwordless login
}
}
}
- Asyncio
- Syncio
from supertokens_python.recipe.multitenancy.asyncio import get_tenant
async def some_func():
tenant = await get_tenant("customer1")
if tenant is None:
print("tenant does not exist")
else:
core_config = tenant.core_config
first_factors = tenant.first_factors
providers = tenant.third_party_providers
print(core_config)
print(first_factors)
print(providers)
from supertokens_python.recipe.multitenancy.syncio import get_tenant
tenant = get_tenant("customer1")
if tenant is None:
print("tenant does not exist")
else:
core_config = tenant.core_config
first_factors = tenant.first_factors
providers = tenant.third_party_providers
print(core_config)
print(first_factors)
print(providers)
- Single app setup
- Multi app setup
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request GET '/customer1/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json'
Notice that we added customer1
to the path of the request. This tells the core that the tenant you want to get the information about is customer1
(the one we created before in this page).
If the input tenant does not exist, you will get back a 200
status code with the following JSON:
{"status": "TENANT_NOT_FOUND_ERROR"}
Otherwise you will get a 200
status code with the following JSON output:
{
"status": "OK",
"thirdParty": {
"providers": [...]
},
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
},
"tenantId": "customer1",
"firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"]
}
curl --location --request GET '/customer1/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json'
Notice that we added customer1
to the path of the request. This tells the core that the tenant you want to get the information about is customer1
(the one we created before in this page).
If the input tenant does not exist, you will get back a 200
status code with the following JSON:
{"status": "TENANT_NOT_FOUND_ERROR"}
Otherwise you will get a 200
status code with the following JSON output:
{
"status": "OK",
"emailPassword": {
"enabled": true
},
"thirdParty": {
"enabled": true,
"providers": [...]
},
"passwordless": {
"enabled": true
},
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
},
"tenantId": "customer1",
}
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request GET '/customer1/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json'
Notice that we added customer1
to the path of the request. This tells the core that the tenant you want to get the information about is customer1
(the one we created before in this page).
If the input tenant does not exist, you will get back a 200
status code with the following JSON:
{"status": "TENANT_NOT_FOUND_ERROR"}
Otherwise you will get a 200
status code with the following JSON output:
{
"status": "OK",
"thirdParty": {
"providers": [...]
},
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
},
"tenantId": "customer1",
"firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"]
}
curl --location --request GET '/customer1/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json'
Notice that we added customer1
to the path of the request. This tells the core that the tenant you want to get the information about is customer1
(the one we created before in this page).
If the input tenant does not exist, you will get back a 200
status code with the following JSON:
{"status": "TENANT_NOT_FOUND_ERROR"}
Otherwise you will get a 200
status code with the following JSON output:
{
"status": "OK",
"emailPassword": {
"enabled": true
},
"thirdParty": {
"enabled": true,
"providers": [...]
},
"passwordless": {
"enabled": true
},
"coreConfig": {
"email_verification_token_lifetime": 7200000,
"password_reset_token_lifetime": 3600000,
"postgresql_connection_uri": "postgresql://localhost:5432/db2"
},
"tenantId": "customer1",
}
The returned coreConfig
is the same as what we had set when creating / updating the tenant. The rest of the core configurations for this tenant are inherited from the app's (or the public
tenant) config. The public
tenant, for the public
app inherits its configs from the config.yaml
/ docker env var values.