DEV Community

Cover image for Supabase Data API exposure changes on 30 October: grant tables explicitly
Dave Kurian
Dave Kurian

Posted on Originally published at otf-kit.dev

Supabase Data API exposure changes on 30 October: grant tables explicitly

On a project with the new default, a table you just created is invisible to the Data API until someone grants it. Your app gets an error where it used to get rows, while the table sits in your database looking fine. Supabase announced the change on 28 April 2026, and it reaches every existing project on 30 October 2026.

This post covers the exposure change only: what changed, the dates, the grants to add, and how to check them before the 30th. For the policy side, see the row-level security production checklist.

What changed

Until now, a new table in the public schema was reachable through the Data API as soon as it existed. The Supabase changelog states the new rule in one line: "New tables in the public schema will no longer be exposed to the Data API automatically." A new table now needs an explicit Postgres grant before the Data API can see it.

The changelog gives three dates:

  • 28 April 2026: the new setting is available for new projects, at project creation.
  • 30 May 2026: it becomes the default for new projects, rolled out over a few weeks.
  • 30 October 2026: it is applied to all existing projects.

Existing tables are not touched. The changelog says they "keep their current grants and stay reachable." The change matters most for tables you create from now on, and for any project that moves to the new default.

If your app only uses a direct Postgres connection, such as an ORM, psql or a server with a connection string, the changelog says it is unaffected. The change is about the Data API, which supabase-js and the other client libraries call over HTTP.

Why a table goes missing

The Data API docs put the rule plainly: "A table isn't reachable through the Data API unless you have granted a role privileges on it." A missing grant returns a 42501 error from PostgREST.

On existing projects today, new tables in public get select, insert, update and delete for anon, authenticated and service_role by default. The docs say Supabase is "changing the platform default to revoke these automatic grants so that exposure becomes opt-in." That default is the thing you need to plan around.

1. Find the tables the Data API can reach

Run this in the SQL Editor. It is our query, not Supabase's, so check the output against your own schema:

select table_name, grantee, privilege_type
from information_schema.role_table_grants
where table_schema = 'public'
  and grantee in ('anon', 'authenticated', 'service_role')
order by table_name, grantee;
Enter fullscreen mode Exit fullscreen mode

A table your app needs that is missing from the output has no grants, so it stays unreachable until you add them. Also open the Security Advisor. The changelog says it will flag affected tables before the date, and that owners and admins will be emailed.

A three-by-three grid of table cards, with one card dimmed

2. Grant each role the minimum

The docs recommend the smallest grant that works: read-only for anonymous clients, full access for signed-in users with RLS still applied, and full access for the service role only where server code reaches the table through the Data API.

-- Read-only access for anonymous clients
grant select on table public.your_table to anon;

-- Full access for signed-in users; RLS still applies
grant select, insert, update, delete on table public.your_table to authenticated;

-- Full access for server-side code using the service role
grant select, insert, update, delete on table public.your_table to service_role;
Enter fullscreen mode Exit fullscreen mode

Our rule is no write grant to anon unless anonymous writes are the point of the table.

3. Turn on row level security

The docs say tables created through the Supabase Dashboard have RLS on by default. Tables created in the SQL Editor or with another tool do not, so enable it yourself:

alter table public.your_table enable row level security;
Enter fullscreen mode Exit fullscreen mode

The docs warn that "Tables and views exposed through the Data API without RLS can be accessed by any role with matching grants." A grant lets a role in, and RLS decides which rows it sees. The docs also note that RLS does not apply to functions, so grant EXECUTE only to the roles that need it.

A padlock on a glowing plate on the left, and a golden key on a plate on the right

4. Choose grant-per-table or opt in early

You have two routes. The first is the grant in step 2, table by table. The second is to revoke the defaults, so every new table starts closed. The docs give the statements:

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 execute on functions from anon, authenticated, service_role;

alter default privileges for role postgres in schema public
  revoke usage, select on sequences from anon, authenticated, service_role;

alter default privileges for role postgres in schema public
  revoke execute on functions from public;
Enter fullscreen mode Exit fullscreen mode

The changelog says running the revoke statements the dashboard runs at project creation adopts the new behavior on an existing project today. The docs list the statements above, but we could not confirm they match the dashboard's exactly, so compare them before you run anything. Opting in is optional before the 30th. Our reading is that default privileges apply to objects created after you run them, so tables that already exist keep their grants. Run the statements on a staging project first. After that, every new table needs its grants in the same migration that creates it, or your app will not see it.

