Multi-tenancy has become a cornerstone of modern SaaS application development, and Payload CMS multi-tenancy offers developers a powerful solution for building scalable, cost-effective applications that serve multiple clients from a single codebase. This guide covers the official @payloadcms/plugin-multi-tenant plugin, which is the recommended approach in Payload 3, followed by custom isolation patterns for more advanced requirements.
What is Multi-Tenancy in Payload CMS?
Payload CMS multi-tenancy is an architectural pattern that enables a single Payload instance to serve multiple isolated clients (tenants) while maintaining complete data separation and security. Each tenant operates as if they have their own dedicated CMS instance, but they actually share the same underlying infrastructure, reducing costs and complexity.
In a multi-tenant Payload CMS setup, tenants can include:
- Different companies using your SaaS platform
- Multiple brands under one organization
- Various client websites managed by an agency
- Different environments (staging, production) for the same client
Still choosing a platform? Compare Payload with WordPress Multisite, Directus, Strapi, Sanity and Contentful in our multi-tenant CMS comparison. Moving several Drupal sites into one Payload instance? See our Drupal to Payload CMS migration guide.
The Recommended Approach: The Official Payload Multi-Tenant Plugin
In Payload 3, the official @payloadcms/plugin-multi-tenant plugin handles most multi-tenant requirements out of the box. It adds a tenant field to the collections you choose, adds a tenant selector to the admin panel, filters admin lists by the selected tenant, assigns users to one or more tenants, and scopes access so editors only see their own tenant's content.
Step 1: Install the plugin
pnpm add @payloadcms/plugin-multi-tenantStep 2: Create a Tenants collection
// collections/Tenants.ts
import type { CollectionConfig } from 'payload'
export const Tenants: CollectionConfig = {
slug: 'tenants',
admin: { useAsTitle: 'name' },
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'slug', type: 'text', required: true, unique: true, index: true },
{ name: 'domain', type: 'text', unique: true, index: true },
],
}Step 3: Register the plugin
// payload.config.ts
import { buildConfig } from 'payload'
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
import type { Config } from './payload-types'
export default buildConfig({
collections: [Tenants, Users, Pages, Media, Navigation],
plugins: [
multiTenantPlugin<Config>({
collections: {
pages: {},
media: {},
// One navigation document per tenant
navigation: { isGlobal: true },
},
userHasAccessToAllTenants: (user) =>
Boolean(user?.roles?.includes('super-admin')),
}),
],
// db, secret, editor, etc.
})Each collection listed under collections gets a tenant relationship field and tenant-scoped access. Setting isGlobal: true turns a collection into a per-tenant global, ideal for navigation, footer and site settings. The plugin also adds a tenants array to your users collection so each user can be assigned to specific tenants, while userHasAccessToAllTenants lets super admins manage everything.
Because the tenant is just a relationship field, your tenant-scoped collections follow normal Payload modeling rules. Our Payload CMS content modeling guide covers relationships, join fields and blocks in depth.
Step 4: Resolve the tenant on the frontend
In your Next.js frontend, look up the tenant from the request's domain and filter every query by it:
import { headers } from 'next/headers'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function getTenantPage(slug: string) {
const payload = await getPayload({ config })
const host = (await headers()).get('host')?.replace(/^www[.]/, '')
const { docs: [tenant] } = await payload.find({
collection: 'tenants',
where: { domain: { equals: host } },
limit: 1,
depth: 0,
})
if (!tenant) return null
const { docs: [page] } = await payload.find({
collection: 'pages',
where: {
and: [{ tenant: { equals: tenant.id } }, { slug: { equals: slug } }],
},
limit: 1,
})
return page ?? null
}For most agencies and SaaS products, this shared-database, tenant-field approach is all you need. Use the custom patterns below when you need database-per-tenant isolation or behavior the plugin doesn't cover.
Key Benefits of Multi-Tenant Architecture in Payload CMS
Cost Efficiency and Resource Optimization
Implementing Payload CMS multi-tenancy significantly reduces infrastructure costs by sharing server resources, database connections, and application code across multiple tenants. Instead of maintaining separate Payload instances for each client, you can serve hundreds of tenants from a single deployment.
Simplified Maintenance and Updates
With multi-tenant Payload CMS, you deploy updates, security patches, and new features once for all tenants simultaneously. This eliminates the nightmare of maintaining multiple separate installations and ensures consistency across your entire client base.
Enhanced Scalability
Payload multi-tenancy provides horizontal scalability advantages. As your SaaS grows, adding new tenants doesn't require additional server provisioning or complex deployment processes – new clients can be onboarded through configuration rather than infrastructure changes. For the production setup behind this (caching, revalidation, connection pooling and horizontal scaling), read how Payload CMS scales for high-traffic applications.
Centralized Monitoring and Analytics
A multi-tenant Payload setup allows for centralized logging, monitoring, and analytics across all tenants while maintaining data privacy and isolation.
Custom Implementation Strategies for Advanced Multi-Tenancy
The examples in this section show the underlying patterns behind tenant isolation. Some use the Express-based setup from Payload 2; in Payload 3, equivalent logic belongs in access control functions, hooks and Next.js middleware.
1. Database-Level Tenant Isolation
The most secure approach to Payload CMS multi-tenancy involves giving each tenant their own MongoDB database:
// payload.config.js
import { buildConfig } from 'payload/config';
export default buildConfig({
// Dynamic database connection based on tenant
db: mongooseAdapter({
url: process.env.DATABASE_URI,
migrationDir: path.resolve(__dirname, 'migrations'),
connect: {
// Dynamic database selection
useNewUrlParser: true,
useUnifiedTopology: true,
}
}),
// Tenant identification middleware
express: {
middleware: [
(req, res, next) => {
// Extract tenant from subdomain
const subdomain = req.get('host').split('.')[0];
req.tenant = subdomain;
// Switch database connection
req.payload.db.connection = getTenantDatabase(subdomain);
next();
}
]
}
});2. Collection-Level Multi-Tenancy
For Payload multi-tenant applications where database-level separation isn't required, you can implement tenant filtering at the collection level:
// collections/Posts.js
const Posts = {
slug: 'posts',
fields: [
{
name: 'tenantId',
type: 'text',
required: true,
access: {
create: () => false, // Prevent manual setting
update: () => false,
},
hooks: {
beforeChange: [
({ req }) => {
return req.tenant; // Auto-set tenant ID
}
]
}
},
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'content',
type: 'richText',
}
],
// Tenant-based access control
access: {
read: ({ req }) => {
return {
tenantId: {
equals: req.tenant
}
};
},
create: ({ req }) => Boolean(req.tenant),
update: ({ req }) => ({
tenantId: {
equals: req.tenant
}
}),
delete: ({ req }) => ({
tenantId: {
equals: req.tenant
}
})
}
};
export default Posts;3. Subdomain-Based Tenant Routing
Implement Payload CMS multi-tenancy with subdomain routing for clean tenant separation:
// middleware/tenantResolver.js
export const resolveTenant = (req, res, next) => {
const host = req.get('host');
const subdomain = host.split('.')[0];
// Validate tenant exists
const tenant = findTenantBySubdomain(subdomain);
if (!tenant) {
return res.status(404).json({
error: 'Tenant not found'
});
}
// Attach tenant context to request
req.tenant = tenant.id;
req.tenantConfig = tenant.config;
next();
};Advanced Multi-Tenancy Features in Payload CMS
Dynamic Collection Configuration
Multi-tenant Payload CMS supports dynamic collection configuration per tenant:
// Dynamic collection based on tenant
const getDynamicCollections = (tenantId) => {
const tenantConfig = getTenantConfiguration(tenantId);
return tenantConfig.collections.map(collection => ({
slug: collection.slug,
fields: collection.fields,
access: {
read: () => ({ tenantId: { equals: tenantId } }),
create: () => Boolean(tenantId),
update: () => ({ tenantId: { equals: tenantId } }),
delete: () => ({ tenantId: { equals: tenantId } })
}
}));
};Tenant-Specific Admin Panel Customization
Customize the admin interface per tenant in your Payload multi-tenant setup:
// Custom admin configuration per tenant
export const getTenantAdminConfig = (tenantId) => {
const tenant = getTenant(tenantId);
return {
meta: {
titleSuffix: `- ${tenant.name}`,
favicon: tenant.favicon || '/default-favicon.ico',
},
css: tenant.customCSS || '/default-admin.css',
components: {
Nav: tenant.customNav || DefaultNav,
}
};
};Tenant-Based File Storage
Implement isolated file storage for each tenant:
// Tenant-specific upload configuration
const getUploadConfig = (tenantId) => ({
staticDir: path.resolve(__dirname, `../uploads/${tenantId}`),
staticURL: `/uploads/${tenantId}`,
imageSizes: getTenantImageSizes(tenantId),
});Security Considerations for Payload CMS Multi-Tenancy
Data Isolation Best Practices
Ensuring complete data isolation is crucial in multi-tenant Payload CMS applications:
- Always validate tenant context in access control functions
- Use database-level separation for highly sensitive applications
- Implement comprehensive logging to track cross-tenant access attempts
- Regular security audits of tenant isolation mechanisms
// Robust tenant validation
const validateTenantAccess = ({ req, id }) => {
const document = findDocumentById(id);
if (document.tenantId !== req.tenant) {
throw new Forbidden('Access denied: Invalid tenant');
}
return true;
};Authentication and Authorization
Implement tenant-aware authentication:
// Tenant-scoped JWT tokens
const generateTenantToken = (user, tenantId) => {
return jwt.sign({
id: user.id,
email: user.email,
tenant: tenantId,
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24) // 24 hours
}, process.env.JWT_SECRET);
};Performance Optimization for Multi-Tenant Payload CMS
Database Query Optimization
Optimize queries in your Payload multi-tenancy implementation:
// Efficient tenant filtering with indexes
db.collection.createIndex({
tenantId: 1,
createdAt: -1
});
// Optimized query with tenant filtering
const findTenantPosts = async (tenantId, page = 1, limit = 10) => {
return await payload.find({
collection: 'posts',
where: {
tenantId: { equals: tenantId }
},
page,
limit,
sort: '-createdAt'
});
};Caching Strategies
Implement tenant-aware caching:
// Tenant-specific cache keys
const getCacheKey = (collection, tenantId, query) => {
return `${collection}:${tenantId}:${JSON.stringify(query)}`;
};
// Cached tenant data retrieval
const getCachedTenantData = async (tenantId, collection, query) => {
const cacheKey = getCacheKey(collection, tenantId, query);
let data = cache.get(cacheKey);
if (!data) {
data = await payload.find({
collection,
where: { tenantId: { equals: tenantId }, ...query }
});
cache.set(cacheKey, data, 300); // 5 minutes
}
return data;
};Migration and Scaling Strategies
Database Migration for Multi-Tenancy
When migrating existing Payload CMS installations to multi-tenant architecture:
// Migration script for adding tenantId to existing documents
const migrateSingleTenantToMultiTenant = async (defaultTenantId) => {
const collections = ['posts', 'pages', 'media'];
for (const collection of collections) {
await payload.db.connection.collection(collection).updateMany(
{ tenantId: { $exists: false } },
{ $set: { tenantId: defaultTenantId } }
);
}
};Horizontal Scaling
Scale your multi-tenant Payload CMS horizontally:
// Load balancer configuration for tenant routing
const tenantLoadBalancer = {
algorithm: 'tenant-aware',
routes: [
{
pattern: /^(tenant1|tenant2)[.]/,
target: 'server-cluster-1'
},
{
pattern: /^(tenant3|tenant4)[.]/,
target: 'server-cluster-2'
}
]
};Common Challenges and Solutions
Challenge 1: Cross-Tenant Data Leakage
Solution: Implement comprehensive access control testing:
// Automated testing for tenant isolation
describe('Tenant Isolation', () => {
it('should prevent cross-tenant data access', async () => {
const tenant1Post = await createTestPost('tenant1');
const tenant2User = await authenticateUser('tenant2');
await expect(
payload.findByID({
collection: 'posts',
id: tenant1Post.id,
req: { user: tenant2User, tenant: 'tenant2' }
})
).rejects.toThrow('Access denied');
});
});Challenge 2: Configuration Management
Solution: Use environment-based tenant configuration:
// Tenant configuration management
const getTenantConfig = (tenantId) => {
const configPath = `./configs/tenants/${tenantId}.json`;
if (!fs.existsSync(configPath)) {
return getDefaultTenantConfig();
}
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
};Best Practices for Payload CMS Multi-Tenancy
- Start with the official plugin and only build custom isolation where it falls short
- Start with clear tenant boundaries and data isolation requirements
- Implement comprehensive logging for security auditing
- Use database indexes strategically for tenant-filtered queries
- Plan for tenant onboarding automation from day one
- Monitor resource usage per tenant to identify scaling needs
- Implement graceful degradation for tenant-specific failures
- Design for easy tenant migration between database strategies
Conclusion
Payload CMS multi-tenancy provides a robust foundation for building scalable SaaS applications with isolated tenant data and shared infrastructure benefits. For most projects, the official multi-tenant plugin is the fastest and safest starting point; database-level separation remains an option for applications with strict compliance requirements.
The key to successful multi-tenant Payload CMS implementation lies in careful planning of your tenant isolation strategy, robust security measures, and performance optimization from the start. With proper implementation, you can build applications that scale efficiently while maintaining the security and isolation that enterprise clients demand.
Related Payload CMS Guides
- Best Multi-Tenant CMS: Payload vs WordPress Multisite vs Directus vs Strapi vs Sanity vs Contentful
- PayloadCMS Multi-Tenant Development: Architecture and Per-Tenant Sitemaps
- Payload CMS Content Modeling: Handling Complex, Relational Data Structures
- How Payload CMS Scales for High-Traffic Applications in Production
- How to Migrate from Drupal to Payload CMS: A Step-by-Step Guide
- How to Build a Dynamic Sitemap in Payload CMS
Building a multi-tenant platform on Payload? Execudea's Payload CMS development team designs and ships multi-tenant SaaS products and agency platforms.