Supabase stops auto-granting on Oct 30. Here is what breaks, and five traps the changelog does not mention.
DEV Community

Supabase stops auto-granting on Oct 30. Here is what breaks, and five traps the changelog does not mention.

I replayed the migration histories of 100 public Supabase projects on a database without automatic grants. In 98 of them, at least one table ended up unreachable through the Data API for some role. In 92, at least one row level security policy could never apply, because the role it names lacks the privilege the policy's command needs. Not one of the 100 histories contains the opt-in migration. That is not a statement about those projects' production databases: existing tables keep their grants. It is a statement about what their next table, their next preview branch, or their next fresh environment will look like. Here is why, and what to look for. What changes Supabase has announced that from 2026-10-30, new tables, views and sequences in the public schema stop getting automatic grants for anon , authenticated and service_role on every existing project (announcement). Until now, a default privilege did the work for you. postgres created a table in public , and the three API roles got access to it immediately. You wrote your row level security policies and forgot that grants existed at all. After the change, you need to say it: create table public.todos ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id), title text not null ); alter table public.todos enable row level security; create policy "read own todos" on public.todos for select to authenticated using (auth.uid() = user_id); -- the new part grant select on public.todos to authenticated; grant select, insert, update, delete on public.todos to service_role; Forget the last two lines and nothing complains. The migration applies cleanly, the policy looks right in the dashboard, and the first request returns: { "code": "42501", "message": "permission denied for table todos" } Existing tables are not affected. The risk is every table you create from now on, and every environment that is built from your migration history instead of from production. Five traps These are the parts that are easy to miss, even after reading the announcement. 1. Replaying your history turns the grants back on If your first migration came from supabase db pull , it probably contains lines like these: alter default privileges for role "postgres" in schema "public" grant all on tables to "anon"; alter default privileges for role "postgres" in schema "public" grant all on tables to "authenticated"; alter default privileges for role "postgres" in schema "public" grant all on tables to "service_role"; pg_dump copies your default privileges as they were when you pulled. Replay that file on a preview branch or a local supabase db reset , and every table your later migrations create gets full access again. Supabase's own branching docs say so. Production no longer has those defaults, so the table that works in your preview branch fails once it ships. The local stack has a second version of the same trap: with the current CLI, new tables are exposed automatically unless you set auto_expose_new_tables = false under [api] in supabase/config.toml . A local test run tells you nothing about grants. Of the 100 histories, 2 end with default grants still switched on by their own statements, after replaying every file. The fix is a migration that switches them off again, after the baseline. 2. The revoke is narrow Per the SQL Supabase published, the opt-in removes select , insert , update and delete on tables, and usage and select on sequences: alter default privileges for role postgres in schema public revoke select, insert, update, delete on tables from anon, authenticated, service_role; alter default privileges for role postgres in schema public revoke usage, select on sequences from anon, authenticated, service_role; The old default was grant all . So truncate , references and trigger (plus maintain on Postgres 17 and later) still go to anon and authenticated on every new table. Row level security does not apply to truncate or references . If you want clients to hold only what you granted, revoke the rest yourself: revoke truncate, references, trigger on public.todos from anon, authenticated; 3. service_role bypasses row level security, not grants A common assumption is that the service role key can do anything. It bypasses policies, but Postgres checks privileges before it looks at policies, and service_role is not a superuser. Your edge functions, cron jobs and admin tools that use the service role key get the same 42501 on a table nobody granted to service_role . grant select, insert, update, delete on public.audit_log to service_role; This was the most common finding in the corpus: 97 of the 100 histories create at least one table service_role cannot read or write without the automatic grants. 4. Policies without grants are dead A policy never grants access. It only narrows access a role already has. This policy: create policy "insert own messages" on public.messages for insert to authenticated with check (auth.uid() = sender_id); does nothing unless authenticated holds insert on public.messages . Without the grant, the request fails before the policy is evaluated. The policy looks like access control, reads like access control, and never runs. In 87 of the 100 histories, a table has policies for anon or authenticated while that role holds no data privilege on it at all. Counting every mismatch, including a for update policy on a table the role can only select, 92 have at least one policy that can never apply. 5. serial columns need their sequence create table public.orders ( id bigserial primary key, user_id uuid not null, total numeric not null ); grant select, insert on public.orders to authenticated; The insert still fails. A serial column's default calls nextval() on its own sequence, and nextval() needs usage (or update ) on that sequence. The announced revoke removes sequence usage too: grant usage on sequence public.orders_id_seq to authenticated; Identity columns (generated always as identity ) and uuid keys do not need this: Postgres does not check sequence privileges for identity columns, and gen_random_uuid() uses no sequence at all. This one is rare (1 history in the corpus), but when it happens the error names a sequence nobody remembers creating. How the numbers were measured The corpus is 100 public GitHub repositories with at least three files in supabase/migrations , pushed within the last year, not forks, each pinned to a commit. Median history length: 30 migrations. Each history was replayed twice: once as a database without automatic grants would build it (every migration checked), and once as the project is configured today. In total the replays flagged 5,820 distinct tables and views as unreachable for at least one role, a median of 21 per project (for scale, 6,601 relations are in scope at the end of the histories). The parser read 103,326 statements and rejected 0.4% of them; each kind of rejected statement was checked against its source, and all were invalid SQL, syntax Postgres does not have, or not SQL at all. Results are published as aggregates only; no project is named. Check your own project I turned these checks into a linter: npx supabase-grants-lint check Run it from your project root, the folder that contains supabase/ . If your migrations live somewhere else, point at them: npx supabase-grants-lint check --dir db/migrations . It reads your migrations, replays them in order into a model of who holds which privilege on which table and sequence, and prints the grant that fixes each finding: supabase/migrations/20261002120000_add_todos.sql 1:1 error GL001 public.todos is created without a grant to service_role: ... fix grant select, insert, update, delete on public.todos to service_role; 10:1 error GL003 Policy "read own todos" on public.todos is for select by authenticated, but authenticated holds no select privilege on it: ... fix grant select on public.todos to authenticated; npx supabase-grants-lint doctor gives a readiness report: whether an opt-in migration exists, whether replaying the history turns automatic grants back on, and which existing tables a fresh database would not expose. To keep it from happening again, run it on every pull request: npx supabase-grants-lint init --since next This writes a config and a GitHub Actions workflow. --since next enforces every migration you add from now on and leaves your history alone, so you do not have to fix a long history before you can turn it on. Findings show up as annotations on the pull request. It reads files only. It never connects to your database and sends no telemetry. How it was built This started as a test in a production app with about 60 tables. When I opted that project in early, I wanted proof that every table would still be reachable, so I wrote a test that replayed the whole migration history and compared the result with the grants production actually had. Every trap above is something that test caught. The open source version is a rewrite, not a copy. It parses migrations with the real Postgres parser (compiled to WebAssembly), and replays them through a small model of grants, default privileges, sequences and policies. Every rule has failing and passing fixtures, and the rules and the model are mutation-tested, so a check that stops catching its bug fails the build. Before release, it ran over the corpus above, and every false positive I found in a hand-checked sample became a fixture and a fix. Try it before Oct 30 - Run npx supabase-grants-lint doctor on your project. - Add the GitHub Action, so the next migration is checked before it ships. - If it flags something that works, or misses something that does not, please open an issue with the SQL: false-positive reports are the most useful thing you can send. The code is MIT licensed at github.com/guptaaman678/supabase-grants-lint. A star helps other people find it before the deadline. Not affiliated with or endorsed by Supabase. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.