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.
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.
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.
Full guide with every SQL block tested: Supabase's Oct 30 grants change.