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
* Base path for your application
4
*
5
* https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath
6
*
7
* @default undefined
8
*/
9
basePath?: string
10
/**
11
* After a tenant is deleted, the plugin will attempt to clean up related documents
12
* - removing documents with the tenant ID
13
* - removing the tenant from users
14
*
15
* @default true
16
*/
17
cleanupAfterTenantDelete?: boolean
18
/**
19
* Automatically
20
*/
21
collections: {
22
[key in CollectionSlug]?: {
23
/**
24
* Set to `true` if you want the collection to behave as a global
25
*
26
* @default false
27
*/
28
isGlobal?: boolean
29
/**
30
* Opt out of adding the tenant field and place
31
* it manually using the `tenantField` export from the plugin
32
*/
33
customTenantField?: boolean
34
/**
35
* Overrides for the tenant field, will override the entire tenantField configuration
36
*/
37
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
38
/**
39
* Set to `false` if you want to manually apply the baseListFilter
40
* Set to `false` if you want to manually apply the baseFilter
41
*
42
* @default true
43
*/
44
useBaseFilter?: boolean
45
/**
46
* @deprecated Use `useBaseFilter` instead. If both are defined,
47
* `useBaseFilter` will take precedence. This property remains only
48
* for backward compatibility and may be removed in a future version.
49
*
50
* Originally, `baseListFilter` was intended to filter only the List View
51
* in the admin panel. However, base filtering is often required in other areas
52
* such as internal link relationships in the Lexical editor.
53
*
54
* @default true
55
*/
56
useBaseListFilter?: boolean
57
/**
58
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
59
*
60
* @default true
61
*/
62
useTenantAccess?: boolean
63
}
64
}
65
/**
66
* Enables debug mode
67
* - Makes the tenant field visible in the admin UI within applicable collections
68
*
69
* @default false
70
*/
71
debug?: boolean
72
/**
73
* Enables the multi-tenant plugin
74
*
75
* @default true
76
*/
77
enabled?: boolean
78
/**
79
* Localization for the plugin
80
*/
81
i18n?: {
82
translations: {
83
[key in AcceptedLanguages]?: {
84
/**
85
* Shown inside 3 dot menu on edit document view
86
*
87
* @default 'Assign Tenant'
88
*/
89
'assign-tenant-button-label'?: string
90
/**
91
* Shown as the title of the assign tenant modal
92
*
93
* @default 'Assign "{{title}}"'
94
*/
95
'assign-tenant-modal-title'?: string
96
/**
97
* Shown as the label for the assigned tenant field in the assign tenant modal
98
*
99
* @default 'Assigned Tenant'
100
*/
101
'field-assignedTenant-label'?: string
102
/**
103
* Shown as the label for the global tenant selector in the admin UI
104
*
105
* @default 'Filter by Tenant'
106
*/
107
'nav-tenantSelector-label'?: string
108
}
109
}
110
}
111
/**
112
* Field configuration for the field added to all tenant enabled collections
113
*/
114
tenantField?: RootTenantFieldConfigOverrides
115
/**
116
* Field configuration for the field added to the users collection
117
*
118
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
119
* This is useful if you want to customize the field or place the field in a specific location
120
*/
121
tenantsArrayField?:
122
| {
123
/**
124
* Access configuration for the array field
125
*/
126
arrayFieldAccess?: ArrayField['access']
127
/**
128
* Name of the array field
129
*
130
* @default 'tenants'
131
*/
132
arrayFieldName?: string
133
/**
134
* Name of the tenant field
135
*
136
* @default 'tenant'
137
*/
138
arrayTenantFieldName?: string
139
/**
140
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
141
*/
142
includeDefaultField?: true
143
/**
144
* Additional fields to include on the tenants array field
145
*/
146
rowFields?: Field[]
147
/**
148
* Access configuration for the tenant field
149
*/
150
tenantFieldAccess?: RelationshipField['access']
151
}
152
| {
153
arrayFieldAccess?: never
154
arrayFieldName?: string
155
arrayTenantFieldName?: string
156
/**
157
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
158
*/
159
includeDefaultField?: false
160
rowFields?: never
161
tenantFieldAccess?: never
162
}
163
/**
164
* Customize tenant selector label
165
*
166
* Either a string or an object where the keys are i18n codes and the values are the string labels
167
*
168
* @deprecated Use `i18n.translations` instead.
169
*/
170
tenantSelectorLabel?:
171
| Partial<{
172
[key in AcceptedLanguages]?: string
173
}>
174
| string
175
/**
176
* The slug for the tenant collection
177
*
178
* @default 'tenants'
179
*/
180
tenantsSlug?: string
181
/**
182
* Function that determines if a user has access to _all_ tenants
183
*
184
* Useful for super-admin type users
185
*/
186
userHasAccessToAllTenants?: (
187
user: ConfigTypes extends { user: unknown }
188
? ConfigTypes['user']
189
: TypedUser,
190
) => boolean
191
/**
192
* Opt out of adding access constraints to the tenants collection
193
*/
194
useTenantsCollectionAccess?: boolean
195
/**
196
* Opt out including the baseListFilter to filter tenants by selected tenant
197
*/
198
useTenantsListFilter?: boolean
199
/**
200
* Opt out including the baseListFilter to filter users by selected tenant
201
*/
202
useUsersTenantFilter?: boolean
203
}

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