Skip to content

23 tools · 104 fixes · 29 issue guides · Sources: GitHub, Supabase, OWASP, Vercel + 28

Issues

New Supabase tables need an explicit grant

From May 30, 2026 for new projects, and October 30, 2026 for existing ones, a new table in public is not on the Data API or GraphQL until you grant access. Row level security does not replace that grant. PostgREST returns HTTP 401 or 403 with JSON code 42501.

Share link · Share on X

By ShipSecurely Editorial · Updated · How we make these guides

Shows up in: Supabase, Postgres

What it is

On May 30, 2026, Supabase starts making a new default for new projects, rolled out over a few weeks: a new table in the public schema is not exposed to the Data API or the GraphQL API until you grant a role access. On October 30, 2026 that setting is applied to existing projects. The symptom is HTTP 401 or 403 with JSON code 42501 and the message permission denied for table, even when the table has row level security policies. Existing tables keep the grants they already have.

Before this change, a new table in public received select, insert, update, and delete for anon, authenticated, and service_role. A new function received execute. That is why a table showed up in the client as soon as it was created.

A direct Postgres connection is outside this Data API change. It still follows the grants of the role in the connection string. Those grants are separate from the Data API roles anon, authenticated, and service_role. service_role bypasses row level security. Keep that key on the server. Never put it in the browser.

Supabase's changelog, Tables not exposed to Data and GraphQL API automatically, is the source for the date and the behavior. It says new tables in public need an explicit GRANT before the Data API can see them, and that existing tables keep their current grants. Grants and row level security are separate. A grant decides whether a role can touch the table at all. Row level security decides which rows that role can see. If the grant is missing, Postgres rejects the query before any policy runs.

The changelog shows the JSON PostgREST returns when a grant is missing. Clients see that JSON on HTTP 401 or 403. The same JSON is documented on Securing your API, and the HTTP status is described on the 42501 troubleshooting page:

{
  "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 hint is an example of the privilege that failed. It is not an instruction to open the table to anon.

You can see what the anon key can already read, without sending that key to this site, with the Supabase exposure checker.

How to check in 5 minutes

  1. Open the migration or SQL that created the new table. If it has enable row level security and policies, but no grant for anon, authenticated, or service_role, the Data API will return 42501 for the roles you skipped.

  2. In the Supabase dashboard, open Integrations, then Data API, and look at the grants for that table. The securing your API guide describes the same screen.

  3. Call the table with the publishable key, or the anon key, and no user session. permission denied for table with code 42501 is a missing grant. An empty list does not prove a policy hid the rows. The table may simply be empty.

  4. Confirm the app is using the Data API (supabase-js, /rest/v1/, or /graphql/v1). A direct database connection follows the grants of the role in that connection string, not the Data API grants for anon, authenticated, or service_role. service_role bypasses row level security and stays on the server.

How to fix

Supabase

Put the grant, row level security, and the policies in the same migration. Grant only the commands each role needs. The changelog's own example grants select to anon, and select, insert, update, delete to authenticated and service_role, then enables row level security and adds a policy. Match the grant to the role that should have access. A table only signed-in users should read does not need a grant to anon.

-- The role that inserts from the browser, under row level security.
grant select, insert, update, delete on public.notes to authenticated;

-- service_role bypasses row level security. Server code only. Never the browser.
grant select, insert, update, delete on public.notes to service_role;

alter table public.notes enable row level security;

-- This sample assumes a user_id column. Use the real owner column.
create policy "notes_select_own"
on public.notes
for select
to authenticated
using (auth.uid() = user_id);

PostgreSQL's GRANT reference says a table grant does not extend to sequences the table uses, including a serial column. Grant usage and select on that sequence to the role that actually inserts: authenticated when a signed-in user inserts under row level security, or service_role when only the server inserts. The block below does nothing when notes.id has no sequence, so a missing sequence does not make the script fail. pg_get_serial_sequence finds the sequence behind a serial column and behind an identity column. Change authenticated in the format string if the inserting role is service_role.

do $$
declare
  seq regclass := pg_get_serial_sequence('public.notes', 'id');
begin
  if seq is not null then
    execute format('grant usage, select on sequence %s to authenticated', seq);
  end if;
end $$;

Add insert, update, and delete policies the same way when those commands are granted. An insert policy needs with check. Repeat the signed-in request and confirm the 42501 is gone, and that a stranger's row still does not come back.

Do not "fix" the error with grant all on public.notes to anon while row level security is off. That publishes every row to anyone who has the anon key, which is the bug in Supabase tables without row level security. A grant without row level security is how a new table becomes world-readable. A policy without a grant is why a secured table still returns 42501.

alter default privileges changes future objects only. The changelog's opt-in SQL revokes future table grants (select, insert, update, delete) and future sequence grants (usage, select) from anon, authenticated, and service_role. It does not remove grants from tables that already exist. Securing your API also revokes default execute on functions in public from those roles, and execute from public. A new function is not on the Data API until you grant execute to the role that should call it.

Next.js

The 42501 appears in the browser when a client calls supabase-js with the publishable key. Changing the Next.js route will not add the missing grant. Fix the SQL, then redeploy only if the app was catching that error and hiding it.

Copy the fix prompt

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

You are fixing Data API access for a new Supabase table. Do not add features. Do not print row contents.

The table returns Postgres 42501, permission denied for table, because a new table in public is not exposed to the Data API until a role is granted access. Row level security policies do not run if the grant is missing. Existing tables keep their old grants. This is the October 30, 2026 Supabase change.

Do this in the same migration:
1. Grant only the commands the role needs. A signed-in user table usually gets select (and insert, update, delete when the app writes) to authenticated, and the server role gets the commands server code uses.
2. If the inserting role is authenticated, grant usage and select on a serial sequence to authenticated. If only the server inserts, grant that sequence to service_role. Skip the grant when pg_get_serial_sequence returns null. service_role bypasses row level security and stays on the server.
3. Enable row level security.
4. Add a separate policy per command. Insert and update need with check, usually auth.uid() = user_id.

Do not GRANT ALL to anon. Do not turn row level security off. Do not grant anon access just because the error hint mentions anon.

Show the SQL. Then say how to confirm the 42501 is gone and an anonymous request still cannot read another user's row.

Questions

Do I have to re-grant every table I already have?

No. Supabase's changelog says existing tables keep their current grants and stay reachable. The new rule applies to tables you create after the setting is on. For an existing project, that date is October 30, 2026, unless you already opted in.

Why do my policies exist and the API still says permission denied?

A policy answers "which rows?" A grant answers "may this role touch the table?" Postgres checks the grant first. Code 42501 with permission denied for table is the grant. An empty array does not prove a policy hid the rows. The table may simply be empty.

Does this affect storage or auth tables?

The changelog says no. The change is default privileges in the public schema. Tables in storage, auth, realtime, and custom schemas keep their current grants.

Sources

Related

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.