Skip to content

Commit 813e70b

Browse files
feat: adds multi-tenant plugin (#10447)
### 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, }, }, }) ```
1 parent 592f02b commit 813e70b

File tree

112 files changed

+3459
-909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+3459
-909
lines changed

docs/plugins/multi-tenant.mdx

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
---
2+
title: Multi-Tenant Plugin
3+
label: Multi-Tenant
4+
order: 40
5+
desc: Scaffolds multi-tenancy for your Payload application
6+
keywords: plugins, multi-tenant, multi-tenancy, plugin, payload, cms, seo, indexing, search, search engine
7+
---
8+
9+
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-multi-tenants)](https://www.npmjs.com/package/@payloadcms/plugin-multi-tenants)
10+
11+
This plugin sets up multi-tenancy for your application from within your [Admin Panel](../admin/overview). 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.
12+
13+
14+
<Banner type="info">
15+
This plugin is completely open-source and the [source code can be found
16+
here](https://github.com/payloadcms/payload/tree/main/packages/plugin-multi-tenant). If you need
17+
help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've
18+
found a bug, please [open a new
19+
issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%multi-tenant&template=bug_report.md&title=plugin-multi-tenant%3A)
20+
with as much detail as possible.
21+
</Banner>
22+
23+
## Core features
24+
25+
- Adds a `tenant` field to each specified collection
26+
- Adds a tenant selector to the admin panel, allowing you to switch between tenants
27+
- Filters list view results by selected tenant
28+
29+
## Installation
30+
31+
Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com):
32+
33+
```bash
34+
pnpm add @payloadcms/plugin-multi-tenant@beta
35+
```
36+
37+
### Options
38+
39+
The plugin accepts an object with the following properties:
40+
41+
```ts
42+
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
43+
/**
44+
* After a tenant is deleted, the plugin will attempt to clean up related documents
45+
* - removing documents with the tenant ID
46+
* - removing the tenant from users
47+
*
48+
* @default true
49+
*/
50+
cleanupAfterTenantDelete?: boolean
51+
/**
52+
* Automatically
53+
*/
54+
collections: {
55+
[key in CollectionSlug]?: {
56+
/**
57+
* Set to `true` if you want the collection to behave as a global
58+
*
59+
* @default false
60+
*/
61+
isGlobal?: boolean
62+
/**
63+
* Set to `false` if you want to manually apply the baseListFilter
64+
*
65+
* @default true
66+
*/
67+
useBaseListFilter?: boolean
68+
/**
69+
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
70+
*
71+
* @default true
72+
*/
73+
useTenantAccess?: boolean
74+
}
75+
}
76+
/**
77+
* Enables debug mode
78+
* - Makes the tenant field visible in the admin UI within applicable collections
79+
*
80+
* @default false
81+
*/
82+
debug?: boolean
83+
/**
84+
* Enables the multi-tenant plugin
85+
*
86+
* @default true
87+
*/
88+
enabled?: boolean
89+
/**
90+
* Field configuration for the field added to all tenant enabled collections
91+
*/
92+
tenantField?: {
93+
access?: RelationshipField['access']
94+
/**
95+
* The name of the field added to all tenant enabled collections
96+
*
97+
* @default 'tenant'
98+
*/
99+
name?: string
100+
}
101+
/**
102+
* Field configuration for the field added to the users collection
103+
*
104+
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
105+
* This is useful if you want to customize the field or place the field in a specific location
106+
*/
107+
tenantsArrayField?:
108+
| {
109+
/**
110+
* Access configuration for the array field
111+
*/
112+
arrayFieldAccess?: ArrayField['access']
113+
/**
114+
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
115+
*/
116+
includeDefaultField?: true
117+
/**
118+
* Additional fields to include on the tenants array field
119+
*/
120+
rowFields?: Field[]
121+
/**
122+
* Access configuration for the tenant field
123+
*/
124+
tenantFieldAccess?: RelationshipField['access']
125+
}
126+
| {
127+
arrayFieldAccess?: never
128+
/**
129+
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
130+
*/
131+
includeDefaultField?: false
132+
rowFields?: never
133+
tenantFieldAccess?: never
134+
}
135+
/**
136+
* The slug for the tenant collection
137+
*
138+
* @default 'tenants'
139+
*/
140+
tenantsSlug?: string
141+
/**
142+
* Function that determines if a user has access to _all_ tenants
143+
*
144+
* Useful for super-admin type users
145+
*/
146+
userHasAccessToAllTenants?: (
147+
user: ConfigTypes extends { user } ? ConfigTypes['user'] : User,
148+
) => boolean
149+
}
150+
```
151+
152+
## Basic Usage
153+
154+
In the `plugins` array of your [Payload Config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options):
155+
156+
```ts
157+
import { buildConfig } from 'payload'
158+
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
159+
import type { Config } from './payload-types'
160+
161+
const config = buildConfig({
162+
collections: [
163+
{
164+
slug: 'tenants',
165+
admin: {
166+
useAsTitle: 'name'
167+
}
168+
fields: [
169+
// remember, you own these fields
170+
// these are merely suggestions/examples
171+
{
172+
name: 'name',
173+
type: 'text',
174+
required: true,
175+
},
176+
{
177+
name: 'slug',
178+
type: 'text',
179+
required: true,
180+
},
181+
{
182+
name: 'domain',
183+
type: 'text',
184+
required: true,
185+
}
186+
],
187+
},
188+
],
189+
plugins: [
190+
multiTenantPlugin<Config>({
191+
collections: {
192+
pages: {},
193+
navigation: {
194+
isGlobal: true,
195+
}
196+
},
197+
}),
198+
],
199+
})
200+
201+
export default config
202+
```
203+
204+
## Front end usage
205+
206+
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.
207+
208+
209+
In your frontend you can query and constrain data by tenant with the following:
210+
211+
```tsx
212+
const pagesBySlug = await payload.find({
213+
collection: 'pages',
214+
depth: 1,
215+
draft: false,
216+
limit: 1000,
217+
overrideAccess: false,
218+
where: {
219+
// your constraint would depend on the
220+
// fields you added to the tenants collection
221+
// here we are assuming a slug field exists
222+
// on the tenant collection, like in the example above
223+
'tenant.slug': {
224+
equals: 'gold',
225+
},
226+
},
227+
})
228+
```
229+
230+
### NextJS rewrites
231+
232+
Using NextJS rewrites and this route structure `/[tenantDomain]/[slug]`, we can rewrite routes specifically for domains requested:
233+
234+
```ts
235+
async rewrites() {
236+
return [
237+
{
238+
source: '/((?!admin|api)):path*',
239+
destination: '/:tenantDomain/:path*',
240+
has: [
241+
{
242+
type: 'host',
243+
value: '(?<tenantDomain>.*)',
244+
},
245+
],
246+
},
247+
];
248+
}
249+
```
250+
251+
252+
## Examples
253+
254+
The [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) also contains an official [Multi-Tenant](https://github.com/payloadcms/payload/tree/main/examples/multi-tenant) example.

examples/multi-tenant/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
4040

4141
**Domain-based Tenant Setting**:
4242

43-
This example also supports domain-based tenant selection, where tenants can be associated with specific domains. If a tenant is associated with a domain (e.g., `abc.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 optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
43+
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 optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
4444

45-
By default, this functionality is commented out in the code but can be enabled easily. See the `setCookieBasedOnDomain` hook in the `Users` collection for more details.
45+
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/hosts` file:
46+
47+
- gold.localhost.com:3000
48+
- silver.localhost.com:3000
49+
- bronze.localhost.com:3000
4650

4751
- #### Pages
4852

examples/multi-tenant/next.config.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ import { withPayload } from '@payloadcms/next/withPayload'
33
/** @type {import('next').NextConfig} */
44
const nextConfig = {
55
// Your Next.js config here
6+
async rewrites() {
7+
return [
8+
{
9+
source: '/((?!admin|api))tenant-domains/:path*',
10+
destination: '/tenant-domains/:tenant/:path*',
11+
has: [
12+
{
13+
type: 'host',
14+
value: '(?<tenant>.*)',
15+
},
16+
],
17+
},
18+
]
19+
},
620
}
721

822
export default withPayload(nextConfig)

examples/multi-tenant/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dependencies": {
1919
"@payloadcms/db-mongodb": "latest",
2020
"@payloadcms/next": "latest",
21+
"@payloadcms/plugin-multi-tenant": "file:payloadcms-plugin-multi-tenant-3.15.1.tgz",
2122
"@payloadcms/richtext-lexical": "latest",
2223
"@payloadcms/ui": "latest",
2324
"cross-env": "^7.0.3",

0 commit comments

Comments
 (0)