Proxy and Routing
Tenant headers must come from the proxy, never from the client. Any caller can attach an x-tenant-id header to a request, and if the proxy forwards that value untouched, your app trusts it and serves data for whichever tenant the caller picked. The examples below delete or overwrite each tenant header they set before forwarding the request, including on paths that skip tenant resolution, so a client-supplied x-tenant-id never reaches a route that runs behind the proxy. If your app reads other x-tenant-* headers, strip those in the proxy too. Requests to paths that a matcher excludes, such as /api in the first example, skip the proxy entirely, so don't trust tenant headers in those routes.
Extract tenant identity from subdomains like tenant1.yourapp.com:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server';
export async function proxy(request: NextRequest) {
const hostname = request.headers.get('host') || '';
// Strip inbound tenant headers; only the proxy sets them
const requestHeaders = new Headers(request.headers);
requestHeaders.delete('x-tenant-id');
requestHeaders.delete('x-tenant-subdomain');
// Extract subdomain (tenant identifier)
const subdomain = hostname.split('.')[0];
// Skip processing for main app domains
if (subdomain === 'www' || subdomain === 'app' || subdomain === 'admin') {
return NextResponse.next({ request: { headers: requestHeaders } });
}
// Validate tenant exists (you might cache this for performance)
const tenant = await validateTenant(subdomain);
if (!tenant) {
// Redirect to main app or show 404
return NextResponse.redirect(new URL('/not-found', request.url));
}
// Forward tenant context to your app on the request headers
requestHeaders.set('x-tenant-id', tenant.id);
requestHeaders.set('x-tenant-subdomain', subdomain);
return NextResponse.next({ request: { headers: requestHeaders } });
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};This example:
- Extracts
tenant1fromtenant1.yourapp.com - Validates the tenant exists in your database
- Strips inbound tenant headers so clients can't forge tenant context
- Adds tenant context to request headers for your app to use
- Redirects invalid tenants to a 404 page
Handle custom domains like tenant.com mapping to tenants:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server';
import { get } from '@vercel/global-config';
export async function proxy(request: NextRequest) {
const hostname = request.headers.get('host') || '';
// Strip inbound tenant headers; only the proxy sets them
const requestHeaders = new Headers(request.headers);
requestHeaders.delete('x-tenant-id');
requestHeaders.delete('x-tenant-type');
// Check if this is a custom domain
const customDomainTenant = await get<{ id: string }>(`domain_${hostname}`);
if (customDomainTenant) {
// Custom domain found, forward tenant context on the request headers
requestHeaders.set('x-tenant-id', customDomainTenant.id);
requestHeaders.set('x-tenant-type', 'custom-domain');
return NextResponse.next({ request: { headers: requestHeaders } });
}
// Fall back to subdomain resolution
const subdomain = hostname.split('.')[0];
const subdomainTenant = await get<{ id: string }>(`subdomain_${subdomain}`);
if (subdomainTenant) {
requestHeaders.set('x-tenant-id', subdomainTenant.id);
requestHeaders.set('x-tenant-type', 'subdomain');
return NextResponse.next({ request: { headers: requestHeaders } });
}
// No tenant found
return NextResponse.redirect(new URL('/not-found', request.url));
}This example:
- First checks if the hostname is a registered custom domain
- Falls back to subdomain parsing if not a custom domain
- Uses Global Config for fast tenant lookups
- Sets tenant type so your app knows how the tenant was resolved
Extract tenant from URL paths like /tenant1/dashboard:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server';
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// Strip inbound tenant headers; only the proxy sets them
const requestHeaders = new Headers(request.headers);
requestHeaders.delete('x-tenant-id');
requestHeaders.delete('x-tenant-slug');
// Extract tenant from first path segment: /tenant_name_1/dashboard
const pathSegments = pathname.split('/');
const tenantSlug = pathSegments[1];
if (!tenantSlug || tenantSlug.startsWith('_')) {
return NextResponse.next({ request: { headers: requestHeaders } });
}
// Validate tenant exists
const tenant = await validateTenant(tenantSlug);
if (!tenant) {
return NextResponse.redirect(new URL('/not-found', request.url));
}
// Rewrite URL to remove tenant from path
const newPath = `/${pathSegments.slice(2).join('/')}`;
const rewriteUrl = new URL(newPath, request.url);
requestHeaders.set('x-tenant-id', tenant.id);
requestHeaders.set('x-tenant-slug', tenantSlug);
return NextResponse.rewrite(rewriteUrl, {
request: { headers: requestHeaders },
});
}This example:
- Extracts
tenant1from/tenant1/dashboard - Rewrites the URL to
/dashboard(removes tenant from path) - Your app receives
/dashboardwith tenant context in headers - Skips processing for Next.js internal routes (starting with
_)
Route different URLs to tenant-specific pages:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server';
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
const hostname = request.headers.get('host') || '';
const tenantId = await resolveTenantId(hostname);
if (!tenantId) {
return NextResponse.redirect(new URL('/not-found', request.url));
}
// Overwrite any inbound tenant header with the resolved tenant
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant-id', tenantId);
// Route to tenant-specific pages
if (pathname === '/') {
// Rewrite homepage to tenant-specific version
const url = request.nextUrl.clone();
url.pathname = `/tenant/${tenantId}`;
return NextResponse.rewrite(url, {
request: { headers: requestHeaders },
});
}
if (pathname.startsWith('/blog')) {
// Route blog requests to tenant-specific blog
const url = request.nextUrl.clone();
url.pathname = `/tenant/${tenantId}/blog${pathname.replace('/blog', '')}`;
return NextResponse.rewrite(url, {
request: { headers: requestHeaders },
});
}
// Forward tenant context to all other requests
return NextResponse.next({ request: { headers: requestHeaders } });
}This example:
- Routes
tenant1.app.com/to/tenant/tenant1page - Routes
tenant1.app.com/blog/post-1to/tenant/tenant1/blog/post-1page - Other routes get tenant context but keep the same URL
- Your page components can access tenant ID from headers
Was this helpful?