Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Multi-Tenant Plugin

https://www.npmjs.com/package/@payloadcms/plugin-multi-tenant

This plugin sets up multi-tenancy for your application from within your Admin Panel. It does so by adding a tenant field to all specified collections. Your front-end application can then query data by tenant. You must add the Tenants collection so you control what fields are available for each tenant.

Core features

  • Adds a tenant field to each specified collection
  • Adds a tenant selector to the admin panel, allowing you to switch between tenants
  • Filters list view results by selected tenant
  • Filters relationship fields by selected tenant
  • Ability to create "global" like collections, 1 doc per tenant
  • Automatically assign a tenant to new documents

Installation

Install the plugin using any JavaScript package manager like pnpm, npm, or Yarn:

1
pnpm add @payloadcms/plugin-multi-tenant

Options

The plugin accepts an object with the following properties:

1
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
2
/**
3
* After a tenant is deleted, the plugin will attempt to clean up related documents
4
* - removing documents with the tenant ID
5
* - removing the tenant from users
6
*
7
* @default true
8
*/
9
cleanupAfterTenantDelete?: boolean
10
/**
11
* Automatically
12
*/
13
collections: {
14
[key in CollectionSlug]?: {
15
/**
16
* Set to `true` if you want the collection to behave as a global
17
*
18
* @default false
19
*/
20
isGlobal?: boolean
21
/**
22
* Set to `false` if you want to manually apply the baseListFilter
23
*
24
* @default true
25
*/
26
useBaseListFilter?: boolean
27
/**
28
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
29
*
30
* @default true
31
*/
32
useTenantAccess?: boolean
33
}
34
}
35
/**
36
* Enables debug mode
37
* - Makes the tenant field visible in the admin UI within applicable collections
38
*
39
* @default false
40
*/
41
debug?: boolean
42
/**
43
* Enables the multi-tenant plugin
44
*
45
* @default true
46
*/
47
enabled?: boolean
48
/**
49
* Field configuration for the field added to all tenant enabled collections
50
*/
51
tenantField?: {
52
access?: RelationshipField['access']
53
/**
54
* The name of the field added to all tenant enabled collections
55
*
56
* @default 'tenant'
57
*/
58
name?: string
59
}
60
/**
61
* Field configuration for the field added to the users collection
62
*
63
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
64
* This is useful if you want to customize the field or place the field in a specific location
65
*/
66
tenantsArrayField?:
67
| {
68
/**
69
* Access configuration for the array field
70
*/
71
arrayFieldAccess?: ArrayField['access']
72
/**
73
* Name of the array field
74
*
75
* @default 'tenants'
76
*/
77
arrayFieldName?: string
78
/**
79
* Name of the tenant field
80
*
81
* @default 'tenant'
82
*/
83
arrayTenantFieldName?: string
84
/**
85
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
86
*/
87
includeDefaultField?: true
88
/**
89
* Additional fields to include on the tenants array field
90
*/
91
rowFields?: Field[]
92
/**
93
* Access configuration for the tenant field
94
*/
95
tenantFieldAccess?: RelationshipField['access']
96
}
97
| {
98
arrayFieldAccess?: never
99
arrayFieldName?: string
100
arrayTenantFieldName?: string
101
/**
102
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
103
*/
104
includeDefaultField?: false
105
rowFields?: never
106
tenantFieldAccess?: never
107
}
108
/**
109
* Customize tenant selector label
110
*
111
* Either a string or an object where the keys are i18n codes and the values are the string labels
112
*/
113
tenantSelectorLabel?:
114
| Partial<{
115
[key in AcceptedLanguages]?: string
116
}>
117
| string
118
/**
119
* The slug for the tenant collection
120
*
121
* @default 'tenants'
122
*/
123
tenantsSlug?: string
124
/**
125
* Function that determines if a user has access to _all_ tenants
126
*
127
* Useful for super-admin type users
128
*/
129
userHasAccessToAllTenants?: (
130
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
131
) => boolean
132
/**
133
* Opt out of adding access constraints to the tenants collection
134
*/
135
useTenantsCollectionAccess?: boolean
136
/**
137
* Opt out including the baseListFilter to filter tenants by selected tenant
138
*/
139
useTenantsListFilter?: boolean
140
/**
141
* Opt out including the baseListFilter to filter users by selected tenant
142
*/
143
useUsersTenantFilter?: boolean
144
}

Basic Usage

In the plugins array of your Payload Config, call the plugin with options:

1
import { buildConfig } from 'payload'
2
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
3
import type { Config } from './payload-types'
4
5
const config = buildConfig({
6
collections: [
7
{
8
slug: 'tenants',
9
admin: {
10
useAsTitle: 'name'
11
}
12
fields: [
13
// remember, you own these fields
14
// these are merely suggestions/examples
15
{
16
name: 'name',
17
type: 'text',
18
required: true,
19
},
20
{
21
name: 'slug',
22
type: 'text',
23
required: true,
24
},
25
{
26
name: 'domain',
27
type: 'text',
28
required: true,
29
}
30
],
31
},
32
],
33
plugins: [
34
multiTenantPlugin<Config>({
35
collections: {
36
pages: {},
37
navigation: {
38
isGlobal: true,
39
}
40
},
41
}),
42
],
43
})
44
45
export default config

Front end usage

The plugin scaffolds out everything you will need to separate data by tenant. You can use the tenant field to filter data from enabled collections in your front-end application.

In your frontend you can query and constrain data by tenant with the following:

1
const pagesBySlug = await payload.find({
2
collection: 'pages',
3
depth: 1,
4
draft: false,
5
limit: 1000,
6
overrideAccess: false,
7
where: {
8
// your constraint would depend on the
9
// fields you added to the tenants collection
10
// here we are assuming a slug field exists
11
// on the tenant collection, like in the example above
12
'tenant.slug': {
13
equals: 'gold',
14
},
15
},
16
})

NextJS rewrites

Using NextJS rewrites and this route structure /[tenantDomain]/[slug], we can rewrite routes specifically for domains requested:

1
async rewrites() {
2
return [
3
{
4
source: '/((?!admin|api)):path*',
5
destination: '/:tenantDomain/:path*',
6
has: [
7
{
8
type: 'host',
9
value: '(?<tenantDomain>.*)',
10
},
11
],
12
},
13
];
14
}

React Hooks

Below are the hooks exported from the plugin that you can import into your own custom components to consume.

useTenantSelection

You can import this like so:

1
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
2
3
...
4
5
const tenantContext = useTenantSelection()

The hook returns the following context:

1
type ContextType = {
2
/**
3
* Array of options to select from
4
*/
5
options: OptionObject[]
6
/**
7
* The currently selected tenant ID
8
*/
9
selectedTenantID: number | string | undefined
10
/**
11
* Prevents a refresh when the tenant is changed
12
*
13
* If not switching tenants while viewing a "global", set to true
14
*/
15
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
16
/**
17
* Sets the selected tenant ID
18
*
19
* @param args.id - The ID of the tenant to select
20
* @param args.refresh - Whether to refresh the page after changing the tenant
21
*/
22
setTenant: (args: {
23
id: number | string | undefined
24
refresh?: boolean
25
}) => void
26
}

Examples

The Examples Directory also contains an official Multi-Tenant example.

Next

Stripe Plugin