Supabase SSR with Next.js - Server-Side Auth and Data

Supabase SSR with Next.js trips up a lot of developers for one specific reason: the server cannot read the user's session unless you wire up cookie handling correctly. Get that wrong and getUser() returns null even when the user is logged in. Here's the exact setup that makes server-side auth and data work.
Quick Answer
Use the @supabase/ssr package to create a Supabase client that reads and writes auth cookies on the server. Create a server client in Server Components and route handlers, pass the cookie store so sessions persist, and keep a separate browser client for Client Components. Then enforce access with row-level security in the database.
What SSR means here
Server-side rendering (SSR) means the page is built on the server before it reaches the browser. For auth, that means the server must be able to read the user's session from cookies — which is exactly what @supabase/ssr enables. This is the server-side companion to how to use Supabase with Next.js.
Prerequisites
A Next.js App Router app and a Supabase project.
@supabase/ssrand@supabase/supabase-jsinstalled.
Step 1: Create a server client
The server client wires Supabase to Next.js cookies. (Untested-here: follow the current Supabase SSR docs for exact helper signatures.)
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function getServerClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (items) =>
items.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
),
},
}
);
}Step 2: Cookie handling
The getAll/setAll handlers are what make sessions persist across server requests. Without them, the server cannot refresh or read the session and getUser() returns null even when the user is logged in.
Step 3: Read the user and data on the server
// app/dashboard/page.tsx
import { redirect } from 'next/navigation';
import { getServerClient } from '@/lib/supabase/server';
export default async function Dashboard() {
const supabase = await getServerClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect('/login');
const { data: rows } = await supabase.from('projects').select('*');
return <pre>{JSON.stringify(rows, null, 2)}</pre>;
}Row-level security reminder
Supabase enables row-level security (RLS) by default. Authenticating the user is not enough — you must also write RLS policies that allow that user to read or write the rows in question, or queries return empty.
Common issues
getUser() is null on the server
You are using the browser client on the server, or the cookie handlers are not wired. Use the server client from Step 1.
Cookies not set after sign-in
Server Components cannot always set cookies; perform sign-in in a route handler or server action where setAll can run.
Alternative: own your backend
If you would rather not depend on a hosted backend's session model, you can own your auth and data layer instead. FastStaq is an own-your-backend boilerplate — it uses Express, Prisma, and PostgreSQL and does not use Supabase. It is a different approach to the same goal: server-side auth and data you control. See Supabase vs Firebase for the hosted-platform trade-offs.


