Skip to content

Supabase's October 30 change: new public tables need a grant for Data API access. What to doSupabase's October 30 change is live: new public tables need a grant for Data API access. What broke and how to fix it

24 tools · 139 fixes · 42 issue guides · Sources: Supabase, GitHub, postgresql.org, Vercel + 53

Issues

How do I add Supabase Data API grants to my migrations?

Write each role's grant in the migration that creates the table, next to enable row level security and the policies. Grant anon only for public tables.

Share link · Share on X

By ShipSecurely Editorial · Updated · How we make these guides

Shows up in: Supabase

What it is

Put each role's grant in the same migration file that creates the table, together with alter table ... enable row level security and the policies. Grant only what each role needs, and grant select to anon only when the table is meant to be publicly readable.

This matters for tables your app reaches through the Data API: supabase-js, REST, or GraphQL. An app that reads and writes Postgres only over a direct connection (psql, an ORM, or an app server with a connection string) is not affected, per the Supabase changelog. The changelog names October 30, 2026 as the day the new behavior is enforced on existing projects, and existing tables keep their grants.

Once it reaches a project, a new table in public with no grant is not reachable through the Data API. Writing the grant into the migration keeps it next to the create table, so every environment that runs the migration gets it.

What does a migration with grants look like?

One migration file. The table first, then the three steps in the order the changelog lists them: grants, enable row level security, policies.

-- supabase/migrations/<timestamp>_create_notes.sql
create table public.notes (
  id bigint generated always as identity primary key,
  user_id uuid not null references auth.users (id) on delete cascade,
  body text not null
);

-- 1. Grants: only the roles and commands the app uses
grant select, insert, update, delete on public.notes to authenticated;
grant select, insert, update, delete on public.notes to service_role;
-- Only if the table is meant to be publicly readable:
-- grant select on public.notes to anon;

-- 2. Enable RLS
alter table public.notes enable row level security;

-- 3. Policy: signed-in users reach only their own rows
create policy "users manage their own notes"
  on public.notes
  for all
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

The changelog says to treat grants, row level security, and policies as a unit. If the grant is missing, Postgres rejects the query before row level security comes into play. Securing your API says to grant the minimum privileges each role needs and to bundle grants with your row level security setup in the same migration. The same page splits the job: grants decide which roles can reach a table, and row level security decides which rows.

Creating API routes shows the same shape: read-only for anon, full read and write for authenticated and service_role. It says "Behind the scenes, the API checks your Postgres permissions", and that only objects with explicit grants are exposed. The example above leaves anon out, because notes belong to signed-in users.

The service_role line is for server code that uses the secret key. The changelog and Securing your API both grant to service_role in their examples.

A missing grant returns error 42501, and the full error and fix are in Supabase's Oct 30 change: new tables aren't exposed automatically.

The changelog's revoke statements also cover sequences. The example uses an identity column. If a table uses a serial column, test an insert as authenticated, and add a sequence grant for that role if it fails.

How do I add grants with the Supabase CLI?

  1. Create the migration file:

    supabase migration new create_notes
    

    supabase migration new creates a new file in supabase/migrations named <timestamp>_create_notes.sql.

  2. Write the SQL from the example above into that file.

  3. Apply it locally:

    supabase db reset
    

    supabase db reset requires the local stack to be running. It recreates the local Postgres container, applies every migration in supabase/migrations, and seeds test data if it is defined in supabase/seed.sql.

  4. Preview, then push to the linked project:

    supabase db push --dry-run
    supabase db push
    

    supabase db push pushes all local migrations to the linked remote database, so run supabase link first. --dry-run prints the migrations that would be applied without applying them.

supabase db diff diffs the schema changes, and -f <name> "Saves schema diff to a new migration file". The CLI reference does not say whether that file contains grant statements. Open the generated file, check it for grant statements, and add any that are missing before you push.

What changes in my local config.toml?

