DEV Community

Shipsealed
Shipsealed

Posted on Originally published at shipsealed.com

Supabase's Oct 30 grants change: what breaks, and the fix that leaks

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.

What changes on October 30

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).

  • New tables — created without a 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.
  • Existing tables — not affected. They keep the grants they have today, so your running app keeps working.
  • RLS and policies — unchanged. Grants decide whether a role can touch the table at all; RLS decides which rows.

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.

What "broken" looks like

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;"
}
Enter fullscreen mode Exit fullscreen mode

Postgres rejects the query before RLS even runs. That's the new default working as intended, not a bug in your policies.

The fix that leaks

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;
Enter fullscreen mode Exit fullscreen mode

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.

The migration pattern that works

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;
Enter fullscreen mode Exit fullscreen mode

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;

Check what the API roles can reach today

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;
Enter fullscreen mode Exit fullscreen mode

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.

Keep it from coming back

The new default only protects you while every migration stays honest. Two free, MIT checks help:

  • In CI — npx airlock-migrate reads your migration files and fails the build when one creates a table without enabling RLS, or turns RLS off.
  • On the live database — 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.

Top comments (0)