Next.js Supabase Auth SSR — Middleware, Server Actions & RLS Guide
Production-ready Supabase Auth in the Next.js App Router — middleware session refresh, Server Actions login, and Row Level Security done right.

Supabase Auth is straightforward in a client-only app, but the Next.js App Router's mix of Server Components, Server Actions, and middleware means session handling has to be done correctly across multiple execution contexts — or you'll get intermittent "logged out" bugs that are painful to debug.
This guide sets up a complete, production-correct flow: middleware session refresh, server-side auth checks, Server Action-based login, and Row Level Security.
Step 1: Install and Configure the SSR-Aware Client
Use @supabase/ssr, not the plain @supabase/supabase-js client, for anything server-rendered.
Two separate factories: one for Server Components/Actions/Route Handlers, one for Client Components.
Step 2: Refresh Sessions in Middleware
This is the step most tutorials skip, and it's the one that causes "why am I randomly logged out" bugs in production.
Calling supabase.auth.getUser() inside middleware is what triggers Supabase to refresh an expiring access token and reissue cookies — skip this and Server Components downstream will eventually see expired sessions.
Step 3: Handle Login with a Server Action
Keep credentials off the client-side JS execution path entirely by submitting the form directly to a Server Action.
For OAuth (GitHub, Google), call signInWithOAuth from a Server Action and redirect to the returned URL:
You'll also need a Route Handler at /auth/callback to exchange the OAuth code for a session:
Step 4: Read the Authenticated User in Server Components
Always use getUser(), not getSession(), before making an authorization decision on the server — getUser() revalidates the token against Supabase rather than trusting whatever is in the cookie.
Step 5: Enforce Authorization with Row Level Security
A valid session tells you who the user is. It says nothing about what rows they're allowed to touch — that's RLS's job, enforced at the database level regardless of what your application code does.
With these policies active, even if application code has a bug that queries projects without a user_id filter, Postgres itself will only ever return rows the authenticated user owns — the anon key alone is never enough to read someone else's data.
Key Takeaways
Session handling in the App Router requires a server client for SSR contexts, a browser client for Client Components, and — critically — middleware that calls getUser() on every request to refresh expiring tokens. Route logins through Server Actions to keep credentials off the client, always verify identity with getUser() rather than getSession() before authorizing anything server-side, and treat Row Level Security as the real authorization boundary, not a backup to your application code.






