BuildWithMatija
  1. Home
  2. Blog
  3. Next.js
  4. Production-Ready Multi-Tenant Setup with Next.js & Payload

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

12th December 2025·Updated on:3rd June 2026··
Next.js
Production-Ready Multi-Tenant Setup with Next.js & Payload

⚡ 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.

No spam. Unsubscribe anytime.

Related Posts:

  • •Dynamic Sitemap & Robots.txt for Next.js Multi-Tenant
  • •Multi-Tenant Development Environment: 4-Step Local Guide
  • •Multi-Tenant SEO with Payload & Next.js — Complete Guide
  • •Active Tenant State Management & Admin Display in Payload CMS
  • •Payload Tenant Display: Persistent Admin Banner Guide
📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

Dynamic Sitemap & Robots.txt for Next.js Multi-Tenant
Dynamic Sitemap & Robots.txt for Next.js Multi-Tenant

15th December 2025

Multi-Tenant Development Environment: 4-Step Local Guide
Multi-Tenant Development Environment: 4-Step Local Guide

16th December 2025

Multi-Tenant SEO with Payload & Next.js — Complete Guide
Multi-Tenant SEO with Payload & Next.js — Complete Guide

14th December 2025

Active Tenant State Management & Admin Display in Payload CMS
Active Tenant State Management & Admin Display in Payload CMS

25th September 2025

Payload Tenant Display: Persistent Admin Banner Guide
Payload Tenant Display: Persistent Admin Banner Guide

17th December 2025

Contents

  • Overview
  • 1. Next.js Configuration (next.config.ts)
  • Key Points:
  • 2. Middleware Implementation (src/middleware.ts)
  • Performance Benefits:
  • 3. Payload CMS Configuration (payload.config.ts)
  • 4. Frontend Route Structure
  • Directory Organization:
  • Page Implementation (src/app/(frontend)/tenant-slugs/[tenant]/[...slug]/page.tsx):
  • Layout Implementation (src/app/(frontend)/tenant-slugs/[tenant]/layout.tsx):
  • Metadata Generation
  • 5. Production Deployment Considerations
  • Environment Variables:
  • Vercel Configuration (vercel.json):
  • 6. Development Environment Setup
  • Local Hosts Configuration:
  • 6. Development Environment Setup
  • Development Middleware Adjustment:
  • 7. Performance Optimization
  • Caching Strategy:
  • CDN Benefits:
  • 8. Scaling Considerations
  • Alternative: Vercel Edge Config
  • 9. Security Best Practices
  • Tenant Isolation Validation:
  • Environment-Specific Configuration:
  • 10. Monitoring and Debugging
  • Tenant Logging:
  • Performance Metrics:
  • Summary
  • Related Guides in This Series
On this page:
  • Overview
  • 1. Next.js Configuration (next.config.ts)
  • 2. Middleware Implementation (src/middleware.ts)
  • 3. Payload CMS Configuration (payload.config.ts)
  • 4. Frontend Route Structure
Build with Matija logo

Build with Matija

Modern websites, content systems, and AI workflows built for long-term growth.

Services

  • Headless CMS Websites
  • Next.js & Headless CMS Advisory
  • AI Systems & Automation
  • Website & Content Audit

Resources

  • Case Studies
  • How I Work
  • Blog
  • CMS Hub
  • E-commerce Hub
  • Dashboard

Headless CMS

  • Payload CMS Developer
  • CMS Migration
  • Multi-Tenant CMS
  • Payload vs Sanity
  • Payload vs WordPress
  • Payload vs Contentful

Get in Touch

Ready to modernize your stack? Let's talk about what you're building.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

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)

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.

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:

code
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 ...
code
getFooter(tenant),
getBusinessInfo(),

]);

return (

{children}
); }

code

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

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