---
title: "Production-Ready Multi-Tenant Setup with Next.js & Payload"
slug: "production-ready-multi-tenant-nextjs-payload"
published: "2025-12-12"
updated: "2025-12-25"
categories:
  - "Next.js"
tags:
  - "Next.js multi-tenant setup"
  - "multi-tenant Next.js"
  - "Payload CMS multi-tenant"
  - "Next.js middleware"
  - "tenant isolation"
  - "hardcoded tenant config"
  - "edge middleware"
  - "static generation"
  - "Vercel Edge Config"
  - "Postgres adapter"
  - "tenant-aware collections"
  - "TypeScript"
llm-intent: "how-to"
audience-level: "advanced"
llm-purpose: "Next.js multi-tenant setup with Payload CMS: implement hardcoded tenant routing, edge middleware, and tenant isolation for high-performance enterprise…"
llm-prereqs:
  - "Next.js"
  - "Payload CMS"
  - "TypeScript"
  - "PostgreSQL"
  - "Vercel"
  - "@payloadcms/plugin-multi-tenant"
  - "@payloadcms/db-postgres"
  - "Vercel Edge Config"
---

**Summary Triples**
- (Production-Ready Multi-Tenant Setup with Next.js & Payload, expresses-intent, how-to)
- (Production-Ready Multi-Tenant Setup with Next.js & Payload, covers-topic, Next.js multi-tenant setup)
- (Production-Ready Multi-Tenant Setup with Next.js & Payload, provides-guidance-for, Next.js multi-tenant setup with Payload CMS: implement hardcoded tenant routing, edge middleware, and tenant isolation for high-performance enterprise…)

### {GOAL}
Next.js multi-tenant setup with Payload CMS: implement hardcoded tenant routing, edge middleware, and tenant isolation for high-performance enterprise…

### {PREREQS}
- Next.js
- Payload CMS
- TypeScript
- PostgreSQL
- Vercel
- @payloadcms/plugin-multi-tenant
- @payloadcms/db-postgres
- Vercel Edge Config

### {STEPS}
1. Configure Next.js settings
2. Implement middleware routing
3. Set up Payload multi-tenant
4. Enable admin live preview
5. Build frontend tenant routes
6. Implement static generation
7. Prepare production deployment
8. Harden security and monitoring

<!-- llm:goal="Next.js multi-tenant setup with Payload CMS: implement hardcoded tenant routing, edge middleware, and tenant isolation for high-performance enterprise…" -->
<!-- llm:prereq="Next.js" -->
<!-- llm:prereq="Payload CMS" -->
<!-- llm:prereq="TypeScript" -->
<!-- llm:prereq="PostgreSQL" -->
<!-- llm:prereq="Vercel" -->
<!-- llm:prereq="@payloadcms/plugin-multi-tenant" -->
<!-- llm:prereq="@payloadcms/db-postgres" -->
<!-- llm:prereq="Vercel Edge Config" -->

# Production-Ready Multi-Tenant Setup with Next.js & Payload
> Next.js multi-tenant setup with Payload CMS: implement hardcoded tenant routing, edge middleware, and tenant isolation for high-performance enterprise…
Matija Žiberna · 2025-12-12

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](/blog/payload-cms-multi-tenant-vs-access-control-decision-framework), [SEO](/blog/multi-tenant-seo-payload-nextjs-guide), [State Management](/blog/payload-cms-multi-tenant-state-management), and [Dev Environment](/blog/multi-tenant-dev-environment-nextjs-payload).

## 1. Next.js Configuration (next.config.ts)

```typescript
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:

```typescript
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](/blog/multi-tenant-dev-environment-nextjs-payload).

### 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)

```typescript
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):
```typescript
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):
```typescript
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 (
    <div>
      <Navbar data={navbar} tenant={tenant} />
      {children}
      <Footer data={footer} tenant={tenant} />
    </div>
  );
}
```

### 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):
```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](/blog/multi-tenant-dev-environment-nextjs-payload)

### Development Middleware Adjustment:
```typescript
// 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:
```typescript
// 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
```typescript
// 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:
```typescript
// 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:
```typescript
const allowedHostnames = process.env.ALLOWED_TENANT_DOMAINS?.split(',') || [];
if (!allowedHostnames.includes(hostname) && !isDevelopment) {
  return NextResponse.error();
}
```

## 10. Monitoring and Debugging

### Tenant Logging:
```typescript
// 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:

1. **Maximum Performance**: Hardcoded configuration eliminates runtime lookups
2. **Complete Data Isolation**: Payload CMS plugin ensures database-level separation
3. **Clean URLs**: Domain-based routing with middleware
4. **Type Safety**: Full TypeScript support
5. **Development Efficiency**: Local domain mapping for realistic testing
6. **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](/blog/payload-cms-multi-tenant-vs-access-control-decision-framework) — determine if multi-tenant is right for your project
- **Development Setup:** [4-Step Local Development Guide](/blog/multi-tenant-dev-environment-nextjs-payload) — configure local domains and testing
- **SEO Implementation:** [Complete SEO Guide](/blog/multi-tenant-seo-payload-nextjs-guide) — metadata, OG images, and tenant branding
- **Globals Configuration:** [Tenant-Specific Globals](/blog/how-to-configure-globals-with-multi-tenant-plugin-in-payload-cms) — navbar, footer, and business info per tenant
- **Admin UX:** [Active Tenant State Management](/blog/payload-cms-multi-tenant-state-management) — cookie-based tenant switching
- **Technical SEO:** [Dynamic Sitemap & Robots.txt](/blog/dynamic-sitemap-robots-nextjs-payload-multi-tenant) — tenant-aware SEO files
- **Advanced Routing:** [Multiple Collections on One Route](/blog/payload-cms-multiple-collections-one-route) — route disambiguation patterns