Home / RLS / Fix 42501: permission denied for table

Supabase RLS · Error fix

Fix 42501: permission denied for table

Postgres 15 · Supabase✓ code tested against a real database
TL;DR

The role (anon, authenticated or service_role) has no privilege on the table, so Postgres stops the query before RLS even runs. Grant only that table, only to the role that needs it, in the same migration that creates it. Never fix it with grant ... on all tables.

01 Grants vs RLS: two separate locks

Every query from the Data API passes two checks. Grants decide whether a role may touch the table at all. RLS then decides which rows. 42501 permission denied for table invoices means the first lock failed, so your policies were never consulted. An RLS problem looks different: zero rows, or new row violates row-level security policy.

02 Where it comes from

CAUSE 1 · new tables

A table created after Supabase's grants change

New tables no longer get automatic grants for the API roles. The first query to a new table returns 42501 until you grant it. What changed on Oct 30, and why it matters.

CAUSE 2

A table in another schema

The role also needs usage on the schema, otherwise the message is permission denied for schema app.

CAUSE 3

The grant was revoked or never given to this role

For example, granted to authenticated but called by a signed-out visitor (anon). The Data API answers 401 for anon and 403 for authenticated.

03 The fix: grant per table, with RLS in the same migration

migration.sql

alter table public.invoices enable row level security;

create policy "owner reads" on public.invoices for select to authenticated
  using (user_id = (select auth.uid()));

-- only the role and the operations the app actually uses
grant select on public.invoices to authenticated;

For a table in another schema, add grant usage on schema app to authenticated; as well.

04 The fix that leaks

-- don't paste this to make the error go away
grant select on all tables in schema public to anon, authenticated;

It removes the 42501 and also grants every other table in public. Any of them with RLS off becomes readable by anyone holding your public anon key. Check what each role can reach before and after any grant:

check.sql

select c.relname,
       has_table_privilege('anon', c.oid, 'select') as anon_can_read,
       c.relrowsecurity as rls_on
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
order by 1;

Any row with anon_can_read = true and rls_on = false is public. The grants change guide has a stricter version that also catches column grants and views.

◆ 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 in CI against a database with your migrations applied, before it reaches prod.

npx airlock-rls→

Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.

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

Is 42501 an RLS error?

No. It's a privilege error that happens before RLS. The same SQLSTATE is also used when an insert or update fails a policy's WITH CHECK, but then the message is new row violates row-level security policy, not permission denied for table.

Do I need to grant to service_role too?

If your backend uses the service_role key on a new table, yes. service_role bypasses RLS but still needs the table privilege.