### Improved tenant assignment flow This PR improves the tenant assignment flow. I know a lot of users liked the previous flow where the field was not injected into the document. But the original flow, confused many of users because the tenant filter (top left) was being used to set the tenant on the document _and_ filter the list view. This change shown below is aiming to solve both of those groups with a slightly different approach. As always, feedback is welcome while we try to really make this plugin work for everyone. https://github.com/user-attachments/assets/ceee8b3a-c5f5-40e9-8648-f583e2412199 Added 2 new localization strings: ``` // shown in the 3 dot menu 'assign-tenant-button-label': 'Assign Tenant', // shown when needing to assign a tenant to a NEW document 'assign-tenant-modal-title': 'Assign "{{title}}"', ``` Removed 2 localization strings: ``` 'confirm-modal-tenant-switch--body', 'confirm-modal-tenant-switch--heading' ```
410 lines
11 KiB
Plaintext
410 lines
11 KiB
Plaintext
---
|
|
title: Multi-Tenant Plugin
|
|
label: Multi-Tenant
|
|
order: 50
|
|
desc: Scaffolds multi-tenancy for your Payload application
|
|
keywords: plugins, multi-tenant, multi-tenancy, plugin, payload, cms, seo, indexing, search, search engine
|
|
---
|
|
|
|