A Supabase CLI commit, 5b12151 ("feat(api): flip auto_expose_new_tables default to false", committed June 10, 2026), describes a setting in supabase/config.toml: [api].auto_expose_new_tables. It controls default grants to anon, authenticated, and service_role on new tables, views, sequences, and functions that postgres creates in public. For functions, revoking those named roles' default grants does not remove the global PUBLIC EXECUTE default, so functions still need the separate treatment below.

  • Unset: in CLI versions containing this change, local database setup revokes the default Data API privileges covered by the commit.
  • false: same as unset.
  • true: preserves the legacy default grants and prints a deprecation warning. The commit schedules removal of the field for October 30, 2026.

The warning text in the commit is: "api.auto_expose_new_tables is deprecated and will be removed on 2026-10-30. Remove the field or set it to false to adopt the new default of revoking Data API privileges on new entities in the public schema."

If your config.toml has auto_expose_new_tables = true, remove it and put explicit grants in your migrations. With a CLI version containing this change, replay your migrations using supabase db reset in your local test database and test access as each role. This checks your migrations against the changed table defaults; it does not establish that every privilege matches your hosted project. The generated template in that commit carries this commented setting:

[api]
# auto_expose_new_tables = true

Supabase's CLI config reference does not list this setting as of October 28, 2026, so the commit is the source for it. That page does say config changes need a restart: run supabase stop, then supabase start. The develop JSON schema still describes the previous unset behavior, contradicting the commit's implementation and the develop template.

How do I add grants with Prisma?

This only matters when the app also reads these tables through supabase-js or another Data API client. Prisma over a direct connection is not affected by the October 30 change.

Prisma's Unsupported database features page, written for Prisma ORM 7, gives the steps for SQL the Prisma schema cannot represent:

  1. Generate the migration without applying it:

    npx prisma migrate dev --create-only
    
  2. Open the generated migration.sql. After the create table Prisma wrote, add the grant, enable row level security, and create policy statements from the example above.

  3. Apply it:

    npx prisma migrate dev
    
  4. Commit the modified migration.

Prisma's page names triggers, views, and stored procedures as its examples. Grants and policies follow the same steps.

Prisma's current Editing a migration page describes Prisma ORM 8, where you edit a migration.ts file, recompile it, and write raw SQL with rawSql. The --create-only flow above is the ORM 7 flow. If you are on ORM 8, follow that page.

How do I add grants with Drizzle?

Same condition: this only matters when the app also uses the Data API for these tables.

Drizzle's custom migrations page is for DDL that Drizzle Kit does not support. Generate an empty SQL migration:

npx drizzle-kit generate --custom --name=create_notes

Write the SQL from the example above into it, then apply it:

npx drizzle-kit migrate

drizzle-kit migrate reads the migration .sql files, applies the ones not yet applied, and records them in a migrations log table.

Drizzle has native helpers for part of this. Its RLS page documents pgTable.withRLS(...) to enable row level security, pgPolicy to define policies, and the predefined roles anonRole, authenticatedRole, and serviceRole exported from drizzle-orm/supabase. That page does not document a grant helper, so keep the grant statements in the custom SQL migration.

How do I check a table's grants after the migration runs?

Supabase's Database API 42501 errors page gives this query:

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

Each row is a role and one privilege it holds on that table. The roles you granted should be there, and anon should appear only if the table is public on purpose. If another schema has a table with the same name, add and table_schema = 'public'.

To see whether the anon key can read rows, the Supabase RLS checker counts them table by table in your browser. It does not cover grants: a 401, 403, or 42501 from it is inconclusive for the grant change, so run the query above for grants. The full 42501 flow is in Supabase's Oct 30 change: new tables aren't exposed automatically.

Do new functions need the same treatment?

Yes, and the October 30 change does not do it for you. Securing your API says new functions are callable by every role: Postgres grants execute to PUBLIC through a global default, and a revoke limited to one schema cannot remove it. Put the revoke in the same migration that creates the function:

create function public.count_my_notes()
returns bigint
language sql
security invoker
set search_path = ''
as $$
  select count(*) from public.notes where user_id = (select auth.uid())
$$;

revoke execute on function public.count_my_notes() from public, anon;
grant execute on function public.count_my_notes() to authenticated;

