Payload CMS Multi-Tenant Plugin: Complete Guide to Building Multi-Tenant SaaS Apps

Published on by

Execudea

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-tenant

Step 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:

  1. Always validate tenant context in access control functions
  2. Use database-level separation for highly sensitive applications
  3. Implement comprehensive logging to track cross-tenant access attempts
  4. 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

  1. Start with the official plugin and only build custom isolation where it falls short
  2. Start with clear tenant boundaries and data isolation requirements
  3. Implement comprehensive logging for security auditing
  4. Use database indexes strategically for tenant-filtered queries
  5. Plan for tenant onboarding automation from day one
  6. Monitor resource usage per tenant to identify scaling needs
  7. Implement graceful degradation for tenant-specific failures
  8. 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

Building a multi-tenant platform on Payload? Execudea's Payload CMS development team designs and ships multi-tenant SaaS products and agency platforms.

Frequently Asked Questions

What is the Payload CMS multi-tenant plugin?

It is the official Payload 3 plugin for multi-tenancy. It adds a tenant field to chosen collections, a tenant selector in the admin panel, per-user tenant assignments and tenant-scoped access control, so one Payload instance can serve many sites or clients.

What is the difference between database-level and collection-level multi-tenancy in Payload CMS?

Database-level multi-tenancy gives each tenant their own separate MongoDB database, providing maximum data isolation and security. This approach is ideal for enterprise applications with strict compliance requirements. Collection-level multi-tenancy uses a shared database with tenant filtering through a tenant field in each document. While less isolated, it's more cost-effective and easier to manage for most SaaS applications.

How does Payload CMS handle tenant identification in multi-tenant applications?

Payload CMS supports multiple tenant identification methods including subdomain-based routing (tenant1.example.com), custom domains, path-based routing (example.com/tenant1), custom headers, and authentication tokens. Domain or subdomain routing is the most common because it gives clean separation and easier branding per tenant.

Can I migrate an existing single-tenant Payload CMS to multi-tenant architecture?

Yes. Install the multi-tenant plugin, create a tenants collection, assign existing documents to a default tenant with a migration script, then add further tenants. Database-level separation requires a more complex migration but provides stronger isolation.

What are the performance implications of multi-tenancy in Payload CMS?

Multi-tenant Payload CMS can improve efficiency through resource sharing and caching. Index the tenant field on every tenant-scoped collection, use tenant-aware cache tags, and monitor query performance per tenant.

Is multi-tenant Payload CMS suitable for enterprise-level applications?

Yes. With tenant-scoped access control, role-based permissions, audit logging and the option of database-level separation, Payload CMS multi-tenancy supports enterprise use cases and compliance requirements.