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.
Example 2: Tenants use their sub domain to login
In this UX flow, all tenants login using their assigned sub domain (customer1.example.com
, customer2.example.com
and so on). The login method that's shown on the login page on each sub domain depends on that tenant's tenantId
configuration.
important
Throughout this page, we will assume that the tenant ID for a tenant is equal to their sub domain - so if the sub domain assigned to a tenant is customer1.example.com
, then their tenantId is customer1
.
An example app for this setup with our pre built UI can be found on our github example dir. The app is setup to have three tenants:
tenant1.example.com
: Login with emailpassword + Google sign intenant2.example.com
: Login with emailPasswordtenant3.example.com
: Login with passwordless + Github sign in
#
Step 1: Creating a new tenantWhenever you want to onboard a new customer, you should create and configure a tenantId for them in the SuperTokens core.
websiteDomain
#
Step 2: Change CORS setting and #
CORS setup- In order for the browser to be able to make requests to the backend, the CORS setting on the backend needs to reflect the right set of allowed origins. For example, if you have
customer1.example.com
on the frontend, then the CORS setting on the backend should allowcustomer1.example.com
as an allowed origin. You can specifically whitelist the set of frontend sub domains on the backend, or you can use a regex like*.example.com
.
websiteDomain
setup#
- On the frontend, set the
websiteDomain
towindow.location.origin
- On the backend, you should set the
websiteDomain
to be your main domain (example.com
if your sub domains aresub.example.com
), and then you want to override thesendEmail
functions to change the domain of the link dynamically based on the tenant ID supplied to thesendEmail
function. See the Email Delivery section in our docs for how to override thesendEmail
function.
#
Step 3: Load login methods dynamically on the frontend based on the tenantIdModify the SuperTokens.init
to do the following:
- Set the
usesDynamicLoginMethods
to true. This will tell our frontend SDK that the login page is based on the tenantId and to fetch the tenant config from the backend before showing any login UI. - Initialize the
Multitenancy
recipe and providegetTenantId
config function.
- ReactJS
- Angular
- Vue
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
usesDynamicLoginMethods: true,
recipeList: [
// Other recipes..
(window as any).supertokensUISession.init(),
(window as any).supertokensUIMultitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: async () => {
// We treat the sub domain as the tenant ID
return window.location.host.split('.')[0]
}
}
}
},
})
]
});
import React from 'react';
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import Passwordless from "supertokens-auth-react/recipe/passwordless";
import Session from "supertokens-auth-react/recipe/session";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
usesDynamicLoginMethods: true,
recipeList: [
// Other recipes..
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: async () => {
// We treat the sub domain as the tenant ID
return window.location.host.split('.')[0]
}
}
}
},
})
]
});
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
usesDynamicLoginMethods: true,
recipeList: [
// Other recipes..
(window as any).supertokensUISession.init(),
(window as any).supertokensUIMultitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: async () => {
// We treat the sub domain as the tenant ID
return window.location.host.split('.')[0]
}
}
}
},
})
]
});
#
Step 4: Tell SuperTokens about tenant's sub domainsWe want to restrict users to only be able to access their (sub)domains. SuperTokens makes it easy for you to do this. We start by telling SuperTokens which domain each tenantId has access to:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens from "supertokens-node";
import Multitenancy from "supertokens-node/recipe/multitenancy"
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
Multitenancy.init({
getAllowedDomainsForTenantId: async (tenantId, userContext) => {
// query your db to get the allowed domain for the input tenantId
// or you can make the tenantId equal to the sub domain itself
return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"]
}
}),
// other recipes...
]
})
import (
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
"github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
multitenancy.Init(&multitenancymodels.TypeInput{
GetAllowedDomainsForTenantId: func(tenantId string, userContext supertokens.UserContext) ([]string, error) {
// query your db to get the allowed domain for the input tenantId
// or you can make the tenantId equal to the sub domain itself
return []string{tenantId + ".myapp.com", "myapp.com", "www.myapp.com"}, nil
},
}),
},
})
}
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import multitenancy
from typing import Dict, Any, List
async def get_allowed_domains_for_tenant_id(tenant_id: str, user_context: Dict[str, Any]) -> List[str]:
return [tenant_id + ".myapp.com", "myapp.com", "www.myapp.com"]
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework="django", # Change this to "flask" or "fastapi" if you are using Flask or FastAPI
recipe_list=[
multitenancy.init(
get_allowed_domains_for_tenant_id=get_allowed_domains_for_tenant_id
)
],
)
The config above will tell SuperTokens to add the list of domains returned by you into the user's session claims once they login. This claim can then be read on the frontend and backend to restrict user's access to the right domain(s).
#
Step 5: Sharing sessions across sub domainsYou may want to allow the user's session to be shearable across sub domains. This would lead to a better UX in which even if they visit the main domain (logged in via a.example.com
, and visit example.com
), the frontend app there can detect if the user has a session or not.
This can be achieved by setting the sessionTokenFrontendDomain
value in the Session recipe.
If the sub domain and the main website domain have different backends (on different sub domains), then you can also enable sharing of sessions across API domains.
note
This is not a security issue because we will anyway be restricting access to users based on their domain allow list as shown below.
#
Step 6: Limiting the user's access to their sub domain.We will be using session claim validators on the frontend to restrict sub domain access. Before proceeding, make sure that you have defined the GetAllowedDomainsForTenantId
function mentioned above. This will add the list of allowed domains into the user's access token payload.
On the frontend, we want to check if the tenant has access to the current sub domain. If not, we want to redirect them to the right sub domain. This can be done by using the hasAccessToCurrentDomain
session validator from the multi tenancy recipe:
- ReactJS
- Angular
- Vue
You will have to make changes to the auth route config, as well as to the supertokens-web-js
SDK config at the root of your application:
This change is in your auth route config.
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUISession.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await (window as any).supertokensUISession.getClaimValue({
claim: supertokensMultitenancy.AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
})
This change goes in the supertokens-web-js
SDK config at the root of your application:
import Session from 'supertokens-web-js/recipe/session';
import { AllowedDomainsClaim } from 'supertokens-web-js/recipe/multitenancy';
Session.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await Session.getClaimValue({
claim: AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
})
Above, in Session.init
on the frontend, we add the hasAccessToCurrentDomain
claim validator to the global validators. This means, that whenever we check protect a route, it will check if hasAccessToCurrentDomain
has passed, and if not, SuperTokens will redirect to the user to their right sub domain (via the values set in the AllowedDomainsClaim
session claim).
import React from "react";
import Session from 'supertokens-auth-react/recipe/session';
import { AllowedDomainsClaim } from 'supertokens-auth-react/recipe/multitenancy';
Session.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await Session.getClaimValue({
claim: AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
})
Above, in Session.init
on the frontend, we add the hasAccessToCurrentDomain
claim validator to the global validators. This means, that whenever we check protect a route, it will check if hasAccessToCurrentDomain
has passed, and if not, SuperTokens will redirect to the user to their right sub domain (via the values set in the AllowedDomainsClaim
session claim).
You will have to make changes to the auth route config, as well as to the supertokens-web-js
SDK config at the root of your application:
This change is in your auth route config.
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUISession.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await (window as any).supertokensUISession.getClaimValue({
claim: supertokensMultitenancy.AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
})
This change goes in the supertokens-web-js
SDK config at the root of your application:
import Session from 'supertokens-web-js/recipe/session';
import { AllowedDomainsClaim } from 'supertokens-web-js/recipe/multitenancy';
Session.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await Session.getClaimValue({
claim: AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
})
Above, in Session.init
on the frontend, we add the hasAccessToCurrentDomain
claim validator to the global validators. This means, that whenever we check protect a route, it will check if hasAccessToCurrentDomain
has passed, and if not, SuperTokens will redirect to the user to their right sub domain (via the values set in the AllowedDomainsClaim
session claim).