DEV Community

Cover image for Supabase 42501 permission denied is a grant problem, not RLS
Denys Zaichenko for Denys

Posted on Originally published at madsalt.tech

Supabase 42501 permission denied is a grant problem, not RLS

TL;DR. 42501 permission denied for table … means the role has no grant on the table. Postgres stops the request before any RLS policy even runs. From 30 October 2026 Supabase stops granting new tables in public automatically on existing projects, so every table you create after that answers 42501 until you grant it. The fix is three statements together - a grant, enable row level security and a policy. Grant alone without RLS and the table is open to everyone.

I built a free Supabase security checker that reads the grants on every exposed table. Most projects have too many grants. 42501 is the other side, too few. So I reproduced it locally to see what really happens.

What does 42501 mean?

It's the Postgres code for insufficient privilege. Before a client touches a table Postgres checks two layers. Grants decide if the role can run the operation at all. RLS policies decide which rows it sees. A missing grant fails first and loud, as an error. A policy that matches nothing fails quietly, you just get an empty list. So if you see 42501, look at grants and leave your policies alone for now.

PostgREST (the Data API behind supabase-js) passes the error through with a hint. On my local stack (Supabase CLI 2.109.1, Postgres 17.6, PostgREST 14.14) a request with the publishable key to a table with no grant for anon came back as HTTP 401 with this:

{
  "code": "42501",
  "details": null,
  "hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.orders TO anon;",
  "message": "permission denied for table orders"
}
Enter fullscreen mode Exit fullscreen mode

Why do new tables throw 42501 after 30 October 2026?

Until now every new table in public got select, insert, update and delete for anon, authenticated and service_role through default privileges. So it was reachable over the Data API the second you created it, protected or not. Supabase removes those defaults. Optional from 28 April 2026, default for new projects from 30 May 2026, and on 30 October 2026 it hits all existing projects. Tables you already have keep their grants.

I ran the same two alter default privileges … revoke statements from the changelog on my local project and then created a new table invoices.

What a new table answers after 30 October 2026. Publishable key, no grant: 42501, HTTP 401. Secret key, no grant: 42501, HTTP 403. After grant select and enable RLS: 0 rows, HTTP 200.
My reproduction on a local Supabase stack with the default privileges from the Supabase changelog, 23 October 2026.

Request to the new table Result
Publishable key, no grant HTTP 401, 42501, hint GRANT SELECT ON public.invoices TO anon;
Secret key, no grant HTTP 403, 42501, hint … TO service_role;
Publishable key, after grant select and enable row level security HTTP 200, [] until a policy allows rows

Two things surprised me. The secret key gets refused too. service_role skips RLS but it still needs a grant, so an Edge Function or a server job writing to a new table breaks exactly the same way. And the revoke only takes back those four operations. The new table still came with REFERENCES, TRIGGER and TRUNCATE for anon, authenticated and service_role. No policy applies to TRUNCATE.

How do you fix it without opening the table?

Grant, RLS and policy are one change. Put them in one migration and give each role only what it needs.

revoke all on table public.invoices from anon, authenticated;
grant select, insert, update on table public.invoices to authenticated;
grant select, insert, update, delete on table public.invoices to service_role;

alter table public.invoices enable row level security;

create policy "Owners read their invoices"
on public.invoices for select
to authenticated
using ((select auth.uid()) = user_id);
Enter fullscreen mode Exit fullscreen mode

The revoke all at the top clears the TRUNCATE, TRIGGER and REFERENCES that came with the table. Please don't answer 42501 with grant all on all tables in schema public to anon. That brings the old default back for every table at once, and any table without RLS becomes readable by anyone who has your public key.

If an AI tool creates your tables, tell it to write grants, RLS and policies in the same migration. The hint in the error is written so an agent can fix the grant by itself, but it only names the grant. It says nothing about the RLS that should come with it.

What to check in your own project

Look for three things in the grants layer. Default privileges that still grant new tables to the API automatically. Write grants for anon that no policy uses. And TRUNCATE, TRIGGER and REFERENCES on tables for authenticated, like the ones above. Why RLS still matters after the grant is in how to test Supabase RLS, and how grants and policies look across a whole app is in my Lovable app security checklist.


Originally published at madsalt.tech. Written with AI assistance, checked by me.

Top comments (0)