Production-Ready Multi-Tenant Setup with Next.js & Payload
Hardcoded tenant routing with Next.js middleware and Payload CMS for isolated, high-performance enterprise deployments

⚡ Next.js Implementation Guides
In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
Related Posts:
This guide focuses on implementing a high-performance, hardcoded multi-tenant architecture with minimal overhead for predetermined tenants. Perfect for enterprise applications with a fixed number of tenants.
Overview
The architecture combines Next.js middleware-based routing with Payload CMS's multi-tenant plugin to achieve complete data isolation while maintaining optimal performance through hardcoded tenant configuration.
Note: This is the master guide for building a production-ready multi-tenant application. For specialized topics, see our dedicated guides on Decision Framework, SEO, State Management, and Dev Environment.
1. Next.js Configuration (next.config.ts)
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Tenant routing handled by middleware for better performance
// No rewrites needed here - middleware handles everything
experimental: {
serverActions: {
bodySizeLimit: '500mb',
},
},
// Optional: Image optimization for tenant-specific domains
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**.yourdomain.com", // Your tenant domains
},
],
},
};
export default nextConfig;
Key Points:
- No rewrites in config: Middleware handles all routing more efficiently
- Domain whitelisting: Pre-configure tenant domains for image optimization
- Performance: Middleware runs at edge before Next.js routing
2. Middleware Implementation (src/middleware.ts)
This is the core of the tenant routing system:
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const url = req.nextUrl;
const { pathname } = url;
const hostname = req.headers.get('host') || '';
// ===== HARDCODED TENANT CONFIGURATION =====
// This approach eliminates database lookups for maximum performance
const tenantConfig = {
// Production tenants
'tenant1.yourdomain.com': {
slug: 'tenant1',
name: 'Tenant One',
previewMode: true
},
'tenant2.yourdomain.com': {
slug: 'tenant2',
name: 'Tenant Two',
previewMode: true
},
// Development tenants
'tenant1.localhost:3000': {
slug: 'tenant1',
name: 'Tenant One Dev',
previewMode: true
},
'tenant2.localhost:3000': {
slug: 'tenant2',
name: 'Tenant Two Dev',
previewMode: true
},
// Default fallback (optional)
'localhost:3000': {
slug: 'tenant1',
name: 'Default Dev',
previewMode: true
}
};
const tenant = tenantConfig[hostname];
const isPreview = url.searchParams.get('preview') === 'true';
// ===== ROUTING LOGIC =====
if (tenant &&
!pathname.startsWith('/_next') &&
!pathname.startsWith('/api') &&
!pathname.startsWith('/admin') &&
!pathname.includes('.')) {
// Route to appropriate handler based on preview mode
const routePrefix = tenant.previewMode && isPreview
? 'tenant-slugs-preview'
: 'tenant-slugs';
const slugPath = pathname === '/' ? '/home' : pathname;
// Rewrite to tenant-specific route
const newUrl = new URL(
`/${routePrefix}/${tenant.slug}${slugPath}`,
req.url
);
// Preserve query parameters
newUrl.search = url.search;
return NextResponse.rewrite(newUrl);
}
return NextResponse.next();
}
export const config = {
matcher: [
// Match all paths except static assets and API routes
'/((?!api|_next|_static|_vercel|[\\w-]+\\.\\w+).*)',
],
};
For local development setup with domain mapping and testing, see Multi-Tenant Development Environment: 4-Step Local Guide.
Performance Benefits:
- Zero database lookups: Hardcoded configuration eliminates runtime queries
- Edge execution: Runs at CDN edge for minimal latency
- Type safety: Full TypeScript support for tenant configuration
3. Payload CMS Configuration (payload.config.ts)
import { buildConfig } from 'payload';
import { postgresAdapter } from '@payloadcms/db-postgres';
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant';
const config = buildConfig({
// Database configuration
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URL,
},
migrationDir: './src/migrations',
}),
// Multi-tenant plugin configuration
plugins: [
multiTenantPlugin({
enabled: true,
debug: process.env.NODE_ENV === 'development',
cleanupAfterTenantDelete: true,
tenantsSlug: 'tenants',
// Tenant field access control
tenantField: {
access: {
read: ({ req }) => !!req.user,
update: ({ req }) => req.user?.roles?.includes('super-admin'),
create: ({ req }) => req.user?.roles?.includes('super-admin'),
},
},
// User-tenant relationship
tenantsArrayField: {
includeDefaultField: true,
arrayFieldName: 'tenants',
arrayTenantFieldName: 'tenant',
},
// Super admin access
userHasAccessToAllTenants: (user) =>
user?.roles?.includes('super-admin'),
// Tenant-aware collections
collections: {
// List all collections that should be tenant-isolated
pages: {},
posts: {},
media: {},
products: {},
users: {}, // Users can be assigned to specific tenants
// ... other collections
},
}),
],
// Admin preview configuration
admin: {
livePreview: {
url: ({ data, req }) => {
if (!data?.tenant) return null;
const tenantSlug = typeof data.tenant === 'string'
? data.tenant
: data.tenant?.slug;
if (!tenantSlug) return null;
const protocol = req?.protocol || 'https';
const host = req?.host || 'yourdomain.com';
return `${protocol}//${host}/tenant-slugs-preview/${tenantSlug}`;
},
collections: ['pages', 'posts', 'products'], // Preview-enabled collections
},
},
});
export default config;
4. Frontend Route Structure
Directory Organization:
src/app/
├── (frontend)/
│ ├── tenant-slugs/
│ │ └── [tenant]/
│ │ ├── layout.tsx # Tenant-specific layout
│ │ └── [...slug]/
│ │ └── page.tsx # Dynamic page handler
│ └── tenant-slugs-preview/
│ └── [tenant]/
│ └── [...slug]/
│ └── page.tsx # Preview mode handler
Page Implementation (src/app/(frontend)/tenant-slugs/[tenant]/[...slug]/page.tsx):
import { notFound } from "next/navigation";
import { queryPageBySlug } from "@/payload/db";
type Props = {
params: Promise<{ slug?: string[]; tenant: string }>;
};
export default async function TenantPage({ params: paramsPromise }: Props) {
const { slug, tenant } = await paramsPromise;
// Normalize slug (handle root path)
const normalizedSlug = !slug || slug.length === 0 ? ["home"] : slug;
// Query page with tenant context
const page = await queryPageBySlug({
slug: normalizedSlug,
tenant,
draft: false
});
if (!page) {
return notFound();
}
// Render page blocks
return <RenderPageBlocks blocks={page.layout} />;
}
// Static generation with tenant context
export async function generateStaticParams() {
// Generate static paths for all tenants
const tenants = ['tenant1', 'tenant2']; // Hardcoded list
const paths = [];
for (const tenant of tenants) {
// Get tenant-specific pages
const pages = await getTenantPages(tenant);
paths.push(...pages.map(page => ({
tenant,
slug: page.slug.split('/')
})));
}
return paths;
}
Layout Implementation (src/app/(frontend)/tenant-slugs/[tenant]/layout.tsx):
export default async function TenantLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ tenant: string }>;
}) {
const { tenant } = await params;
// Fetch tenant-specific data
const [navbar, footer, businessInfo] = await Promise.all([
## 3. Payload Configuration (payload.config.ts)
Configure the multi-tenant plugin to enforce data isolation.
> **Related:** For a deep dive on handling tenant-specific globals (like navbars and footers), see [How to Configure Globals with Multi-Tenant Plugin](/blog/how-to-configure-globals-with-multi-tenant-plugin-in-payload-cms).
```typescript
// ... existing config ...
getFooter(tenant),
getBusinessInfo(),
]);
return (
### Metadata Generation
**For SEO metadata generation** across routes with tenant-specific titles, descriptions, and image URLs, see [Multi-Tenant SEO with Payload & Next.js — Complete Guide](/blog/multi-tenant-seo-payload-nextjs-guide).
## 5. Production Deployment Considerations
### Environment Variables:
```bash
# Database
## 4. Middleware & Frontend Route Structure
> **Advanced Routing:** If you need to serve multiple collections (e.g., Pages and Posts) from the same route root, check out [Multiple Collections on One Route](/blog/payload-cms-multiple-collections-one-route).
The frontend needs to know which tenant is active to fetch the correct data.
# Payload
PAYLOAD_SECRET=your-secret-key
# Tenant domains (optional, for validation)
ALLOWED_TENANT_DOMAINS=tenant1.yourdomain.com,tenant2.yourdomain.com
Vercel Configuration (vercel.json):
{
"framework": "nextjs",
"regions": ["iad1"], // Single region for consistency
"functions": {
"src/middleware.ts": {
"runtime": "edge"
}
}
}
6. Development Environment Setup
Local Hosts Configuration:
6. Development Environment Setup
For complete local development configuration including hosts file setup, domain mapping, and testing procedures, see the dedicated guide: Multi-Tenant Development Environment: 4-Step Local Guide
Development Middleware Adjustment:
// In src/middleware.ts, add development detection
const isDevelopment = process.env.NODE_ENV === 'development';
const tenantConfig = {
// Production domains
...(isDevelopment ? {} : {
'tenant1.yourdomain.com': { slug: 'tenant1', previewMode: true },
'tenant2.yourdomain.com': { slug: 'tenant2', previewMode: true },
}),
// Development domains
...(isDevelopment ? {
'tenant1.localhost:3000': { slug: 'tenant1', previewMode: true },
'tenant2.localhost:3000': { slug: 'tenant2', previewMode: true },
} : {}),
## 5. Production Considerations
> **SEO & Sitemaps:** For generating tenant-aware `sitemap.xml` and `robots.txt`, see the [Dynamic Sitemap Guide](/blog/dynamic-sitemap-robots-nextjs-payload-multi-tenant).
When deploying to production (e.g., Vercel), ensure:
7. Performance Optimization
Caching Strategy:
// In database functions
export const getTenantPages = unstable_cache(
async (tenant: string) => {
// Fetch tenant-specific pages
},
[`tenant-pages`],
{
tags: ['pages'],
revalidate: 3600, // 1 hour
}
);
CDN Benefits:
- Middleware runs at edge locations
- Static generation per tenant
- Tenant-specific caching strategies
8. Scaling Considerations
For larger tenant counts (50+), consider:
Alternative: Vercel Edge Config
// Instead of hardcoded config
import { get } from '@vercel/edge-config';
const tenant = await get(`tenants.${hostname}`);
Benefits:
- Zero deployment updates for tenant changes
- Global replication
- Sub-millisecond lookups
But this is overkill for fixed tenant lists where hardcoded configuration provides better performance and type safety.
9. Security Best Practices
Tenant Isolation Validation:
// In API routes
export async function GET(req: NextRequest) {
const tenant = req.headers.get('x-tenant');
if (!isValidTenant(tenant)) {
return new Response('Invalid tenant', { status: 400 });
}
// Proceed with tenant-scoped query
}
Environment-Specific Configuration:
const allowedHostnames = process.env.ALLOWED_TENANT_DOMAINS?.split(',') || [];
if (!allowedHostnames.includes(hostname) && !isDevelopment) {
return NextResponse.error();
}
10. Monitoring and Debugging
Tenant Logging:
// In middleware
console.log(`[Tenant Routing] ${hostname} -> ${tenant?.slug || 'not-found'}`);
// In page handlers
console.log(`[Page Request] Tenant: ${tenant}, Path: ${pathname}`);
// In database queries
console.log(`[DB Query] Fetching page for tenant: ${tenant}, slug: ${slug}`);
Performance Metrics:
- Track middleware execution time
- Monitor cache hit rates per tenant
- Measure database query performance with tenant filters
Summary
This production-ready multi-tenant setup provides:
- Maximum Performance: Hardcoded configuration eliminates runtime lookups
- Complete Data Isolation: Payload CMS plugin ensures database-level separation
- Clean URLs: Domain-based routing with middleware
- Type Safety: Full TypeScript support
- Development Efficiency: Local domain mapping for realistic testing
- Scalability: Easy to add new tenants without code changes
The approach is ideal for enterprise applications with a predetermined number of tenants, offering the best balance of performance, security, and maintainability.
Related Guides in This Series
- Before You Start: Multi-Tenant vs Access Control Decision Framework — determine if multi-tenant is right for your project
- Development Setup: 4-Step Local Development Guide — configure local domains and testing
- SEO Implementation: Complete SEO Guide — metadata, OG images, and tenant branding
- Globals Configuration: Tenant-Specific Globals — navbar, footer, and business info per tenant
- Admin UX: Active Tenant State Management — cookie-based tenant switching
- Technical SEO: Dynamic Sitemap & Robots.txt — tenant-aware SEO files
- Advanced Routing: Multiple Collections on One Route — route disambiguation patterns
Frequently Asked Questions
Comments
No comments yet
Be the first to share your thoughts on this post!
You might be interested in

15th December 2025

16th December 2025

14th December 2025

25th September 2025

17th December 2025