Supabase's Oct 30 grants change: what breaks, and the fix that leaks Supabase will stop automatically granting Data API privileges on newly created tables in the public schema for all existing projects on October 30, 2026, following a gradual rollout that began May 30 for new projects. The change means new tables stay invisible to supabase-js, PostgREST and GraphQL until an explicit GRANT is issued, and the bulk-grant workaround suggested in the announcement's rollback section re-exposes every table in public, including any with row-level security disabled. The writeup recommends per-table grants combined with RLS and policies in a single migration, plus a pg_class query using has_any_column_privilege to flag tables readable by anon with RLS off. If you ship on Supabase, there's a platform change on October 30, 2026 worth five minutes of your time. Short version: a new table in public will no longer be reachable by the Data API until you GRANT it. Existing tables keep working. The trap is the quick fix for the error it throws — it can re-expose every table you have with RLS off. This is a cross-post. The original, with the full SQL and a copy-paste check, lives here: Supabase's Oct 30 grants change https://shipsealed.com/rls/supabase-october-30-grants-change/ . Until now, every table created in public got select / insert / update / delete granted to anon , authenticated and service role automatically. On Oct 30 Supabase turns that off for all existing projects new projects have had it off since a gradual rollout that started May 30 . GRANT , they're invisible to the Data API supabase-js , PostgREST, GraphQL , however you created them: SQL editor, migration, CLI, MCP or an AI tool. If you only talk to your database over a direct Postgres connection psql, an ORM, a server with a connection string , this doesn't affect you. Source: the official announcement https://github.com/orgs/supabase/discussions/45329 . The first new table after the cutover returns this instead of rows: { "code": "42501", "message": "permission denied for table invoices", "hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.invoices TO anon;" } Postgres rejects the query before RLS even runs. That's the new default working as intended, not a bug in your policies. The fastest way to make that error disappear is the bulk grant from the announcement's rollback section: -- don't paste this as a fix grant select, insert, update, delete on all tables in schema public to anon, authenticated, service role; It works — and it also grants every table in public , including the ones you never meant to expose. Any of those with RLS off is now readable and maybe writable by anyone holding your anon key, which ships in your frontend. The new default was your chance to stop exposing tables by accident; the bulk grant gives that back. Ask an AI coding tool to "fix the permission error" and this line is a likely answer. Grant per table instead, and only what each role needs. Treat grant, RLS and policies as one unit, in the same migration, so a table never exists half-configured: create table public.invoices id bigint generated always as identity primary key, user id uuid not null references auth.users id , amount numeric not null ; 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; No grant to anon here, because signed-out visitors don't read invoices. If a table uses serial / bigserial instead of an identity column and the API inserts into it, the sequence needs its own grant: grant usage on sequence public.invoices id seq to authenticated; Run this in the SQL editor. It lists every table and view in public , whether anon or authenticated can read it, and whether RLS is on: select c.relname, has any column privilege 'anon', c.oid, 'select' as anon can read, has any column privilege 'authenticated', c.oid, 'select' as auth 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 in 'r', 'p', 'v', 'm' order by 1; Any row with anon can read = true and rls on = false is a table anyone can read with your anon key. Using has any column privilege instead of the usual information schema.table privileges catches column grants too — grant select id, email on profiles to anon doesn't show up as a table privilege, but the API still serves those columns. The new default only protects you while every migration stays honest. Two free, MIT checks help: npx airlock-migrate SUPABASE DB URL=... npx airlock-rls checks the running database for tables with RLS off and policies that let everyone through. And if you just want to know right now whether your deployed app leaks — a service role key in the browser, or a table anyone can read without logging in — there's a free scanner that checks from outside, reading only row counts, never your data: shipsealed.com/service-role-scanner https://shipsealed.com/service-role-scanner/ . Full guide with every SQL block tested: Supabase's Oct 30 grants change https://shipsealed.com/rls/supabase-october-30-grants-change/ .