In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
This guide explains how to set up a local development environment that mimics production for multi-tenant applications using Payload CMS and Next.js.
Overview
When building multi-tenant applications, you need to test how different tenants (domains) behave in production. Instead of using subdirectories or query parameters, this setup allows you to:
Access different tenants via local domains (e.g., tenant-a.local, tenant-b.local)
Test production-like URL structures locally
Ensure middleware, routing, and tenant resolution work exactly like production
Debug domain-specific features without deploying
Quick Start Checklist
✅ Edit /etc/hosts → add local domains
✅ Start dev server on port 80 (or configure port)
✅ Access http://tenant-a.local in browser
✅ Verify middleware rewrites in Network tab
Step 1: Configure Local Domain Mapping
First, map your production domains to local development domains using your system's hosts file.
Edit /etc/hosts File
bash
sudo nano /etc/hosts
Add these entries to map local domains to localhost:
hosts
# Multi-tenant development domains
127.0.0.1 tenant-a.local
127.0.0.1 tenant-b.local
Why This Works
These entries tell your system to resolve tenant-a.local and tenant-b.local to 127.0.0.1 (localhost)
Your browser will send the host header with the domain name to your local Next.js server
This allows your middleware to identify tenants by hostname, just like in production
Step 2: Configure Next.js Middleware
Step 2: Configure Next.js Middleware
The middleware handles tenant routing based on the hostname. Here's a simplified version for development:
typescript
// Simplified middleware - for full production implementation, see:// [Production-Ready Multi-Tenant Setup - Middleware Section](/blog/production-ready-multi-tenant-nextjs-payload#middleware-implementation)const tenantMap = {
'tenant-a.local': 'tenant-a', // Maps local domain to tenant slug'tenant-b.local': 'tenant-b',
};
// ... logic to rewrite request to /[tenant]/...
For the complete production-ready middleware with authentication, preview mode, and diverse domain handling, see the full implementation guide.
How It Works
Hostname Detection: The middleware extracts the hostname from the request headers
Tenant Mapping: Maps hostnames to tenant slugs using the tenantMap object
URL Rewriting: Rewrites the request to the internal Next.js route structure
Preview Support: Handles both production and preview modes
Key Features
Production Mimicry: Uses the same hostname detection as production
Multi-Protocol Support: Works with both .local and production domains
/**
* Map production domains to development domains for local testing
*/exportconst getDevelopmentDomain = (domain: string): string => {
constdevDomainMap: Record<string, string> = {
'tenant-a.vercel.app': 'tenant-a.local',
'tenant-b.vercel.app': 'tenant-b.local',
};
return devDomainMap[domain] || domain;
};
Usage in SEO and URL Generation
This utility is used in places where URLs need to be generated for the current environment. The getDevelopmentDomain() utility maps production domains to local test domains. See Development Environment Guide for implementation.
When starting your development server, make sure it's configured to handle the custom domains.
Start Development Server
bash
# Using pnpm (as per project conventions)
pnpm dev
The server typically starts on port 3000, but you can configure it in your package.json:
json
{"scripts":{"dev":"next dev -p 80"}}
Why Port 80?
Running on port 80 allows you to access the domains without specifying a port
http://tenant-a.local instead of http://tenant-a.local:3000
More closely mimics production where sites run on standard HTTP/HTTPS ports
Testing the Setup
1. Verify Domain Resolution
Open these URLs in your browser:
http://tenant-a.local - Should show the Tenant A
http://tenant-b.local - Should show the Tenant B
http://localhost:3000 - Should work (fallback behavior)
2. Check Middleware Behavior
Look at the network requests in your browser dev tools:
Request URL: http://tenant-a.local/
Rewritten to: http://localhost:3000/tenant-slugs/tenant-a/home
The response should show content for the Tenant A
3. Test Production Features
Test features that depend on tenant identification:
SEO Metadata: Check that titles include the correct tenant name. For detailed SEO testing with tenant-specific metadata and OG images, see Multi-Tenant SEO with Payload & Next.js
Branding: Verify tenant-specific logos, colors, and content
Routing: Ensure internal links use the correct domain
API Calls: Confirm API requests include the correct tenant context
Troubleshooting
Domain Not Resolving
Issue: Browser shows "Server not found" or DNS error
Solution:
Verify your /etc/hosts entries are correct
Flush your DNS cache:
bash
# On macOSsudo dscacheutil -flushcache
# On Linuxsudo systemd-resolve --flush-caches
Try accessing http://127.0.0.1 to ensure the server is running
Middleware Not Triggering
Issue: Tenant routing not working, showing default content
Solution:
Check the middleware matcher pattern
Verify the hostname is being correctly extracted
Add console logging to the middleware:
typescript
console.log('Middleware called with hostname:', hostname);
Mixed Content Errors
Issue: HTTPS resources on HTTP development domains
Solution:
Use the getDevelopmentDomain utility for all URL generation
Ensure all internal links use protocol-relative URLs
Configure your CMS to generate correct URLs for the environment
Monitor Logs: Watch for any development-specific code paths
For comprehensive production setup including hardcoded tenant configuration, edge middleware optimization, and Payload CMS multi-tenant plugin configuration, see Production-Ready Multi-Tenant Setup.
Benefits of This Approach
Development Advantages
Realistic Testing: Test production-like URL structures locally
Tenant Isolation: Clear separation between tenant data and logic
No Workarounds: No need for subdirectories or query parameters
Single Codebase: Same middleware handles dev and production
Environment-Aware: Automatic switching between dev and prod domains
Type Safety: Full TypeScript support with proper interfaces
Debugging: Easy to debug tenant-specific issues locally
Alternative Approaches
Subdirectory Approach
Instead of domain-based routing, you could use subdirectories:
http://localhost:3000/tenant-a/...
http://localhost:3000/tenant-b/...
Pros: No hosts file modification needed
Cons: Doesn't mimic production URL structure
Query Parameter Approach
Use query parameters to specify tenants:
http://localhost:3000/?tenant=tenant-a
http://localhost:3000/?tenant=tenant-b
Pros: Simplest setup
Cons: Very different from production, SEO issues
Conclusion
This development environment setup provides a production-like experience for multi-tenant applications without complex infrastructure. By using local domain mapping and intelligent middleware, you can:
Test tenant-specific features realistically
Debug production-like URL structures
Maintain a single codebase for all environments
Ensure seamless transition from development to production
The key insight is that by mapping local domains to localhost and using middleware to handle routing, you create a development environment that behaves identically to production while keeping the setup simple and maintainable.