Home / Guides / Supabase "permission denied for table" after the Oct 30 change: the safe fix

Supabase "permission denied for table" after the Oct 30 change: the safe fix

Supabase new table returning 42501 permission denied since Oct 30? Turn on RLS, add policies, then grant only the roles that need it. Never grant all to anon.

Check your live site

One shortcut past this error is switching to your service_role key. If your AI did that, the key is now in your live JavaScript, where anyone can use it. The free scan checks in about 10 seconds.

Uses 1 of your 3 free scans today; a site scanned in the last 24 hours answers from that scan. Passive and read-only: we only look at what any browser can see. Only check sites you own or may test.

If a new Supabase table returns 42501 permission denied for table since October 30, 2026, the table has no grant yet, and nothing is broken. The safe fix is to turn on Row Level Security, add a policy, and then grant only the role that needs the table, usually authenticated, never grant all to anon.

What changed

Supabase stopped exposing new tables in the public schema to the Data API automatically. New projects have worked this way since a gradual rollout that began May 30, 2026. From October 30, Supabase began applying it to existing projects, which is why older apps are hitting it now.

In Supabase's words: new tables "require an explicit opt-in (via a Postgres grant) before the Data API can see them." The Data API is what supabase-js calls. Direct Postgres connections are not affected, and neither are tables you already had: they "keep their current grants and stay reachable."

The error you see

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

In supabase-js, data is null and error.code is "42501". The HTTP status is 401 for signed-out requests and 403 for signed-in ones.

Look at the hint. It suggests a grant to anon, and AI coding tools tend to paste hints as the fix. That grant may be right for a public table. For anything private, it is the wrong fix.

The safe fix, in this order

Run this in the SQL Editor or a migration. Change notes and user_id to your table and column.

-- 1. RLS on first
alter table public.notes enable row level security;

-- 2. A policy for each thing signed-in users may do
create policy "read own notes" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);
create policy "add own notes" on public.notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

-- 3. Grant only what the app uses, to the role that uses it
grant select, insert on table public.notes to authenticated;

Why RLS first: the grant is what opens the table to the API. If RLS and policies are already in place when the grant lands, there is no moment where the table is open.

Only for data every visitor should see, such as published posts, add anon:

alter table public.posts enable row level security;
create policy "anyone can read posts" on public.posts
  for select to anon, authenticated using (true);
grant select on table public.posts to anon, authenticated;

Do not do this

grant all on public.notes to anon; on a table without RLS. Your anon key ships in your JavaScript by design. Anyone who opens your site can copy it and read or change every row that grant allows. Turning RLS off does not fix 42501 either, because Postgres checks the grant before RLS.

If your app is older, check your old tables too

Before this change, Supabase granted every new table to anon when it was created, "including tables a developer forgot to protect." The change only covers new tables. Old ones kept those grants. This query lists tables a public role can reach with RLS off:

select c.relname as table_name, r.rolname as role
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
cross join pg_roles r
where n.nspname = 'public'
  and c.relkind in ('r', 'p')
  and not c.relrowsecurity
  and r.rolname in ('anon', 'authenticated')
  and has_table_privilege(r.rolname, c.oid, 'select, insert, update, delete');

Every row it returns needs RLS and policies.

Check the fix from the outside

SQL tells you what you set. It does not show what a stranger with your anon key can read. The Fix Pack ($29, one time) includes a read-only database check: after you verify you own the app by adding one line to your homepage, we take the table names your app's code uses (up to 12, and only if your Supabase URL and public anon key are in the app's JavaScript) and test each one with that key. We count rows and never download them. You also get 2 re-scans to confirm each fix.

FAQ

My old tables still work. Am I fine?
They work because they kept their grants. Run the query above to see if any of them are open.

Why do I still get 42501 after the grant?
Each action needs its own grant. Granting select does not allow insert. If the message says "violates row-level security policy", the grant is fine and you need a policy.

I dropped and recreated a table, or restored a backup, and it broke.
A recreated table starts with no grants. Put the grant in the same migration that creates the table.

Should my edge function use the service_role key instead?
Only on the server, and new tables may still need a grant to service_role. Never put that key in browser code. The free scan checks for exactly that.

Can I turn the old behavior back on?
Supabase documents rollback SQL, but it brings back automatic grants to anon on every new table. We don't recommend it.

What is PGRST205?
A different error: the API can't find the table at all, for example a typo or a table in a schema that isn't exposed.

Sources: Supabase changelog 45329, Supabase docs (Securing your API, Handling errors in supabase-js), PostgREST error reference. Checked November 2, 2026.

Want the whole pre-launch process in one place? Get App Security for Vibe Coders, our $9 PDF playbook. See what's inside.

More guides