Audit & fix
Supabase “permission denied for table” (42501): the 30 October grants change, and the safe fix
Since 30 October 2026, new Supabase tables need an explicit GRANT before the Data API can see them. Why you get error 42501, the safe fix with RLS, and the risky one.
In short
Since 30 October 2026, Supabase no longer grants new tables in the public schema to the anon, authenticated and service_role roles, so the Data API can’t see a new table until you grant it. Querying one fails with “permission denied for table” (code 42501), even when RLS and policies are correct. The fix is to grant only what each role needs, enable RLS and add policies in the same migration. Granting everything to anon on a table without RLS makes the error go away and exposes the table to anyone. Existing tables keep their grants.
Key takeaways
- Tables created before 30 October keep working. Every new table in public starts with no API access until you grant it.
- Error 42501 means a missing grant, not a broken RLS policy: the request is refused before any policy runs.
- Grant only what each role needs, enable RLS and add policies in the same migration. Grant anon only for genuinely public tables.
- Never fix it with “grant all to anon” on a table without RLS: that makes the table readable by anyone with your public key.
What changed on 30 October
Until now, every table you created in the public schema of a Supabase project was granted to the three roles the Data API uses (anon, authenticated and service_role) the moment it existed. That is why a table you made in the dashboard, or one Lovable or Bolt made for you, showed up in your app straight away.
That default is gone. Supabase announced the change in April, made it the default for new projects on 30 May, and on 30 October 2026 applied it to every existing project. From then on:
- Tables you already had are unaffected. They keep the grants they were created with, and your app keeps working.
- Every new table in `public` starts with no API access at all. The Data API (PostgREST, GraphQL,
supabase-jsand the other client libraries) can’t see it until you grant it explicitly. - Row-level security works exactly as before. Grants and RLS are separate layers: a grant decides whether a role can touch a table at all, and RLS decides which rows it sees.
The change only affects the Data API. If your app talks to Postgres over a direct connection, through an ORM or a server with a connection string, nothing changes for you.
Supabase’s reason is the one this blog keeps returning to: AI tools, scripts and agents now create tables with no human reviewing the change, and the old default exposed every one of them before anyone had secured it.
The error you will see
The first time your app queries a table created after the change, the request fails. Through the API you get a 401 (signed out) or 403 (signed in) with this body:
{
"code": "42501",
"message": "permission denied for table your_table",
"hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;"
} In the SQL editor or a Postgres client, the same thing reads ERROR: permission denied for table your_table.
This catches people out because it usually arrives with a new feature. The app worked yesterday; you asked your AI tool for a new screen, it created a table and its policies, and that screen fails while everything else works. We reproduced it on a local Postgres with Supabase’s roles: a new notes table with RLS enabled and a correct select policy still fails with exactly this error for a signed-in user, because the policy never gets a chance to run.
42501 is a missing grant, not a broken policy. If RLS were blocking you, you would get an empty result or “new row violates row-level security policy”, not “permission denied”.
The fix: grant, RLS and policies in one migration
Supabase’s guidance is to treat three steps as one unit, and we agree. Put them in the same migration, in this order:
-- 1. Grant only what each role needs
grant select, insert, update, delete on public.notes to authenticated;
-- 2. Turn on row-level security
alter table public.notes enable row level security;
-- 3. Say which rows each role may see and change
create policy "Users read their own notes"
on public.notes for select to authenticated
using ((select auth.uid()) = user_id);
create policy "Users write their own notes"
on public.notes for insert to authenticated
with check ((select auth.uid()) = user_id); Tested on the local copy, a signed-in user then reads only their own rows, and a signed-out request is still refused, because anon was never granted.
How to choose the grants:
- `authenticated` gets the operations your signed-in users actually perform. A read-only table gets
selectand nothing else. - `anon` gets nothing unless signed-out visitors genuinely need the table: a public catalogue, published posts, a pricing table.
- `service_role` only matters if your own server code uses the secret key against this table. It bypasses RLS, so grant it deliberately:
grant select, insert, update, delete on public.notes to service_role;.
If you want the details of writing the policies themselves, we covered every common way they go wrong in Supabase RLS not working.
The risky fix to avoid
The fastest way to make the error disappear is the wrong one:
grant all on public.invoices to anon, authenticated;If that table doesn’t have RLS enabled, you have just made it readable and writable by anyone holding your publishable key, and that key is in your JavaScript bundle for anyone to copy. We ran exactly this on the test database: a signed-out request read every customer and amount in the table.
This is the fix we expect to see most often in the next few weeks, because it’s what an AI tool reaches for when told “make the error go away”. The new default exists to stop that exposure, and a blanket grant puts it straight back.
With RLS on and the same grant, the signed-out request returned zero rows instead, so the grant itself isn’t the danger. The danger is a grant without RLS.
“Permission denied for sequence”
If a table uses serial or bigserial ids, inserts will fail with a second, related error even after you grant the table:
ERROR: permission denied for sequence posts_id_seq The serial column draws its numbers from a separate sequence object, and that also needs a grant now:
grant usage, select on sequence public.posts_id_seq to authenticated; Two ways to avoid it entirely, both of which we confirmed insert fine with only the table grant: use uuid ids with default gen_random_uuid(), or an identity column, id bigint generated always as identity primary key. Most AI tools already generate uuid ids for Supabase.
Check your own project in two queries
Has the change reached your project? This creates an empty table, asks whether the API role can see it, and removes it. Run it in the Supabase SQL editor:
create table public._grant_check (id int);
select has_table_privilege('authenticated', 'public._grant_check', 'select') as new_tables_auto_exposed;
drop table public._grant_check; false means new tables now need explicit grants. true means your project still has the old default.
Which tables are reachable, and which are exposed? This lists every table and view in public with what the API roles can read and whether RLS is on:
select c.relname as "table",
has_table_privilege('anon', c.oid, 'select') as anon_can_read,
has_table_privilege('authenticated', c.oid, 'select') as signed_in_can_read,
c.relrowsecurity as rls_on,
case
when has_table_privilege('anon', c.oid, 'select') and not c.relrowsecurity
then 'EXPOSED: anyone can read it'
when not has_table_privilege('authenticated', c.oid, 'select')
and not has_table_privilege('anon', c.oid, 'select')
then 'not reachable from the API'
when not c.relrowsecurity then 'reachable, RLS off'
else 'ok'
end as verdict
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p', 'v', 'm')
order by verdict, 1;| Verdict | What it means | What to do |
|---|---|---|
| EXPOSED: anyone can read it | Signed-out visitors can read every row | Enable RLS and add policies today, or revoke the anon grant |
| reachable, RLS off | Any signed-in user can read every row | Enable RLS and add policies |
| not reachable from the API | The app can’t use it | Fine for internal tables; otherwise grant it |
| ok | Granted, with RLS on | Check the policies say what you mean |
Run it against a project that has existed for a while and you may find the more uncomfortable result: an old table, created under the previous default, that is still exposed. In our test, a table from before the change with RLS off was still readable by anon afterwards. The 30 October change protects new tables only. It doesn’t fix the ones you already have.
Supabase’s Security Advisor flags the same problems in the dashboard, under Advisors.
If you build with Lovable, Bolt or another AI tool
You will meet this change through your AI tool, because it’s the one creating tables. Supabase says its error hint is written so an agent can correct itself, and its own agent skill already includes the grant step. You can’t assume every tool has caught up, so tell yours explicitly. A rule like this, in your project instructions or system prompt, covers it:
When you create a table in the public schema of Supabase, do all of this in the same migration:
1. Grant only the privileges each role needs: authenticated for signed-in users;
anon only if signed-out visitors must read the table.
2. Enable row level security on the table.
3. Create policies for every operation the app performs.
Never grant to anon on a table without RLS, and never fix "permission denied" by granting all.Then check the result yourself rather than trusting the tool’s summary. Sign out of your app, copy one of its own requests from the browser’s Network tab and run it again. Nothing private should come back. That check, and nineteen more, are in the free pre-launch checklist.
Can you turn it off?
Yes. Supabase documents how to restore the old default with alter default privileges ... grant ... to anon, authenticated, service_role, in the FAQ of the changelog entry. We wouldn’t. Turning it off brings back exactly the behaviour that let AI-generated tables go live before anyone secured them. Three lines of SQL in each new migration is a small price, and the grants also make your migrations say plainly who can reach what.
Who doesn’t need to do anything:
- Apps that never use the Data API and only connect to Postgres directly.
- Tables in the
auth,storageandrealtimeschemas, and any custom schemas. The change only touchespublic. - Tables that already existed before the change reached your project, though the second query above is still worth running on them.