Skip to content
Next.js+Supabase Auth
Stack Integration

Next.js + Supabase Auth: Server-Side Pattern

Supabase Auth works beautifully with Next.js App Router — session cookies, server components, and middleware for route protection.

Use Cases
  1. Protected routes with Next.js middleware + Supabase session
  2. Server components that fetch user-specific data
  3. Auth state synced between server and client
  4. OAuth + email/password in the same app
Implementation

Use @supabase/ssr package in Next.js server components. Middleware reads the session cookie and redirects unauthenticated users. Never expose the service role key to the client.

In detail

How the pieces connect

Supabase Auth issues a JWT access token plus a refresh token. In the App Router, the @supabase/ssr package stores both in cookies so every render context can read the same session. The flow has three touchpoints: middleware runs on each request and calls supabase.auth.getUser() to validate the token against Supabase (not just decode it locally), refreshing it when expired and writing updated cookies onto the response. Server Components create a server client bound to cookies() from next/headers and read the session for data fetching. The browser client, created in a "use client" boundary, handles interactive sign-in and subscribes to onAuthStateChange. Because all three read the same cookie store, server and client stay in sync without you passing the user down as a prop or refetching on the client after navigation.

Production gotchas

The most common mistake is trusting getSession() in server code. getSession() only decodes the cookie; it does not verify the token with Supabase, so a tampered cookie can pass. For any server-side authorization decision use getUser(), which round-trips to the Auth server. Second: middleware must return the same response object it wrote cookies onto — creating a fresh NextResponse after getUser() drops the refreshed tokens and silently logs users out mid-session. Third: Server Components cannot set cookies, so token refresh has to happen in middleware or a Route Handler, never inside a page render. Fourth: keep the OAuth callback in a Route Handler at /auth/callback that calls exchangeCodeForSession(code) — the PKCE code only exchanges once. Finally, never instantiate a module-level singleton client on the server; each request needs its own client tied to that request's cookies.

Security considerations

Authentication and authorization are separate layers here. Supabase Auth proves who the user is; Row Level Security (RLS) decides what their token can touch. Enable RLS on every table and write policies against auth.uid() — without RLS, the anon key plus a valid session can read the whole table directly from the browser. The anon (publishable) key is safe to ship to the client because RLS constrains it. The service_role key bypasses RLS entirely, so it stays server-only, read from a non-NEXT_PUBLIC_ environment variable, and is used only in Route Handlers or Server Actions for trusted operations like admin writes. Set cookies as httpOnly, secure, and sameSite=lax so client JavaScript can't read the tokens, which limits XSS token theft. Validate redirect targets after sign-in against an allowlist rather than echoing a user-supplied next parameter, which prevents open-redirect abuse on the callback route.

When this combo fits — and when it doesn't

It fits when you want a Postgres-backed app with auth, data, and authorization in one place: SaaS dashboards, member areas, anything where RLS-per-user maps cleanly onto your tables. The App Router pairs naturally because server-rendered, user-specific pages can fetch behind the cookie session without a client round-trip. It fits less well when most of your app is static marketing content with one gated corner — the middleware and per-request client add overhead you don't need; a single Route Handler check is lighter. It also strains if you need auth logic Supabase doesn't model natively, like complex multi-tenant org hierarchies with nested roles; you can build that with RLS and a roles table, but the policies get involved fast. For pure machine-to-machine APIs with no human sessions, skip the cookie layer and authenticate with a service key or signed JWT directly.

Other integration guidesView all →
Related

Need this built?