• Home
BuildWithMatija
Get In Touch
  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:25th December 2025·MŽMatija Žiberna·
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

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 (

{children}
); }


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

  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
📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

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

Table of 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

Matija Žiberna

Full Stack Developer specializing in Next.js and TypeScript. Co-founder of We Hate Copy Pasting, building solutions for D2C brands.

Quick Links

About
  • Projects
  • Commands
  • Blog
  • Contact
  • Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Crafting digital experiences with code•All rights reserved