This is a complete multi-tenant website example built with Payload CMS and the official @payloadcms/plugin-multi-tenant plugin. Use it to power multi-brand websites, white-label platforms, or any application that needs to serve different content and styling based on custom domains.
This example demonstrates:
- Domain-based tenant routing - Serve different brands on different domains
- Per-tenant branding - Custom colors, typography, logos, and styling for each brand
- Tenant-scoped content - Pages, posts, and media isolated per tenant
- Shared amenities - Reusable content across tenants
- Role-based access control - Super admins can manage all tenants, regular users see only their assigned tenants
- Multi-Tenant Plugin
- Domain-Based Routing
- Custom Branding Per Tenant
- Tenant-Scoped Collections
- Role-Based Access Control
- Layout Builder
- Draft Preview & Live Preview
- SEO & Search
To spin up this example locally, follow these steps:
- Node.js ^18.20.2 || >=20.9.0
- pnpm ^9 || ^10
- MongoDB (local or remote instance)
-
Clone this repository:
git clone <repository-url> cd multi-tenant-example
-
Copy the environment variables:
cp .env.example .env
-
Update your
.envfile with your MongoDB connection string:DATABASE_URI=mongodb://localhost:27017/multi-tenant-example PAYLOAD_SECRET=your-secret-here -
Install dependencies:
pnpm install
-
Start the development server:
pnpm dev
-
Open
http://localhost:3000in your browser -
Create your first admin user through the on-screen instructions
After logging in, you'll need to create at least one tenant (brand):
-
Navigate to the Clients collection in the admin panel
-
Create a new client with:
- Name: Your brand name (e.g., "Luxe Hotels")
- Slug: URL-friendly identifier (e.g., "luxe-hotels")
- Domain: The domain for this brand (e.g., "luxe-hotels.local" for local development)
- Brand Colors: Customize primary, secondary, accent colors
- Typography: Choose heading and body fonts
- Logo: Upload a brand logo
- SEO Metadata: Set site title, description, and default OG image
-
For local development, add the domain to your
/etc/hostsfile:sudo echo "127.0.0.1 luxe-hotels.local" >> /etc/hosts
-
Access your tenant at
http://luxe-hotels.local:3000
Alternatively, for quick testing, use the tenant query parameter: http://localhost:3000?tenant=luxe-hotels
This example is built on Payload's official multi-tenant plugin, which provides:
The multi-tenant plugin is configured in src/payload.config.ts:
multiTenantPlugin<Config>({
tenantsSlug: 'clients',
tenantSelectorLabel: 'Brand',
collections: {
pages: {
/* tenant-scoped */
},
posts: {
/* tenant-scoped */
},
header: { isGlobal: true },
footer: { isGlobal: true },
},
userHasAccessToAllTenants: (user) => {
return user?.roles?.includes('super-admin') || false
},
})This example uses Next.js rewrites to transform domain-based requests into path-based routes. The configuration in next.config.js extracts the domain from the request and rewrites it internally:
How it works:
- A request to
luxe-hotels.local/aboutis internally rewritten to/luxe-hotels.local/about - Next.js matches this against the
[tenant]dynamic route insrc/app/(frontend)/[tenant] - The tenant domain is passed as a route parameter to page components
- Pages query Payload using
'tenant.domain': { equals: tenant }to filter content
Benefits:
- No middleware or custom headers required
- Native Next.js functionality using rewrites
- Supports static generation with
generateStaticParams
The rewrite excludes admin, API, and static asset routes, ensuring they function normally.
The clients collection (src/collections/Clients/index.ts) stores tenant configuration:
General Settings:
- Name, slug, and domain
- Logo and tagline
Brand Colors:
- Primary, secondary, and accent colors
- Background, foreground, and border colors
Typography:
- Heading font (Inter, Playfair Display, Montserrat, Raleway, Poppins)
- Body font (Inter, Open Sans, Lato, Roboto, Source Sans 3)
SEO & Metadata:
- Site title and description
- Open Graph image
- Custom favicon
Contact Information:
- Email and phone
Tenant-scoped pages with layout builder blocks. Each tenant can have their own unique pages with custom layouts.
Tenant-scoped blog posts and articles. Content is isolated per tenant.
Upload collection for images, videos, and other assets. Supports:
- Multiple image sizes
- Focal point selection
- Manual resizing
A shared collection for reusable amenities (e.g., "Free WiFi", "Swimming Pool") that can be referenced across tenants. This demonstrates how to have both tenant-scoped and shared content in the same application.
Icons available: WiFi, Parking, Pool, Gym, Restaurant, Room Service, Spa, Pet Friendly, Air Conditioning, Conference Room, Bar, Laundry, Airport Shuttle, Concierge, Safe.
Taxonomy for grouping posts. Supports nested categories using the Nested Docs Plugin.
Auth-enabled collection with role support:
- Regular users: Access only their assigned tenant(s)
- Super admins: Access all tenants
Tenant-scoped global for navigation and header content. Each tenant can customize their navigation menu.
Tenant-scoped global for footer content. Each tenant can customize their footer links and information.
Each tenant can customize their brand appearance:
- Colors: 6 customizable color values (primary, secondary, accent, background, foreground, border)
- Typography: Separate font choices for headings and body text
- Logo: Custom logo per tenant
- Favicon: Custom favicon per tenant
- SEO: Default meta information per tenant
The frontend can access this branding information and apply it dynamically using CSS custom properties or Tailwind configuration.
The example implements role-based access control:
Users with the super-admin role can:
- Access all tenants
- Create and manage tenants
- View and edit content across all brands
Regular users can only:
- Access tenants they're assigned to
- Create and manage content within their tenant(s)
- Cannot see other tenants' content
- Published pages and posts are publicly accessible
- Media files are publicly accessible
- Tenant (client) configuration is readable (for domain lookup)
- Amenities are publicly readable
Pages and posts use Payload's layout builder with pre-configured blocks:
- Hero - Eye-catching header sections
- Content - Rich text content with Lexical editor
- Media - Image and video blocks
- Call To Action - Conversion-focused sections
- Archive - Dynamic content listings
Each block is designed to work with tenant branding.
All pages and posts support draft previews with Versions:
- Create drafts without publishing
- Preview unpublished changes
- Automatic revalidation on publish
Uses the Payload SEO Plugin for complete SEO control:
- Meta titles and descriptions
- Open Graph tags
- Twitter Card support
- Automatic sitemap generation
Implements the Payload Search Plugin for full-text search:
- Search across pages and posts
- Tenant-scoped search results
- SSR search functionality
You can seed the database with demo data:
- Click the "Seed Database" button in the admin panel, or
- Run the seed script programmatically
This will create:
- Demo clients/tenants
- Sample pages and posts
- Demo user accounts
- Example amenities
NOTICE: Seeding is destructive and will drop your current database. Only run this on a fresh project.
This example is ideal for:
- Multi-brand websites - Manage multiple brands from a single CMS
- White-label platforms - Offer customized sites to different clients
- Agency portfolios - Manage multiple client sites
- Franchise systems - Individual sites for each franchise location
- Multi-region sites - Different content/branding per region
- SaaS platforms - Provide branded customer portals
If you have any issues or questions, reach out to us on:
MIT