Edge-Safe Multitenant Routing with Next.js Middleware
A practical guide to routing tenants at the edge, avoiding infinite rewrites, and keeping Supabase sessions intact.
Why Multitenant Routing Breaks at the Edge
Edge middleware can feel magical until you ship subdomain routing and accidentally create a rewrite loop that wipes auth cookies. This post shows the guardrails we use for multi-tenant sites that rely on Supabase sessions.
“Edge functions are honest: they do exactly what you asked. So ask precisely.”
The Minimal Middleware
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
export async function middleware(request: NextRequest) {
const host = request.headers.get("host") || "";
if (host.startsWith("demo.") && !request.nextUrl.pathname.startsWith("/demo")) {
const url = request.nextUrl.clone();
url.pathname = `/demo${request.nextUrl.pathname}`;
return NextResponse.rewrite(url);
}
return await updateSession(request); // keep Supabase cookies intact
}Guardrails
- Always guard
_next/staticand asset routes to prevent infinite loops. - Rewrite instead of redirect to keep Supabase session cookies on the same host.
- Keep middleware pure; call your session helper last to avoid rework.
Redirects change domains; rewrites do not. If your auth cookie is host-bound, prefer rewrites for tenant routing.
Tenant Resolution Matrix
| Host pattern | Action | Notes |
|---|---|---|
demo.* | Rewrite to /demo namespace | Skip if already under /demo |
*.customer.tld | Inject x-tenant header | Use in createServerClient for RLS |
app. root | No rewrite | Continue with default app routing |
Passing Tenant Context to Supabase
export async function updateSessionWithTenant(request: NextRequest) {
const tenant = request.headers.get("x-tenant");
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
global: { headers: { "x-tenant": tenant ?? "public" } },
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (cookies) => cookies.forEach((c) => request.cookies.set(c.name, c.value)),
},
}
);
return supabase;
}Runbook: Debugging Edge Routing Issues
- Unexpected 500s: check for missing env vars; Supabase client will throw early.
- Infinite loop: confirm matcher excludes assets and you guard against already-rewritten paths.
- Missing cookies: ensure you return the Supabase response object and avoid new
NextResponse. - Wrong tenant data: log
x-tenantheader and RLS claims in the Supabase policy test tool.
Edge routing can be calm if you keep the surface area tiny and the intent explicit. Start small, add one tenant rule at a time, and keep a runbook next to your matcher.