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

> Source: <https://dev.to/mateuszingano/supabases-oct-30-grants-change-what-breaks-and-the-fix-that-leaks-28j9>
> Published: 2026-10-18 04:00:03+00:00

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