Home / RLS / Next.js App Router + Supabase RLS (SSR)
Supabase RLS · Next.js
Next.js App Router + Supabase auth with RLS (SSR)
On the server, build the Supabase client from the request cookies with @supabase/ssr, not the bare anon client. Otherwise auth.uid() is null and RLS returns nothing — the classic 'my query is empty on the server' bug.
01 Why server queries come back empty
In a Server Component, if you create a plain anon Supabase client, it has no session — so auth.uid() is null, and every owner-scoped policy filters your rows to nothing. The query "works" and returns []. The fix is to hand the client the user's session, which lives in the request cookies.
02 The server client (from cookies)
utils/supabase/server.ts
import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' export async function createClient() { const store = await cookies() return createServerClient(URL, ANON_KEY, { cookies: { getAll: () => store.getAll(), setAll: (list) => list.forEach(({ name, value, options }) => store.set(name, value, options)), }, }) }
03 Use it in a Server Component
app/notes/page.tsx
const supabase = await createClient() const { data: notes } = await supabase.from('notes').select() // auth.uid() resolves from the cookie → RLS returns THIS user's notes
The same client works in Route Handlers and Server Actions. Refresh the session in middleware.ts so the cookie stays valid across requests (the @supabase/ssr docs and the nextjs-supabase-starter both wire this up).
04 The service_role trap
Don't "fix" the empty result by switching to the service_role key — it bypasses RLS entirely and, if it ever reaches the browser via NEXT_PUBLIC_*, exposes every row. Keep it server-only and rare (webhooks, cron). For user-facing reads, always go through the cookie-based session so RLS still applies.
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive — the same class of bug, caught on the pull request instead of in prod.
Or start from nextjs-supabase-starter — auth + a table with RLS + an isolation test, so a fresh table is safe by default.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
Why not just use the service_role key on the server?
Because it bypasses RLS. Every query then trusts your code to filter correctly, and one missing WHERE is a cross-tenant leak. Use the cookie-based authenticated client so the database enforces isolation for you; reserve service_role for trusted background jobs.
Does this work in Pages Router / Route Handlers?
Yes. @supabase/ssr has helpers for Pages Router (getServerSideProps) and for Route Handlers; the principle is identical — build the client from the request's cookies so the session, and therefore auth.uid(), is present.