Skip to main content

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.

RRRichard Roe

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/static and 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 patternActionNotes
demo.*Rewrite to /demo namespaceSkip if already under /demo
*.customer.tldInject x-tenant headerUse in createServerClient for RLS
app. rootNo rewriteContinue 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-tenant header 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.