← All posts
·16 min read·GuardLayer

Supabase Oct 30 Data API change: new tables need GRANTs

SupabaseData APIRLSGrantsPostgREST

TL;DR: Since October 30, 2026, Supabase's new Data API default applies to every existing project: new tables in public stop getting automatic grants for anon, authenticated and service_role, so API calls to them fail with 42501 "permission denied for table" until you GRANT access. Tables that already exist keep their grants and keep working. The safe fix is an explicit grant to authenticated plus RLS in the same migration, never grant ... to anon with RLS off, and not grant ... to authenticated with RLS off either: in an app with open sign-ups, that is one free account away from the same leak.

Updated 2026-10-31: the change is now live on every project. The short answers, checked against Supabase's changelog #45329, its GitHub discussion and the Securing your API docs:

  • What changed on October 30, 2026: Supabase applied its new Data API default to every existing project. Since then, a new table created in the public schema no longer gets automatic select, insert, update and delete grants for anon, authenticated and service_role, so the Data API (supabase-js, /rest/v1/, /graphql/v1) can't reach it until you GRANT access. Tables that already exist keep their current grants and keep working. RLS behaves exactly as before.
  • Who is affected: apps that use the Data API and create new public tables without explicit GRANTs, whether through SQL migrations, an ORM, CLI scripts, the Management API or an AI coding tool. That includes server code using the service_role key: it bypasses RLS, but it still needs table privileges. Not affected: apps that only talk to Postgres over a direct connection, and the auth, storage, realtime and custom schemas. Supabase began making this the default for new projects on May 30, 2026 (rolled out over a few weeks), so a recent project may already behave this way.
  • How to check: run the default-privileges query below in the SQL Editor. If anon and authenticated still receive SELECT, INSERT, UPDATE and DELETE on new tables, your project hasn't switched yet. In the dashboard, Security Advisor flags affected tables and shows the remediation SQL, and the Table Editor has a Data API exposure badge. After the switch, a missing grant surfaces as Postgres error 42501 "permission denied for table" (clients often report it as a 401 or 403), with a hint naming the exact GRANT you need.
  • The fix: put the grants in the same migration as the table, its RLS and its policies (SQL below). To switch early instead of waiting, turn off Automatically expose new tables under Integrations → Data API → Settings. Do not fix a failing table with grant ... to anon while RLS is off. That re-opens it to the whole internet.

What changed on October 30, 2026

Before this change, Supabase projects came with Postgres default privileges in public: every new table was automatically granted select, insert, update and delete (plus truncate, references, trigger and, on Postgres 17, maintain) for anon, authenticated and service_role. A table was reachable through the API the moment it existed, and if RLS was off, anyone holding the anon key could read and write it.

