Decorative background gradient
Back to Blog
Next.js Supabase Auth SSRMiddleware Authentication Next.jsSupabase RLS

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.

Next.js Supabase Auth SSR — Middleware, Server Actions & RLS Guide

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.

bash
ts
ts

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.

ts

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.

tsx
tsx

For OAuth (GitHub, Google), call signInWithOAuth from a Server Action and redirect to the returned URL:

ts

You'll also need a Route Handler at /auth/callback to exchange the OAuth code for a session:

ts

Step 4: Read the Authenticated User in Server Components

tsx

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.

sql

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.

Frequently Asked Questions

Why do I need middleware for Supabase auth in the App Router?

Supabase's auth session lives in cookies that expire and need silent refresh. Middleware runs on every request before your route renders, so it's the correct place to call supabase.auth.getUser() and rewrite the session cookies — without it, Server Components can end up reading stale or expired tokens.

What's the difference between getSession() and getUser() on the server?

getSession() reads the session from cookies without verifying it against Supabase's auth server, which means a tampered or stale cookie could pass. getUser() sends the access token to Supabase's server to verify it is still valid, making it the safe choice anywhere you're about to make an authorization decision.

Does Supabase Auth work with Server Actions?

Yes. Create a server client inside the Server Action using the request's cookies, then call supabase.auth.signInWithPassword, signUp, or signOut directly — no client-side fetch or API route needed.

Is Row Level Security required if I already check auth in my app code?

Yes, treat RLS as mandatory, not optional. Application-level checks can be bypassed by a bug, a missed code path, or direct calls to the Supabase REST/PostgREST API using the anon key. RLS enforces authorization at the database layer regardless of what your app code does.

Working on something similar? Take a look at my services and case studies, or book a free call to talk about your idea.

Related Articles

Caching Strategies and Cache Invalidation — The Complete Guide
CachingSystem DesignPerformance

Caching Strategies and Cache Invalidation — The Complete Guide

A practical guide to caching strategies (cache-aside, write-through, write-behind) and the cache invalidation techniques that keep them from serving stale data.

September 8, 2026Read more →
Database Indexing and Read Replicas — A Practical Guide
DatabasePostgreSQLSystem Design

Database Indexing and Read Replicas — A Practical Guide

How to choose the right database indexes, avoid the ones that quietly hurt write performance, and scale reads with replicas without introducing replication lag bugs.

September 8, 2026Read more →
Load Balancing and Stateless Service Design — A Practical Guide
System DesignLoad BalancingScalability

Load Balancing and Stateless Service Design — A Practical Guide

How load balancers distribute traffic, why stateless services are what actually makes horizontal scaling work, and how to fix the sticky-session traps that quietly reintroduce state.

September 8, 2026Read more →
Session Stores and Database Connection Pooling Explained
BackendRedisDatabase

Session Stores and Database Connection Pooling Explained

Why in-memory sessions break horizontally scaled apps, how to move session state to Redis correctly, and how connection pooling keeps your database from falling over under concurrent load.

September 8, 2026Read more →
Vertical vs Horizontal Scaling: How to Choose and Implement Each
System DesignScalabilityArchitecture

Vertical vs Horizontal Scaling: How to Choose and Implement Each

A practical comparison of vertical and horizontal scaling — what each actually fixes, where each breaks down, and the architecture changes horizontal scaling requires that most guides skip.

September 8, 2026Read more →
AI Agent Guardrails and Safety — Preventing Prompt Injection and Runaway Actions
Ai Agent SafetyPrompt Injection DefenseAgent Guardrails

AI Agent Guardrails and Safety — Preventing Prompt Injection and Runaway Actions

Build practical guardrails for AI agents — prompt injection defenses, destructive-action confirmation, iteration caps, and permission scoping.

September 7, 2026Read more →

Trending Topics