Skip to main content
Use middleware to control request flow at the edge: session sync, redirects, headers/rewrites, or selective execution via matchers. This page centralizes patterns used across the app (not just the dashboard).

What middleware does

  • Session sync: keep Supabase auth cookies in sync for SSR.
  • Redirects: gate protected pages and bounce signed‑in users away from auth pages.
  • Selective execution: use matchers to include/exclude paths for performance or correctness.
  • Rewrites/headers: optionally set security headers or route rewrites (advanced).

Auth gate pattern (Supabase)

Key logic lives in sabo/src/lib/supabase/middleware.ts:
  const {
    data: { user },
  } = await supabase.auth.getUser()

  const protectedRoutes = ['/dashboard']
  const isProtectedRoute = protectedRoutes.some((route) =>
    request.nextUrl.pathname.startsWith(route)
  )

  if (isProtectedRoute && !user) {
    return NextResponse.redirect(new URL('/sign-in', request.url))
  }

  const authRoutes = ['/sign-in', '/sign-up']
  const isAuthRoute = authRoutes.some((route) =>
    request.nextUrl.pathname.startsWith(route)
  )

  if (isAuthRoute && user) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }
Keep auth.getUser() directly after the client is created to avoid session flakiness.

Matcher design (include/exclude strategy)

  • Start with the narrowest matcher that achieves your goal (e.g., only ^/dashboard), then expand if needed.
  • Exclude static assets, images, and favicons to reduce overhead.
  • Avoid running middleware on webhook endpoints or any route requiring raw request bodies.
Example patterns to consider (adapt to your project root middleware.ts when you add one):
- Include only dashboard:            ^/dashboard(.*)
- Include all except assets:         /((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)
- Exclude webhooks:                  negative lookahead for ^/api/webhooks/stripe
Overly broad matchers increase latency. Default to including the minimum set of paths; grow carefully.

Top-level proxy file (used in this repo)

This repository ships with a src/proxy.ts that delegates to updateSession() and declares a matcher. It is functionally equivalent to a top‑level middleware.ts.
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'

export async function proxy(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * Feel free to modify this pattern to include more paths.
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Webhooks (Stripe or Polar)

Webhook handlers (e.g., /api/webhooks/stripe or /api/webhooks/polar) verify signatures using the raw request body. Our default matcher in src/proxy.ts currently runs on all paths except static assets, so it will execute before these webhook routes. Because updateSession() never touches the request body, this works out of the box, but you can exclude the endpoints for extra safety:
src/proxy.ts
export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$|api/webhooks/stripe|api/webhooks/polar).*)",
  ],
};
If you keep the default matcher, make sure the webhook handlers themselves perform signature verification using the raw request (see /api/webhooks/stripe/route.ts or /api/webhooks/polar/route.ts). Only add the exclusion once you’re ready so you don’t unintentionally bypass auth on other API routes.

Roles / Tenant guards (skeleton)

You can extend the auth gate with role or organization checks:
// Pseudo-code inside middleware.ts after user fetch
// const { role, orgId } = await fetchUserClaims(user.id) // via server fetch or cached store
// if (request.nextUrl.pathname.startsWith("/admin") && role !== "admin") {
//   return NextResponse.redirect(new URL("/dashboard", request.url))
// }
// if (request.nextUrl.pathname.startsWith(`/org/${orgId}/`) === false) {
//   return NextResponse.redirect(new URL(`/org/${orgId}/dashboard`, request.url))
// }
Prefer performing heavy data lookups in route handlers or layouts; keep middleware light. Cache claims if possible.

Security & performance checklist

  • Correct scope: Match only the routes you need; exclude static and webhook paths.
  • Early auth fetch: Call auth.getUser() immediately after creating the client.
  • No heavy work: Avoid DB lookups or large network calls in middleware.
  • Predictable redirects: Keep redirect targets simple and relative.
  • Webhook integrity: Do not wrap webhook paths with auth middleware; perform signature verification inside the route.

See also

  • Auth with Supabase (flows, UI, server actions) — route protection is summarized there and links back here for details.