The change (changelog #45329, "Tables not exposed to Data and GraphQL API automatically") revokes those automatic API grants. Supabase's dates:

DateWhat happens
2026-04-28Changelog published. Projects can opt out of auto-exposure at creation.
2026-05-30The new behavior starts becoming the default for new projects, rolled out over a few weeks.
2026-10-30The new behavior is applied to all existing projects.

Once it reaches your project:

  • New tables in public need an explicit GRANT before anon, authenticated or service_role can use them through the Data API. New views too, since Postgres default privileges on tables also cover views.
  • New sequences no longer get automatic usage and select.
  • Existing tables keep their grants and stay reachable. What breaks is any table created afterwards without a grant, including tables your own code creates at runtime.
  • Some privileges remain. Per Supabase's docs, new tables still get truncate, references and trigger (plus maintain on Postgres 17), and new sequences keep update. The Data API doesn't use these.
  • Functions stay callable. New functions in public remain callable by every role, because Postgres grants EXECUTE to PUBLIC by default. A new RPC function is still reachable by anon unless you revoke that.

What the change does not do is touch RLS. It only removes automatic API exposure (the grant). A table can be invisible to the API and still have RLS off, and one grant ... to anon makes it public again. Conflating the two mechanisms is where the footgun lives.

How to check whether your project has switched

Run this in the SQL Editor. It lists what anon, authenticated and service_role automatically receive on new tables (r) and sequences (S) that postgres creates in public:

select d.defaclobjtype as object_type,  -- r = tables, S = sequences
       a.grantee::regrole as role,
       string_agg(a.privilege_type, ', ' order by a.privilege_type) as privileges
from pg_default_acl d
cross join lateral aclexplode(d.defaclacl) a
where d.defaclrole = 'postgres'::regrole
  and d.defaclnamespace = 'public'::regnamespace
  and d.defaclobjtype in ('r', 'S')
  and a.grantee in ('anon'::regrole, 'authenticated'::regrole, 'service_role'::regrole)
group by 1, 2
order by 1, 2;
  • The r rows for anon / authenticated include SELECT, INSERT, UPDATE and DELETE: not switched. New tables are still exposed automatically. After October 30 that can mean the old defaults were restored (for example with the rollback statements from the changelog) or the rollout hasn't reached your project yet.
  • The r rows show only REFERENCES, TRIGGER, TRUNCATE (and MAINTAIN), or there are no rows: switched. Every new table needs an explicit GRANT.

Then list the tables anon can reach right now with RLS off. Those are the holes, before and after the switch:

select g.table_name,
       string_agg(g.privilege_type, ', ' order by g.privilege_type) as anon_privileges
from information_schema.role_table_grants g
join pg_tables t
  on t.schemaname = g.table_schema and t.tablename = g.table_name
where g.table_schema = 'public'
  and g.grantee = 'anon'
  and g.privilege_type in ('SELECT', 'INSERT', 'UPDATE', 'DELETE')
  and not t.rowsecurity
group by g.table_name
order by g.table_name;

In the dashboard, Security Advisor and the Table Editor's Data API exposure badge point to the tables worth reviewing.

The error you'll see after the switch

You create a table with the SQL Editor, psql, a Prisma or Drizzle migration, or an AI tool, and the first API call fails even though the table plainly exists. Supabase documents the response as:

{
  "code": "42501",
  "message": "permission denied for table your_table",
  "hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;"
}

The cause is the grant, not your query and not your policies. The role your client calls as (anon for the anon/publishable key, authenticated for a signed-in user's JWT, service_role for the service_role/secret key) has no privilege on the table, and the hint names the role and privilege that are missing. Our companion post on permission denied for table (42501) walks through the other causes of the same error code.

Grants vs RLS: two different locks

Postgres checks access in a fixed order, and the grant check runs long before RLS:

  1. Database CONNECT
  2. Schema USAGE
  3. Table privilege (SELECT / INSERT / UPDATE / DELETE)
  4. Column and sequence privileges
  5. Row-Level Security policies

RLS is dead last. With no grant at layer 3, Postgres never reaches layer 5: the request fails with 42501 and no policy ever executes. That is why adding more RLS policies does nothing for a table the API can't see. Keep the mental model straight:

Grants decide WHETHER a role can touch a table. RLS decides WHICH rows it sees.

You almost always want both, configured together in the same migration so they never drift. The complete guide to Supabase RLS covers the row-filtering half end to end.

The fix: grants, RLS and policies in one migration

Supabase's guidance is to treat these steps as one unit: without the grant, Postgres rejects the query before RLS comes into play. For user-scoped data, grant authenticated (never anon), then enable RLS and scope the rows:

-- 1. table privileges: only the verbs each role needs
grant select, insert, update, delete on table public.notes to authenticated;
-- only if server code uses the service_role / secret key
grant select, insert, update, delete on table public.notes to service_role;

-- 2. turn RLS on so the table fails closed the moment it's reachable
alter table public.notes enable row level security;

-- 3. scope every row to its owner, for reads and writes
create policy "own rows" on public.notes
  for all to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

(select auth.uid()) instead of a bare auth.uid() lets Postgres evaluate it once per statement rather than once per row. It's a free performance win that doesn't change what the policy allows.

If an insert then fails with permission denied for sequence, the table uses a serial/bigserial key: grant usage on that one sequence (grant usage on sequence public.notes_id_seq to authenticated;). GENERATED ... AS IDENTITY columns don't need it.

If your project still auto-grants. If the default-privileges query above still shows full grants for anon and authenticated, turn off Automatically expose new tables in the Data API settings, or run the statements from Supabase's changelog in the SQL Editor. They change the defaults for future tables and sequences only, so existing objects keep their grants:

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 dangerous fix people reach for: granting to anon

Here is the trap. A new table's API call starts failing, you're in a hurry, and you "open it up" to the client with an anon grant and no RLS. The comment below blames a 404 while Supabase documents a 42501, but the reflex is the same:

-- 20261030_open_waitlist_to_api.sql
-- The REST API started returning 404 for this table after the 2026
-- Data-API change, so we "opened it up" to the client. This hands every
-- anonymous visitor direct read/write access with no policy in front of it.
create table public.waitlist (
  id uuid primary key default gen_random_uuid(),
  email text not null,
  referred_by text,
  created_at timestamptz default now()
);

grant select, insert on public.waitlist to anon;
guardlayer scan · supabase/migrations/20261030_open_waitlist_to_api.sqlLive engine output
Check failed
67/100 · C
  • Criticalsupabase/migrations/20261030_open_waitlist_to_api.sql:12

    Table or view granted to the anon role without RLS

    For a table, enable RLS (ALTER TABLE <t> ENABLE ROW LEVEL SECURITY;) and add scoped policies. For a view, create it WITH (security_invoker = true) and repeat that clause on every CREATE OR REPLACE VIEW. Materialized views and foreign tables cannot be protected: revoke the anon grant or move them to a schema the Data API does not expose. Prefer per-table grants over ON ALL TABLES IN SCHEMA public. Only keep an anon grant for genuinely public data that is still RLS-protected.
  • Warningsupabase/migrations/20261030_open_waitlist_to_api.sql:5

    Table created without enabling RLS

    Add ALTER TABLE <table> ENABLE ROW LEVEL SECURITY; plus access policies right after the CREATE TABLE, and grant anon / authenticated only the privileges the app needs, per table, after RLS is on. A later migration in the same scan that enables RLS (or drops the table) clears this warning. It still shows when the enable is in a file whose run order is unknown (a schema.sql with no version number) or not part of this scan (a pull-request scan sees only the files the PR changes).

Before the change, a stray anon grant was mostly redundant: default privileges had already granted the table. Now that an explicit grant is the only way a new table reaches the API, grant ... to anon with RLS off is a deliberate public re-exposure. The anon key ships in every browser bundle, so this hands every anonymous internet visitor direct read/write on the table with no policy in front of it. That is exactly the CVE-2025-48757 class of leak — the May 2025 disclosure where 303 endpoints across 170 production apps leaked data through the public anon key.

Note that a real scan of this migration surfaces two findings, not one: supabase/grant-to-anon (critical: the anon grant on a table with no RLS) and supabase/rls-missing-on-table (a warning: a public table created without RLS in the same file). The migration trips both because it does both wrong things at once. With the default gate setting, the critical anon grant is what fails the check; the missing RLS on its own would only be reported as a warning.

The quieter version: granting to authenticated with RLS off

The anon grant is the loud mistake. The quieter one is the "safe" fix from the 42501 hint, applied to a table that never got RLS:

create table public.invoices (
  id uuid primary key default gen_random_uuid(),
  customer_id uuid not null,
  amount_cents int not null
);

-- fix: permission denied for table invoices
grant select, insert, update, delete on public.invoices to authenticated;

It looks responsible because it never mentions anon. But with RLS off, authenticated means every signed-in user, and every one of them can read, change and delete every invoice, including other customers' rows. If anyone can sign up for your app, which is the default for most Supabase projects, the gap between this and an anon grant is one free account.

The fix is the same as above: enable RLS and add a policy in the same migration as the grant. A grant answers "can this role touch the table?" and RLS answers "which rows?". A grant on its own answers the first question with yes and never asks the second.

The same goes for the broad versions people paste in to make the error go away: grant ... on all tables in schema public to authenticated reaches every table that already exists, and alter default privileges ... grant ... on tables to authenticated brings back automatic exposure for every table created afterwards. Both undo the point of the change for any table without RLS.

When granting to anon is actually fine

An anon grant is legitimate for genuinely public data — a blog_posts-style table with nothing private — but only if RLS stays enabled with a read-only policy so anonymous writes are still blocked:

alter table public.blog_posts enable row level security;
grant select on public.blog_posts to anon;
create policy "public read" on public.blog_posts
  for select to anon using (true);

"Public read" and "RLS off" are not the same thing. Read-only with a using (true) policy blocks anonymous inserts and updates; RLS off blocks nothing. If you do reach for using (true), know its failure modes first — it's the right tool only for data that is truly meant to be world-readable.

FAQ

Did my app break on October 30, 2026? Not by itself. Existing tables keep their current grants and stay reachable. What breaks is any public table created after the switch without a GRANT, whether it comes from a migration, an ORM push, an AI coding tool or code that creates tables at runtime. Its API calls fail with 42501 until you grant access.

Why does my new table return "permission denied for table" (42501)? Because new tables no longer get automatic grants for anon, authenticated and service_role. The table exists, but the role your client calls as has no privilege on it. Grant that role the privileges it needs (the error's hint names them) and enable RLS in the same migration.

Does the change turn RLS on for me? No. It only removes automatic API exposure (the grant). RLS is a separate mechanism you still enable yourself with alter table ... enable row level security. A table can be invisible to the API and still have RLS off.

Should I grant to anon, authenticated or service_role? authenticated for anything tied to a logged-in user; service_role only if server code uses that key; anon only for data you intend to be world-readable, and even then keep RLS on with a read-only using (true) policy. Never give anon write access unless anonymous writes are the actual goal.

My INSERT still fails after I granted the table. Why? With RLS on, an insert also needs a for insert or for all policy whose check passes for the new row; otherwise Postgres rejects the row with "new row violates row-level security policy". If the message names a sequence instead, grant usage on that sequence. GENERATED ... AS IDENTITY columns don't need it.

Does this affect auth, storage, realtime or my custom schemas? No. Supabase says the change only touches default privileges in public; tables in those schemas keep their current grants and defaults.

Can I go back to automatic exposure? Supabase's changelog documents a rollback for projects that opted in early: restore the default privileges, then bulk-grant the tables created since. It doesn't say whether that stays supported after October 30, and it would bring back the exact default that exposed tables nobody had locked down. Explicit per-table grants are the better path.

How do I check what's actually granted right now? Query the catalog, which is the runtime source of truth: select table_name, grantee, privilege_type from information_schema.role_table_grants where table_schema = 'public'; lists every table grant, and the RLS-off query above narrows it to the holes. Any privilege granted to anon on a table with RLS off needs fixing.

Where GuardLayer fits (and where it doesn't)

GuardLayer ships a rule, supabase/grant-to-anon (CWE-732), that flags a public-schema table created and granted to the anon role (or PUBLIC) in the same migration file when that file leaves RLS off for it — the exact pattern in the sample above. For a table, that finding is critical, so with the default gate setting the check fails. (If the same file enables RLS in a loop over table names it looks up at run time, such as a pg_tables query, the scanner can't tell which tables that covers, so it reports the grant as a warning instead.) Views that aren't security_invoker, materialized views and foreign tables created and granted the same way are flagged too, as warnings. A table created without RLS is also reported by supabase/rls-missing-on-table (a warning), and that finding says when the same file grants the table to authenticated or anon: directly, through GRANT ... ON ALL TABLES IN SCHEMA public after the CREATE, or through ALTER DEFAULT PRIVILEGES before it. Since the change, that grant is what makes the table reachable. Be clear about the limits, because it's a static, per-file scanner. It correlates the CREATE, the GRANT and ENABLE ROW LEVEL SECURITY only within a single .sql file, so RLS enabled in a later migration is a false positive (for a table, one that still fails the check), a grant is suppressed when the same file enables RLS on that table by name, or later revokes every privilege it granted from the same role(s) (a REVOKE inside a function body doesn't count, since it only runs if the function is called), and a grant on a table created in an earlier migration is not checked at all. A schema-wide GRANT ... ON ALL TABLES IN SCHEMA public TO anon is flagged only for the tables that same file created without RLS before the grant; in a file that creates no tables, it produces no finding. A grant of only TRUNCATE, REFERENCES, TRIGGER or MAINTAIN, the privileges new tables still receive after the switch, isn't flagged, because the Data API doesn't use them. And it cannot see live privilege state — whether a migration was actually applied, whether your project has switched to the new default yet, or grants added via the dashboard or psql outside the repo. For that, the two catalog queries above are the source of truth. GuardLayer also doesn't block your merge on its own: it posts a check, and blocking requires you to make it a required status check via branch protection. What it does do is catch the anon-grant-without-RLS pattern in code review, before that "quick fix" for a failing new table ships a public table to production. Want to gut-check a migration right now? Paste it into the Supabase security checker — it runs the same rule in your browser, instantly.

See which of your Supabase tables anyone can read, free

Connect your Supabase project read-only. GuardLayer checks RLS, policies, grants and functions as they are right now, and gives you the SQL to fix each problem. It never reads your rows and can't change anything.

Check my Supabase project

Prefer to check code instead? Scan your repo, no signup.

Keep reading