Supabase's lint 0028 docs say to revoke from PUBLIC as well, because the Postgres default grant lives there, and that revoking from anon alone usually leaves the function callable by every signed-up user. The full guide is Can anyone call your Supabase functions?.

What if an AI tool creates my tables?

The changelog says that without a grant, the API cannot see the table however it was created: the SQL editor, migrations, the Management API, MCP, the CLI, or an AI coding tool. For AI coding tools, it says to update the tool's system prompt or adopt the Supabase agent skill, which "includes the grants step", and that agents can self-correct from the PostgREST error hint. The agent prompt below can be pasted into the tool's instructions.

How to check in 5 minutes

  1. Open the migration that creates the table. Confirm there is a grant line for each role the app uses, and nothing for anon unless the table is public on purpose.

  2. Confirm enable row level security and at least one create policy are in the same file.

  3. Run supabase db reset locally, then call the table with the publishable key, once with no session and once with a signed-in user's session, to confirm each role gets what you meant.

  4. Run the grants query above against the project and read the rows for the table.

How to fix

Supabase

For a new table that already exists without its grants, add a new migration with the missing grants instead of editing one that has already run. Existing tables keep their grants, per the changelog, so only tables created after the change need this.

  1. Create a migration: supabase migration new grant_notes.

  2. Add only the grants the app uses:

    grant select, insert, update, delete on public.notes to authenticated;
    grant select, insert, update, delete on public.notes to service_role;
    
  3. Confirm the table has enable row level security and policies that cover each command you granted. If either is missing, add it to the same migration.

  4. Run supabase db reset locally and test as each role.

  5. Run supabase db push.

Do not add the grant without row level security. Securing your API says tables exposed through the Data API without row level security can be accessed by any role with a matching grant.

Copy the fix prompt

Paste this into Cursor, Lovable, Bolt, or Claude Code. Read the diff before you merge it.

You are adding Supabase Data API grants to the migrations in this repo. Do not add features. Do not run anything against my database without asking me first.

1. When you create a table in the public schema, write the grants in the same migration file as the create table. Grant only the commands each role the app uses needs: authenticated for signed-in users, service_role for server code with the secret key.
2. Never grant anything to anon unless I say the table is public. For a public table, grant select only.
3. In the same migration, enable row level security on the table and add a policy for each command you granted. Policies for signed-in users check (select auth.uid()) against the owner column. Never turn row level security off.
4. When you create a function in public, revoke execute from public and anon in the same migration, unless I say the function is public. Then grant execute only to the roles that call it.
5. Do not set auto_expose_new_tables to true in supabase/config.toml. If it is there, tell me.
6. If a request returns 42501, read the hint, then add the missing grant for that role in a new migration. Do not grant to anon only because the hint names anon.
7. Show me the SQL before applying anything.

Output the migration SQL, then a short list of each table with its roles and grants.

Questions

Does the October 30 change affect my existing tables?

No. The changelog says "Existing tables are not affected in your project, they keep their current grants and stay reachable." Only new tables in public need an explicit grant. Write the grant into the migration for each one.

Do I need grants if I only use Prisma or Drizzle over a direct connection?

No. The changelog says "If your app reads and writes Postgres over a direct connection (via psql, ORM or an app server with a connection string), this change will not affect you." If the app also uses supabase-js or REST for the same tables, those tables need grants.

Should I set auto_expose_new_tables = true to make local work like before?

No. The CLI commit deprecates explicit true and schedules removal of the field for October 30, 2026. Keeping the legacy default grants locally can hide missing table grants in your migrations. Use explicit grants and test them with a CLI version containing the changed default.

Do grants replace row level security?

No. Grants decide whether a role can touch the table, and row level security decides which rows, per the changelog and Securing your API. A missing grant fails before row level security runs. A grant without row level security leaves the table open to that role.

Sources

Related

Need someone to fix it for you? See /help.

The weekly fix

The weekly fix: 3 real security mistakes in vibe-coded apps and how to fix them, every Tuesday.

One email, every Tuesday. A welcome note follows the first signup.