DEV Community

Cenk KURTOĞLU
Cenk KURTOĞLU

Posted on • Originally published at cenkkurtoglu.com

Why Supabase RLS Returns Nothing or 42501 (auth.uid() Is Null on the Server)

Originally published on cenkkurtoglu.com.

Your policy looks right. It works in the SQL editor. But from a server route the same query returns an empty array, or fails with 42501 permission denied — for a user who is clearly logged in. Nine times out of ten the policy is fine and the request is the problem: it never arrived authenticated, so auth.uid() is null and every ownership rule filters the rows away.

First, tell the two failures apart

  • Empty result set (no error): RLS evaluated your policy to false for every row. Usually auth.uid() is null, so auth.uid() = user_id matches nothing.
  • 42501 permission denied for table: this is not RLS at all. It is a missing table-level GRANT for the role the request runs as — most often because the table lives in a custom schema (e.g. api) that the authenticated role was never granted access to.

Confirm which role the request runs as

Run this inside the exact server call that is failing:

select auth.role() as role, auth.uid() as uid;
Enter fullscreen mode Exit fullscreen mode

If role is anon and uid is null, the user's token never reached PostgREST. That is the real bug.

Fix 1: pass the user's token to the server client

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function getServerSupabase() {
  const cookieStore = await cookies()
  return createServerClient(URL, ANON_KEY, {
    cookies: {
      getAll: () => cookieStore.getAll(),
      setAll: (list) => list.forEach(c => cookieStore.set(c.name, c.value, c.options)),
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

Then re-run the check: auth.role() should be authenticated and auth.uid() non-null.

Fix 2: using Clerk (or another third-party auth)?

First, supabase.auth.getUser() returns null and no user appears in auth.users — that is expected, because the session belongs to Clerk, not Supabase Auth. Configure Clerk as a third-party auth provider and attach its token:

createClient(URL, ANON_KEY, {
  accessToken: async () => (await getToken()) ?? null,
})
Enter fullscreen mode Exit fullscreen mode

Second, auth.uid() casts the JWT sub claim to a UUID, but Clerk's sub looks like user_2ab... and is not a UUID — so auth.uid() comes back null even with a valid token. In Clerk-backed policies, compare the raw claim and store Clerk's id as text:

create policy "own rows" on public.your_table
for all to authenticated
using ( (auth.jwt() ->> 'sub') = user_id );
Enter fullscreen mode Exit fullscreen mode

Fix 3: a 42501 with a custom schema

Supabase auto-grants on public, but not on a schema you created. If your table lives in api:

grant usage on schema api to authenticated;
grant select, insert, update, delete on all tables in schema api to authenticated;
alter default privileges in schema api grant select, insert, update, delete on tables to authenticated;
Enter fullscreen mode Exit fullscreen mode

Grants decide whether the query is allowed to run at all; RLS then decides which rows come back. A 42501 means you never cleared the first gate.


Want the full version — 60 checks plus a role-simulation harness that queries your database as anon and as a second user to prove isolation? Supabase RLS Audit Kit ($29). Free read-only demo (runs in ~2s, no Docker): github.com/cekuu35/supabase-rls-leak-demo.

Top comments (0)