|
|
|
|
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.
|
|
|
|
<Banner type="info">
|
|
This plugin is completely open-source and the [source code can be found
|
|
here](https://github.com/payloadcms/payload/tree/main/packages/plugin-multi-tenant).
|
|
If you need help, check out our [Community
|
|
Help](https://payloadcms.com/community-help). If you think you've found a bug,
|
|
please [open a new
|
|
issue](https://github.com/payloadcms/payload/issues/new/choose) with as much
|
|
detail as possible.
|
|
</Banner>
|
|
|
|
## 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
|
|
|
|
<Banner type="error">
|
|
**Warning**
|
|
|
|
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
|
strong access control on your tenants collection to prevent deletions by unauthorized users.
|
|
|
|
You can disable this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
|
|
|
</Banner>
|
|
|
|
## Installation
|
|
|
|
Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com):
|
|
|
|
```bash
|
|
pnpm add @payloadcms/plugin-multi-tenant
|
|
```
|
|
|
|
### Options
|
|
|
|
The plugin accepts an object with the following properties:
|
|
|
|
```ts
|
|
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
|
/**
|
|
* Base path for your application
|
|
*
|
|
* https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath
|
|
*
|
|
* @default undefined
|
|
*/
|
|
basePath?: string
|
|
/**
|
|
* 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
|
|
/**
|
|
* Opt out of adding the tenant field and place
|
|
* it manually using the `tenantField` export from the plugin
|
|
*/
|
|
customTenantField?: boolean
|
|
/**
|
|
* Overrides for the tenant field, will override the entire tenantField configuration
|
|
*/
|
|
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
|
|
/**
|
|
* Set to `false` if you want to manually apply the baseListFilter
|
|
* Set to `false` if you want to manually apply the baseFilter
|
|
*
|
|
* @default true
|
|
*/
|
|
useBaseFilter?: boolean
|
|
/**
|
|
* @deprecated Use `useBaseFilter` instead. If both are defined,
|
|
* `useBaseFilter` will take precedence. This property remains only
|
|
* for backward compatibility and may be removed in a future version.
|
|
*
|
|
* Originally, `baseListFilter` was intended to filter only the List View
|
|
* in the admin panel. However, base filtering is often required in other areas
|
|
* such as internal link relationships in the Lexical editor.
|
|
*
|
|
* @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
|
|
/**
|
|
* Localization for the plugin
|
|
*/
|
|
i18n?: {
|
|
translations: {
|
|
[key in AcceptedLanguages]?: {
|
|
/**
|
|
* Shown inside 3 dot menu on edit document view
|
|
*
|
|
* @default 'Assign Tenant'
|
|
*/
|
|
'assign-tenant-button-label'?: string
|
|
/**
|
|
* Shown as the title of the assign tenant modal
|
|
*
|
|
* @default 'Assign "{{title}}"'
|
|
*/
|
|
'assign-tenant-modal-title'?: string
|
|
/**
|
|
* Shown as the label for the assigned tenant field in the assign tenant modal
|
|
*
|
|
* @default 'Assigned Tenant'
|
|
*/
|
|
'field-assignedTenant-label'?: string
|
|
/**
|
|
* Shown as the label for the global tenant selector in the admin UI
|
|
*
|
|
* @default 'Filter by Tenant'
|
|
*/
|
|
'nav-tenantSelector-label'?: string
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Field configuration for the field added to all tenant enabled collections
|
|
*/
|
|
tenantField?: RootTenantFieldConfigOverrides
|
|
/**
|
|
* 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']
|
|
/**
|
|
* Name of the array field
|
|
*
|
|
* @default 'tenants'
|
|
*/
|
|
arrayFieldName?: string
|
|
/**
|
|
* Name of the tenant field
|
|
*
|
|
* @default 'tenant'
|
|
*/
|
|
arrayTenantFieldName?: string
|
|
/**
|
|
* 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
|
|
arrayFieldName?: string
|
|
arrayTenantFieldName?: string
|
|
/**
|
|
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
|
|
*/
|
|
includeDefaultField?: false
|
|
rowFields?: never
|
|
tenantFieldAccess?: never
|
|
}
|
|
/**
|
|
* Customize tenant selector label
|
|
*
|
|
* Either a string or an object where the keys are i18n codes and the values are the string labels
|
|
*
|
|
* @deprecated Use `i18n.translations` instead.
|
|
*/
|
|
tenantSelectorLabel?:
|
|
| Partial<{
|
|
[key in AcceptedLanguages]?: string
|
|
}>
|
|
| string
|
|
/**
|
|
* 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: unknown }
|
|
? ConfigTypes['user']
|
|
: TypedUser,
|
|
) => boolean
|
|
/**
|
|
* Opt out of adding access constraints to the tenants collection
|
|
*/
|
|
useTenantsCollectionAccess?: boolean
|
|
/**
|
|
* Opt out including the baseListFilter to filter tenants by selected tenant
|
|
*/
|
|
useTenantsListFilter?: boolean
|
|
/**
|
|
* Opt out including the baseListFilter to filter users by selected tenant
|
|
*/
|
|
useUsersTenantFilter?: boolean
|
|
}
|
|
```
|
|
|
|
## Basic Usage
|
|
|
|
In the `plugins` array of your [Payload Config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options):
|
|
|
|
```ts
|
|
import { buildConfig } from 'payload'
|
|
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
|
|
import type { Config } from './payload-types'
|
|
|
|
const config = buildConfig({
|
|
collections: [
|
|
{
|
|
slug: 'tenants',
|
|
admin: {
|
|
useAsTitle: 'name',
|
|
},
|
|
fields: [
|
|
// remember, you own these fields
|
|
// these are merely suggestions/examples
|
|
{
|
|
name: 'name',
|
|
type: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
name: 'slug',
|
|
type: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
name: 'domain',
|
|
type: 'text',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
plugins: [
|
|
multiTenantPlugin<Config>({
|
|
collections: {
|
|
pages: {},
|
|
navigation: {
|
|
isGlobal: true,
|
|
},
|
|
},
|
|
}),
|
|
],
|
|
})
|
|
|
|
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:
|
|
|
|
```tsx
|
|
const pagesBySlug = await payload.find({
|
|
collection: 'pages',
|
|
depth: 1,
|
|
draft: false,
|
|
limit: 1000,
|
|
overrideAccess: false,
|
|
where: {
|
|
// your constraint would depend on the
|
|
// fields you added to the tenants collection
|
|
// here we are assuming a slug field exists
|
|
// on the tenant collection, like in the example above
|
|
'tenant.slug': {
|
|
equals: 'gold',
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
### NextJS rewrites
|
|
|
|
Using NextJS rewrites and this route structure `/[tenantDomain]/[slug]`, we can rewrite routes specifically for domains requested:
|
|
|
|
```ts
|
|
async rewrites() {
|
|
return [
|
|
{
|
|
source: '/((?!admin|api)):path*',
|
|
destination: '/:tenantDomain/:path*',
|
|
has: [
|
|
{
|
|
type: 'host',
|
|
value: '(?<tenantDomain>.*)',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
```
|
|
|
|
### 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:
|
|
|
|
```tsx
|
|
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
|
|
|
|
...
|
|
|
|
const tenantContext = useTenantSelection()
|
|
```
|
|
|
|
The hook returns the following context:
|
|
|
|
```ts
|
|
type ContextType = {
|
|
/**
|
|
* Array of options to select from
|
|
*/
|
|
options: OptionObject[]
|
|
/**
|
|
* The currently selected tenant ID
|
|
*/
|
|
selectedTenantID: number | string | undefined
|
|
/**
|
|
* Prevents a refresh when the tenant is changed
|
|
*
|
|
* If not switching tenants while viewing a "global",
|
|
* set to true
|
|
*/
|
|
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
|
|
/**
|
|
* Sets the selected tenant ID
|
|
*
|
|
* @param args.id - The ID of the tenant to select
|
|
* @param args.refresh - Whether to refresh the page
|
|
* after changing the tenant
|
|
*/
|
|
setTenant: (args: {
|
|
id: number | string | undefined
|
|
refresh?: boolean
|
|
}) => void
|
|
}
|
|
```
|
|
|
|
## Examples
|
|
|
|
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.
|