Cenk Kurtoğlu©26

Supabase security · 6 minute read

Why Supabase RLS returns nothing or 42501 on the server

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

They have different causes, so do not treat them the same:

Confirm which role the request runs as

Run these inside the exact server call that is failing, not in the SQL editor:

select auth.role() as role, auth.uid() as uid;

If role is anon and uidis null, the user's token never reached PostgREST. That is the real bug, and no policy change will fix it.

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

On the server you must build the Supabase client with the signed-in user's access token. In the Next.js App Router, let @supabase/ssr read the request cookies for you:

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)),
    },
  })
}

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

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

Two things bite here. 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. Do not chase it. Configure Clerk as a third-party auth provider and attach its token:

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

Second, auth.uid() casts the JWT subclaim 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 instead 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 );

Fix 3: a 42501 with a custom schema

Supabase auto-grants privileges on the public schema, but not on a schema you created yourself. If your table is in, say, api, grant the roles that need it:

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;

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.

Prove it with a public fixture

The public Supabase RLS leak demo runs a read-only harness that queries as anon and as an authenticated user, so you can see exactly which role a request runs as and confirm the fix. Synthetic data, no credentials, runs in about two seconds.

Stop guessing which policy or role is wrong

Supabase RLS Audit Kit · $29 one-time

Seven commented SQL audits and 60 checks map every table's RLS status, the role each policy actually targets, grants, bypass paths, and a role-simulation harness that queries your database as another user to prove isolation. Report and fix-SQL templates included.

Get the kit - $29 ↗Review the files, scope and license →

Related: the misconfigurations that leak data even when auth is working are in 7 ways your Supabase app still leaks data with RLS enabled. And if a launch depends on proving cross-user paths are closed, a fixed-price Supabase RLS audit from $99 returns reproducible evidence and prioritized fixes.

Template bundle

Review all 20 frontend starters in one bundle

20 editable frontend starters with live demos are $79 — one payment, no subscription.

View the Bundle — $79