5. Test each role

Call your own endpoints three ways: with the anon key, with a signed-in user's token, and with the service role key from server code. Each request should return what that role is meant to see.

Our reading of the two errors you will meet: a 42501 on a table the app needs means a grant is missing. An empty result from a table you expected rows from usually points to a policy that filters every row, not to the grant.

Work backward from 30 October

Give yourself at least a week. Run the query in step 1 and list the tables your app reads and writes. Put the grants and the RLS policies into a migration, and test that migration on staging with all three roles. Run the opt-in statements on staging, then on production, and check the Security Advisor the next day.

What we could not confirm

  • The docs do not say whether the default has already taken effect on a given project. Check your project's Data API settings and the tables it exposes, rather than relying on the date alone.
  • The changelog gives the dates but no per-project migration steps. The Security Advisor and the emails are the only signals it names.
  • We have not run these steps against a live Supabase project. Test the SQL on staging before you run it on production.

Sources

Top comments (6)

Collapse
 
cekuu35 profile image
Cenk KURTOĞLU •

Good, and the grants query is the right starting point — two things that bite once you act on it. First, the new default covers tables created after the change, but an ALTER DEFAULT PRIVILEGES sitting in an old migration can silently re-grant everything, so check pg_default_acl too or your “opt-in” posture quietly becomes opt-out again. Second, a grant only makes the table reachable — reachable plus a permissive-or-missing policy is the new footgun, which is why the grant and the policy have to move together. Worth separating the failure modes while you test: a missing grant returns 42501, a policy denial returns an empty set. Knowing which one you get tells you which layer broke, and it stops you from “fixing” a policy when the real gap was the grant (or the reverse). The service_role grant is its own decision too — it has BYPASSRLS, so granting to it is a different risk class than granting to anon/authenticated and shouldn’t ride along with the same statement. Testing as the anon key rather than reading the grant table is what catches the table that is granted and simply has no policy yet.

Collapse
 
ahmedznouda profile image
Ahmed ZNOUDA •

Thanks for sticking to the exposure change only and for quoting the changelog directly. Most posts on this mix it up with RLS, and they are two different questions: the grant decides whether a role can reach the table at all, the policy decides which rows it gets.

A habit that makes this painless: put the grant statements in the same migration file as the create table, right next to "enable row level security". Then a new table can never ship half configured, whichever default the project is on.

One question: does the change behave the same for tables created by CLI migrations as for tables created in the dashboard, or does it depend on which role runs the migration?

Collapse
 
davekurian profile image
Dave Kurian •

Good habit, and the grants in the same migration is how I'd do it too. On your question: I could not confirm it from the docs, and I have not run it on a live project (the post says so). What I can say is that the opt-in statements in the docs are scoped to "for role postgres". In Postgres, default privileges apply to objects created by the named role, so a migration run as a different role would need the same statements for that role. I'd check on staging by creating a table through each route and reading the grants back.

Collapse
 
ahmedznouda profile image
Ahmed ZNOUDA •

Thanks, that matches how default privileges work in Postgres. For anyone landing here later, a quick way to read the grants back after creating a table through each route:

select grantee, privilege_type from information_schema.role_table_grants where table_schema = 'public' and table_name = 'your_table';

Enter fullscreen mode Exit fullscreen mode

If anon or authenticated is missing on one route and not the other, you know which role ran the migration.

Collapse
 
edtrackai profile image
Stack & Ship •

Useful breakdown, and the line that a grant lets a role in while RLS decides which rows it sees is the part I'd underline for anyone skimming. I'd treat the two as a pair rather than picking one: once you add explicit grants for authenticated before the 30th, make sure RLS is on for that table and has policies written for each operation, otherwise the grant alone exposes every row to anyone with matching access. The reverse also bites, since a perfect policy does nothing if the role has no grant and you just get 42501. Keeping the grant, the enable row level security line and the policies in one migration, as mentioned above, makes it hard to ship one without the others. Thanks for flagging the date early.

Collapse
 
shipmarkco profile image
Shipmark •

The no write grant to anon rule is a clean line to hold, even if you are not deep in Postgres. Read access is a choice you can argue table by table. Write access for a key that ships in the browser is harder to defend. Do any of your own projects grant anon select on a table on purpose? If so, what does that table hold?