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 1: Tenants use a common domain to login
In this UX flow, all tenants login using the same page (like https://example.com/auth
) and are redirected to their sub domain after login. The login method that's shown on the login page depend on the tenant's tenantId
configuration.
The process of getting the tenantId
from the user is left up to you, but a common method is to ask the user to enter their organisation name (which is equal to the tenantId
that you configure in SuperTokens).
important
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
: Login with emailpassword + Google sign intenant2
: Login with emailPasswordtenant3
: Login with passwordless + Github sign in
You can also generate a demo app using the following command:
npx create-supertokens-app@latest --recipe=multitenancy
#
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.
#
Step 2: Ask for the tenant ID on the login pageIf you have followed the pre built UI setup, when you visit the login screen, you immediately see the login screen. We want to change the flow so that the user is first asked to enter their tenant ID and then the login UI is shown based on the tenant ID.
In order to do that, we must first get the tenant ID from the user. This can be done by building a UI that will ask them to enter their tenantId or Organisation name (which can be used as the tenant ID). We build this UI in a component called AuthPage
(as shown below):
- ReactJS
- Angular
- Vue
caution
No code snippet provided here, however, if you visit the auth component, you will see that we are rendering the pre built UI in the "supertokensui"
div
element on page load. The logic here needs to change to first check if the user has provided us with the tenantId. If they have, we render the SuperTokens UI as usual. If they have not, we render a simple UI which asks the user for their tenant id and save that in localstorage.
Switch to the React code tab here to see how this is implemented in React, and a similar logic can be followed here.
- We render a simple UI which asks the user for their organisation's name. Their input will be treated as their tenant ID.
- Once the user has submitted that form, we will store their input in localstorage. This value will be read later on (as shown below) by SuperTokens to render the right login method based on the saved tenantId.
- In case the tenantID exists in localstorage, we render the SuperTokens UI as usual.
- We render the tenant form if the user has not yet provided us with the information about which tenant they belong to. In all our flows, we get the tenantId threw ways via:
- If the user had previously etnered the tenantId and it's stored in localstorage
- If there already exists a session - in which case we get the tenantId from the session in the pre built components.
- If there is a query param called
tenantId
- in which case we get the tenantId from the query param in the pre built components. This query param will be there in email verification, password reset, or magic links.
- In the code above, we render a box in which the user can enter their tenant ID / org name, but you can also make it a drop down or any other UI that you want - the key is that their input should be saveed in localstorage so that it can be read later
important
We want to render the AuthPage
component to show on /auth/*
paths of the website.
The AuthPage
will replace the call to getSuperTokensRoutesForReactRouterDom
or getRoutingComponent
that you may have added to your app from the quick setup section.
caution
No code snippet provided here, however, if you visit the auth component, you will see that we are rendering the pre built UI in the "supertokensui"
div
element on page load. The logic here needs to change to first check if the user has provided us with the tenantId. If they have, we render the SuperTokens UI as usual. If they have not, we render a simple UI which asks the user for their tenant id and save that in localstorage.
Switch to the React code tab here to see how this is implemented in React, and a similar logic can be followed here.
#
Step 3: Tell SuperTokens about the saved tenantId from the previous stepInitialise the multi tenancy recipe with the following callback which reads from the browser's localstorage to get the previously saved tenantId
:
- 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: "...",
apiDomain: "...",
websiteDomain: "...",
apiBasePath: "...",
websiteBasePath: "..."
},
usesDynamicLoginMethods: true,
recipeList: [
(window as any).supertokensUIMultitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
let tid = localStorage.getItem("tenantId");
return tid === null ? undefined : tid;
}
}
}
}
})
// other recipes...
]
});
important
We also set the usesDynamicLoginMethods
to true
which tells SuperTokens that the login methods are dynamic (based on the tenantId). This means that on page load (of the login page), SuperTokens will first fetch the configured login methods for the tenantId and display the login UI based on the result of the API call.
import React from 'react';
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
apiBasePath: "...",
websiteBasePath: "..."
},
usesDynamicLoginMethods: true,
recipeList: [
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
let tid = localStorage.getItem("tenantId");
return tid === null ? undefined : tid;
}
}
}
}
})
// other recipes...
]
});
important
We also set the usesDynamicLoginMethods
to true
which tells SuperTokens that the login methods are dynamic (based on the tenantId). This means that on page load (of the login page), SuperTokens will first fetch the configured login methods for the tenantId and display the login UI based on the result of the API call.
// 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: "...",
apiDomain: "...",
websiteDomain: "...",
apiBasePath: "...",
websiteBasePath: "..."
},
usesDynamicLoginMethods: true,
recipeList: [
(window as any).supertokensUIMultitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
let tid = localStorage.getItem("tenantId");
return tid === null ? undefined : tid;
}
}
}
}
})
// other recipes...
]
});
important
We also set the usesDynamicLoginMethods
to true
which tells SuperTokens that the login methods are dynamic (based on the tenantId). This means that on page load (of the login page), SuperTokens will first fetch the configured login methods for the tenantId and display the login UI based on the result of the API call.
#
Step 4: (Optional) Tell SuperTokens about tenant's sub domainsYou may have a flow in which each tenant has access to specific sub domains in your application. So after login, you would not only want to redirect those users to their sub domain, but you also want to restrict which sub domains they have access to.
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: (Optional) Redirect the user to their sub domain post sign inOn the frontend side, post sign in, by default, our frontend SDK will redirect the user to the /
route. You can change this to instead redirect them to their sub domain based on their tenantId.
- 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: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
getRedirectionURL: async (context) => {
if (context.action === "SUCCESS" && context.newSessionCreated) {
let claimValue: string[] | undefined = await (window as any).supertokensUISession.getClaimValue({
claim: (window as any).supertokensUIMultitenancy.AllowedDomainsClaim
});
if (claimValue !== undefined) {
window.location.href = "https://" + claimValue[0];
} else {
// there was no configured allowed domain for this user. Throw an error cause of
// misconfig or redirect to a default sub domain
}
}
return undefined;
},
recipeList: [ /* Recipe init here... */ ]
});
import SuperTokens from "supertokens-auth-react";
import Session from "supertokens-auth-react/recipe/session";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
getRedirectionURL: async (context) => {
if (context.action === "SUCCESS" && context.newSessionCreated) {
let claimValue: string[] | undefined = await Session.getClaimValue({
claim: Multitenancy.AllowedDomainsClaim
});
if (claimValue !== undefined) {
window.location.href = "https://" + claimValue[0];
} else {
// there was no configured allowed domain for this user. Throw an error cause of
// misconfig or redirect to a default sub domain
}
}
return undefined;
},
recipeList: [ /* Recipe init here... */ ]
});
// 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: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
getRedirectionURL: async (context) => {
if (context.action === "SUCCESS" && context.newSessionCreated) {
let claimValue: string[] | undefined = await (window as any).supertokensUISession.getClaimValue({
claim: (window as any).supertokensUIMultitenancy.AllowedDomainsClaim
});
if (claimValue !== undefined) {
window.location.href = "https://" + claimValue[0];
} else {
// there was no configured allowed domain for this user. Throw an error cause of
// misconfig or redirect to a default sub domain
}
}
return undefined;
},
recipeList: [ /* Recipe init here... */ ]
});
- The
AllowedDomainsClaim
claim is auto added to the session by our backend SDK if you provide theGetAllowedDomainsForTenantId
config from the previous step. - This claim contains a list of domains that are allowed for this user, based on their tenant ID.
#
Step 6: (Optional) Sharing sessions across sub domainsSince the user logged into your main website domain (https://example.com/auth
), and are being redirected to their sub domain, we need to configure the session recipe to allow sharing of sessions across sub domains. This can be achieved by setting the sessionTokenFrontendDomain
value in the Session recipe.
If the sub domains assigned to your tenants have their own backend, on a separate sub domain (one per tenant), then you can also enable sharing of sessions across API domains.
#
Step 7: (Optional) 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).