Multi-Tenant Architecture in Next.js: A Production Guide
Three patterns for building multi-tenant SaaS in Next.js — subdomain routing, path-based tenancy, and database-level isolation — with their trade-offs.
What Multi-Tenancy Actually Means
A multi-tenant application serves multiple customers (tenants) from a single deployment. Each tenant's data is isolated but the codebase is shared. The three main approaches differ in how you identify which tenant is making a request.
Pattern 1: Subdomain-Based Routing
Each tenant gets their own subdomain: acme.yourapp.com, globex.yourapp.com.
This is the premium option — it looks professional and allows per-tenant custom domains. It requires wildcard DNS and SSL configuration.
Middleware approach:
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host') ?? ''
const subdomain = hostname.split('.')[0]
// Pass tenant slug as a header to all route handlers
const response = NextResponse.next()
response.headers.set('x-tenant', subdomain)
return response
}
// app/page.tsx — read tenant from headers
import { headers } from 'next/headers'
export default async function Page() {
const headersList = await headers()
const tenant = headersList.get('x-tenant')
const data = await db.query.tenants.findFirst({
where: eq(tenants.slug, tenant),
})
// ...
}
Wildcard DNS in next.config.ts:
const config: NextConfig = {
async headers() {
return [{ source: '/(.*)', headers: [{ key: 'x-tenant', value: '' }] }]
},
}
Deploy on Vercel and add a wildcard domain *.yourapp.com. Vercel handles the SSL automatically.
Pattern 2: Path-Based Tenancy
Tenants are identified by a path prefix: yourapp.com/acme/..., yourapp.com/globex/....
Simpler to set up — no DNS configuration required. Works well for internal tools and B2B apps where vanity subdomains aren't important.
app/
[tenant]/
page.tsx ← /acme, /globex
dashboard/
page.tsx ← /acme/dashboard
// app/[tenant]/layout.tsx
export default async function TenantLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ tenant: string }>
}) {
const { tenant } = await params
const tenantData = await db.query.tenants.findFirst({
where: eq(tenants.slug, tenant),
})
if (!tenantData) notFound()
return (
<TenantProvider tenant={tenantData}>
{children}
</TenantProvider>
)
}
Pattern 3: Database-Level Isolation
How you isolate tenant data at the database level matters as much as routing. Three common approaches:
Shared tables with tenant_id column (most common):
CREATE TABLE projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id),
name text NOT NULL,
created_at timestamptz DEFAULT now()
);
CREATE INDEX ON projects(tenant_id);
Every query must include a tenant_id filter. Use Postgres Row Level Security to enforce this at the database level — it's the safest approach because it's enforced even if application code forgets the filter.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant isolation"
ON projects
USING (tenant_id = current_setting('app.tenant_id')::uuid);
Set the tenant context before queries:
async function withTenantContext<T>(tenantId: string, fn: () => Promise<T>) {
return db.transaction(async (tx) => {
await tx.execute(sql`SELECT set_config('app.tenant_id', ${tenantId}, true)`)
return fn()
})
}
Separate schemas per tenant (better isolation, more complexity):
CREATE SCHEMA tenant_acme;
CREATE TABLE tenant_acme.projects (...);
Dynamically set search_path to the tenant schema before queries. More isolation, but harder to run cross-tenant analytics and schema migrations become O(n tenants).
Separate databases (maximum isolation, highest cost):
Each tenant gets their own database instance. Used in enterprise contracts with strict data residency requirements. The operational overhead is significant — only consider this if it's a hard requirement.
Handling Tenant Resolution in Auth
Always resolve the tenant as part of your auth flow — don't trust the URL alone.
// lib/auth.ts
export async function getCurrentTenantUser(request: NextRequest) {
const session = await getSession(request)
if (!session) return null
const tenantSlug = resolveTenantFromRequest(request)
const membership = await db.query.memberships.findFirst({
where: and(
eq(memberships.userId, session.userId),
eq(memberships.tenantSlug, tenantSlug)
),
})
if (!membership) return null
return { user: session.user, tenant: membership.tenant, role: membership.role }
}
A user authenticated to acme.yourapp.com should never be able to read data from globex.yourapp.com — even if they guess the path. Verify membership on every request.
Caching Considerations
Per-tenant data cannot be cached globally. Use unstable_cache with the tenant ID as part of the cache key:
import { unstable_cache } from 'next/cache'
export const getTenantProjects = (tenantId: string) =>
unstable_cache(
async () => db.query.projects.findMany({ where: eq(projects.tenantId, tenantId) }),
[`projects-${tenantId}`],
{ revalidate: 60, tags: [`tenant-${tenantId}`] }
)()
Invalidate the tag on mutations:
import { revalidateTag } from 'next/cache'
revalidateTag(`tenant-${tenantId}`)
Choosing a Pattern
| Requirement | Pattern |
|---|---|
| Custom domains per tenant | Subdomain routing |
| Simple B2B internal tool | Path-based |
| Startup, moving fast | Shared table + tenant_id |
| Enterprise data residency | Separate schema/database |
| Regulatory compliance (HIPAA/SOC2) | Separate schema minimum |
Multi-tenancy is one of the architectural decisions that's expensive to change later. Pick the isolation level your target customers actually require — not the one that sounds most impressive on a product page.
Continue reading
Related articles
Rate Limiting in Next.js: Protecting Your API Routes
How to implement production-grade rate limiting in Next.js — with Middleware-level protection, per-user limits, and distributed rate limiting using Upstash Redis.
EngineeringNext.js Parallel Routes and Intercepting Routes: A Complete Guide
Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.
EngineeringVercel vs Netlify vs AWS Amplify for Next.js in 2026
A practical comparison of the three most common Next.js hosting platforms — Vercel, Netlify, and AWS Amplify — with real cost and capability trade-offs.
Stay informed
Get our monthly deep dives.
Engineering, design, and growth insights — once a month. No spam.
Browse all resources