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

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",
14
* set to true
15
*/
16
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
17
/**
18
* Sets the selected tenant ID
19
*
20
* @param args.id - The ID of the tenant to select
21
* @param args.refresh - Whether to refresh the page
22
* after changing the tenant
23
*/
24
setTenant: (args: {
25
id: number | string | undefined
26
refresh?: boolean
27
}) => void
28
}

Examples

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

Next

Nested Docs Plugin