### Multi Tenant Plugin
This PR adds a `@payloadcms/plugin-multi-tenant` package. The goal is to
consolidate a source of truth for multi-tenancy. Currently we are
maintaining different implementations for clients, users in discord and
our examples repo. When updates or new paradigms arise we need to
communicate this with everyone and update code examples which is hard to
maintain.
### What does it do?
- adds a tenant selector to the sidebar, above the nav links
- adds a hidden tenant field to every collection that you specify
- adds an array field to your users collection, allowing you to assign
users to tenants
- by default combines the access control (to enabled collections) that
you define, with access control based on the tenants assigned to user on
the request
- by default adds a baseListFilter that filters the documents shown in
the list view with the selected tenant in the admin panel
### What does it not do?
- it does not implement multi-tenancy for your frontend. You will need
to query data for specific tenants to build your website/application
- it does not add a tenants collection, you **NEED** to add a tenants
collection, where you can define what types of fields you would like on
it
### The plugin config
Most of the options listed below are _optional_, but it is easier to
just lay out all of the configuration options.
**TS Type**
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
* @default true
*/
cleanupAfterTenantDelete?: boolean
/**
* Automatically
*/
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Set to `false` if you want to manually apply the baseListFilter
*
* @default true
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
*
* @default true
*/
useTenantAccess?: boolean
}
}
/**
* Enables debug mode
* - Makes the tenant field visible in the admin UI within applicable collections
*
* @default false
*/
debug?: boolean
/**
* Enables the multi-tenant plugin
*
* @default true
*/
enabled?: boolean
/**
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant enabled collections
*
* @default 'tenant'
*/
name?: string
}
/**
* Field configuration for the field added to the users collection
*
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
*/
tenantsArrayField?:
| {
/**
* Access configuration for the array field
*/
arrayFieldAccess?: ArrayField['access']
/**
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
*/
includeDefaultField?: true
/**
* Additional fields to include on the tenants array field
*/
rowFields?: Field[]
/**
* Access configuration for the tenant field
*/
tenantFieldAccess?: RelationshipField['access']
}
| {
arrayFieldAccess?: never
/**
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
tenantFieldAccess?: never
}
/**
* The slug for the tenant collection
*
* @default 'tenants'
*/
tenantsSlug?: string
/**
* Function that determines if a user has access to _all_ tenants
*
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user: User } ? ConfigTypes['user'] : User,
) => boolean
}
```
**Example usage**
```ts
import type { Config } from './payload-types'
import { buildConfig } from 'payload'
export default buildConfig({
plugins: [
multiTenantPlugin<Config>({
collections: {
pages: {},
},
userHasAccessToAllTenants: (user) => isSuperAdmin(user),
}),
],
})
```
### How to configure Collections as Globals for multi-tenant
When using multi-tenant, globals need to actually be configured as
collections so the content can be specific per tenant.
To do that, you can mark a collection with `isGlobal` and it will behave
like a global and users will not see the list view.
```ts
multiTenantPlugin({
collections: {
navigation: {
isGlobal: true,
},
},
})
```
Payload Multi-Tenant Example
This example demonstrates how to achieve a multi-tenancy in Payload. Tenants are separated by a Tenants collection.
Quick Start
To spin up this example locally, follow these steps:
- Run the following command to create a project from the example:
npx create-payload-app --example multi-tenant
pnpm dev,yarn devornpm run devto start the server- Press
ywhen prompted to seed the database
- Press
open http://localhost:3000to access the home pageopen http://localhost:3000/adminto access the admin panel- Login with email
demo@payloadcms.comand passworddemo
- Login with email
How it works
A multi-tenant Payload application is a single server that hosts multiple "tenants". Examples of tenants may be your agency's clients, your business conglomerate's organizations, or your SaaS customers.
Each tenant has its own set of users, pages, and other data that is scoped to that tenant. This means that your application will be shared across tenants but the data will be scoped to each tenant.
Collections
See the Collections docs for details on how to extend any of this functionality.
-
Users
The
userscollection is auth-enabled and encompass both app-wide and tenant-scoped users based on the value of theirrolesandtenantsfields. Users with the rolesuper-admincan manage your entire application, while users with the tenant role ofadminhave limited access to the platform and can manage only the tenant(s) they are assigned to, see Tenants for more details.For additional help with authentication, see the official Auth Example or the Authentication docs.
-
Tenants
A
tenantscollection is used to achieve tenant-based access control. Each user is assigned an array oftenantswhich includes a relationship to atenantand theirroleswithin that tenant. You can then scope any document within your application to any of your tenants using a simple relationship field on theusersorpagescollections, or any other collection that your application needs. The value of this field is used to filter documents in the admin panel and API to ensure that users can only access documents that belong to their tenant and are within their role. See Access Control for more details.For more details on how to extend this functionality, see the Payload Access Control docs.
Domain-based Tenant Setting:
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g.,
gold.localhost.com:3000), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optionalafterLoginhook that sets apayload-tenantcookie based on the domain.The seed script seeds 3 tenants, for the domain portion of the example to function properly you will need to add the following entries to your systems
/etc/hostsfile:- gold.localhost.com:3000
- silver.localhost.com:3000
- bronze.localhost.com:3000
-
Pages
Each page is assigned a
tenant, which is used to control access and scope API requests. Only users with thesuper-adminrole can create pages, and pages are assigned to specific tenants. Other users can view only the pages assigned to the tenant they are associated with.
Access control
Basic role-based access control is setup to determine what users can and cannot do based on their roles, which are:
super-admin: They can access the Payload admin panel to manage your multi-tenant application. They can see all tenants and make all operations.user: They can only access the Payload admin panel if they are a tenant-admin, in which case they have a limited access to operations based on their tenant (see below).
This applies to each collection in the following ways:
users: Only super-admins, tenant-admins, and the user themselves can access their profile. Anyone can create a user, but only these admins can delete users. See Users for more details.tenants: Only super-admins and tenant-admins can read, create, update, or delete tenants. See Tenants for more details.pages: Everyone can access pages, but only super-admins and tenant-admins can create, update, or delete them.
If you have versions and drafts enabled on your pages, you will need to add additional read access control condition to check the user's tenants that prevents them from accessing draft documents of other tenants.
For more details on how to extend this functionality, see the Payload Access Control docs.
CORS
This multi-tenant setup requires an open CORS policy. Since each tenant contains a dynamic list of domains, there's no way to know specifically which domains to whitelist at runtime without significant performance implications. This also means that the serverURL is not set, as this scopes all requests to a single domain.
Alternatively, if you know the domains of your tenants ahead of time and these values won't change often, you could simply remove the domains field altogether and instead use static values.
For more details on this, see the CORS docs.
Front-end
The frontend is scaffolded out in this example directory. You can view the code for rendering pages at /src/app/(app)/[tenant]/[...slug]/page.tsx. This is a starter template, you may need to adjust the app to better fit your needs.
Questions
If you have any issues or questions, reach out to us on Discord or start a GitHub discussion.