LaunchVet

Guides

Supabase's 30 October change: fixing "permission denied" without opening your tables

On 30 October 2026, Supabase changes how new tables work in existing projects. If your app talks to Supabase through supabase-js or another client library, the first new table you create after that date will throw an error until you grant access to it.

What changed

Until now, every new table in the public schema was automatically granted select, insert, update and delete for the anon, authenticated and service_role roles. That made it reachable through the Data API (Supabase's REST and GraphQL layer) the moment it was created.

From 30 October, existing projects stop doing this. New tables in public stay invisible to the Data API until you add an explicit GRANT. New projects have worked this way since a gradual rollout that began on 30 May.

Some things stay the same:

  • Tables you already have keep their current grants and keep working.
  • Row level security (RLS) works exactly as before.
  • If your app only connects directly to Postgres (psql, an ORM, a server with a connection string), you're not affected.

Supabase's reason is that AI tools, scripts and migrations now create tables with nobody reviewing the SQL, and those tables could be exposed before anyone secured them.

What the error looks like

The request fails with Postgres error 42501:

JSON
{
  "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;"
}

The browser may show it as a 401 or 403. The hint names the missing role and privilege, so log the whole error object, not just error.message.

The shortcut to avoid

When an app breaks with "permission denied", people and AI assistants often blame RLS and switch it off. Turning RLS off doesn't fix a missing grant, because Postgres checks grants first. So the next move is often a blanket grant to anon, and you end up with a table that has RLS off and full access for anon.

Your anon key (or the publishable key that replaces it) ships inside your app, and anyone can read it from the browser. With RLS off and those grants in place, anyone holding that key can read, change or delete every row in the table.

The safe fix

Do three things together, ideally in the same migration:

  1. Grant only the privileges each role needs.
  2. Keep RLS on.
  3. Write a policy for each operation.

Here is a table where signed-in users manage their own notes and signed-out visitors get nothing:

SQL
-- 1. Grant only what each role needs (anon gets nothing here)
grant select, insert, update, delete on table public.notes to authenticated;
grant select, insert, update, delete on table public.notes to service_role;

-- 2. Keep RLS on
alter table public.notes enable row level security;

-- 3. One policy per operation
create policy "Users can read their own notes"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );

create policy "Users can create their own notes"
on public.notes for insert
to authenticated
with check ( (select auth.uid()) = user_id );

create policy "Users can update their own notes"
on public.notes for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

create policy "Users can delete their own notes"
on public.notes for delete
to authenticated
using ( (select auth.uid()) = user_id );

This assumes notes has a user_id uuid column holding the owner's ID. Signed-out visitors will still get 42501, which is correct. Grant select to anon only for data that's meant to be public, such as published posts, and pair it with a read policy for anon.

If the table uses a serial or bigserial ID, inserts also need access to its sequence:

SQL
grant usage, select on sequence public.notes_id_seq to authenticated;

generated ... as identity columns don't need this.

If an AI tool creates your tables, tell it to include grants, RLS and policies every time, or install Supabase's agent skill, which does this.

Check your tables

Run this in the SQL Editor. It lists every table in public, whether RLS is on, and what anon and authenticated can do:

SQL
select
  c.relname as table_name,
  c.relrowsecurity as rls_enabled,
  has_table_privilege('anon', c.oid, 'select') as anon_can_read,
  has_table_privilege('anon', c.oid, 'insert,update,delete') as anon_can_write,
  has_table_privilege('authenticated', c.oid, 'select') as signed_in_can_read,
  has_table_privilege('authenticated', c.oid, 'insert,update,delete') as signed_in_can_write
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind in ('r', 'p')
order by c.relrowsecurity, c.relname;

Tables with RLS off sort to the top. If one of them also shows true for anon_can_read or anon_can_write, anyone with your publishable key can reach it, so fix it first. A write column is true if the role has any of insert, update or delete.

The Security Advisor in the Supabase dashboard also flags affected tables and shows the SQL to fix them.

Checklist

  1. Before 30 October, add an explicit GRANT step to every migration or prompt that creates a table.
  2. When you see 42501, read the hint and grant that privilege to that role, and nothing broader.
  3. Keep RLS on for every table in public, including tables your AI tool created.
  4. Write a policy for each operation each role needs, and give anon access only to public data.
  5. Run the check query above and fix any table with RLS off that anon can reach.

LaunchVet's scan doesn't look for this yet, and we're adding this check.

Sources

Paste a URL. Get a grade.