Home / RLS / Next.js App Router + Supabase RLS (SSR)

Supabase RLS · Next.js

Next.js App Router + Supabase auth with RLS (SSR)

Next.js 14+ · @supabase/ssr✓ code tested against a real database
TL;DR

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.

◆ FREE · MIT · npm + GitHub Action

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.

npx airlock-rls

Or start from nextjs-supabase-starter — auth + a table with RLS + an isolation test, so a fresh table is safe by default.

FREE PDF

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.