buildwiththis home

Supabase error 42501 “permission denied for table”: why new tables break and the safe fix

Short answer: Supabase no longer gives new public tables automatic API access. That’s been the default for new projects since May 30, 2026, and applies to existing projects from October 30, 2026. Until you GRANT a role access, supabase-js returns error 42501. Grant only the roles that need it, per table, and turn on RLS with policies in the same migration.

Tested and sources checked Oct 23, 2026Fix guide, not a tool verdictRe-test due by Nov 30, 2026

What the error looks like

This is what our test table returned to a logged-out request. You’ll see it in the error object from supabase-js, in your browser’s network tab, or in the console.

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

A logged-out request came back as HTTP 401 and a signed-in one as 403, both with code 42501. Supabase’s 42501 troubleshooting page lists other causes of the same code, such as forbidden schemas, custom schemas you haven’t granted, column restrictions, and RLS blocking a write.

What changed on October 30, 2026

The timeline

DateWhat Supabase says happens
Apr 28, 2026You can opt in when you create a new project.
May 18, 2026pg_graphql no longer enabled by default.
May 30, 2026The new behaviour starts becoming the default for new projects (Supabase: “a gradual rollout over a few weeks”).
Oct 30, 2026Applied to all existing projects.

Source: Supabase changelog, “Breaking Change: Tables not exposed to Data and GraphQL API automatically” (published Apr 28, 2026, checked Oct 23, 2026).

What’s affected and what isn’t

  • Affected: new tables in the public schema, reached through the Data API: supabase-js and the other client libraries, PostgREST, GraphQL, or HTTP to /rest/v1/ and /graphql/v1.
  • Not affected, per Supabase: existing tables (they keep their grants), the storage, auth and realtime schemas, custom schemas, and apps that only use a direct Postgres connection (psql, an ORM, a server connection string).
  • How the table was made doesn’t matter: Supabase says the rule covers the SQL editor, migrations, the Management API, MCP, the CLI and AI coding tools.

Why Supabase did it

Supabase’s reason: agents, CLI scripts and AI platforms now create tables with no human reviewing the change, and the old default exposed every new public table on creation, “including tables a developer forgot to protect.” If you build with an AI tool, that’s good news. A table it forgets to secure is now closed instead of open.

The fix: grant, RLS and policies in one migration

Supabase says to treat these three steps as a unit. Replace your_table and user_id with your table and its owner column, and drop any role your app doesn’t use.

-- 1. Grant only what each role needs
grant select, insert, update, delete on table public.your_table to authenticated;
grant select, insert, update, delete on table public.your_table to service_role;
-- grant select on table public.your_table to anon;  -- ONLY if logged-out visitors must read this table

-- 2. Turn on RLS
alter table public.your_table enable row level security;

-- 3. Add a policy per operation (example: owner reads own rows)
create policy "users can read their own rows"
  on public.your_table
  for select
  to authenticated
  using ((select auth.uid()) = user_id);

From Supabase’s changelog and Securing your API. Tested by BuildWithThis on Oct 23, 2026. We changed one thing: Supabase’s example also grants select to anon. We commented that line out, because most app tables are for signed-in users and Supabase’s own advice is to grant each role the minimum. The wrapped (select auth.uid()) form is from Supabase’s RLS docs.

What we saw: with the anon line left out, each signed-in test user read only their own row, and logged-out requests still got 42501. Signing in doesn’t need an anon grant: once a user is signed in, requests run as authenticated. For writes, add an insert or update policy with a with check clause. Without one, our insert was refused by RLS.

When you do need the anon grant

Only when logged-out visitors must read the table, like public posts or a pricing list. In our test, an RLS policy for anon wasn’t enough: logged-out reads got 42501 until we ran this. Keep RLS on with a policy that limits the rows.

grant select on table public.your_table to anon;

Tested by BuildWithThis on Oct 23, 2026. With only select granted, logged-out inserts were still refused.

Don’t copy the hint blindly

The hint names whichever role made the failing request. If you were logged out when you tested, it says anon, even if only signed-in users should see that data.

Don’t use the bulk grant

grant … on all tables in schema public appears in Supabase’s FAQ only as the rollback for projects that opted in early. It reopens every table at once, including ones you meant to keep private. Grant per table instead.

If you build with an AI agent

Supabase recommends updating your tool’s system prompt or using its agent skill. A rule you can paste: “Every migration that creates a table in public must, in the same migration, grant only the roles that need it, enable row level security, and add a policy for each operation. Never grant anon unless I say logged-out users should read it.” Then run the free vibe-coding security checklist.

Check your own project

Run these in the SQL Editor (Block E runs against your logs, see below).

Which roles can reach a table

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

From Supabase’s 42501 troubleshooting page, with a table_schema filter we added. Tested Oct 23, 2026. You need SELECT for reads and INSERT, UPDATE or DELETE for writes. In our test, roles we hadn’t granted still listed TRUNCATE, REFERENCES and TRIGGER; those didn’t give API access.

Tables an API role can read with RLS off

select c.relname as table_name,
       has_table_privilege('anon', c.oid, 'select') as anon_can_read,
       has_table_privilege('authenticated', c.oid, 'select') as authenticated_can_read
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
  and not c.relrowsecurity
  and (has_table_privilege('anon', c.oid, 'select')
       or has_table_privilege('authenticated', c.oid, 'select'));

BuildWithThis query, built from Postgres catalog functions, not a Supabase snippet. Tested Oct 23, 2026: it returned exactly the one table we’d left with RLS off. Any row it returns is a table anyone with your public key may be able to read. Turn RLS on, or revoke access (below). Supabase’s linter calls this “RLS Disabled in Public”.

Security Advisor says RLS Disabled in Public? Here’s what it means and the fix we tested.

What new tables will get by default

select
  case d.defaclobjtype when 'r' then 'tables' when 'S' then 'sequences' when 'f' then 'functions' end as object_type,
  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 a.grantee in ('anon'::regrole, 'authenticated'::regrole, 'service_role'::regrole)
group by 1, 2
order by 1, 2;

From Securing your API. Tested Oct 23, 2026. How to read it: if the tables rows include SELECT, INSERT, UPDATE and DELETE, new tables are still exposed the old way. After the opt-in SQL below, our project still showed rows (tables: MAINTAIN, REFERENCES, TRIGGER, TRUNCATE; sequences: UPDATE; functions: EXECUTE), but new tables were closed to the API. So don’t wait for “no rows”: look for SELECT.

Find recent 42501s in your logs

select timestamp, event_message,
  log_attributes['parsed.user_name'] as user_name,
  log_attributes['parsed.query'] as query,
  log_attributes['parsed.hint'] as hint
from logs
where source = 'postgres_logs'
  and log_attributes['parsed.sql_state_code'] = '42501'
order by timestamp desc
limit 100;

From Supabase’s 42501 troubleshooting page. This queries the logs source, not your database, so it won’t run as a normal SQL Editor query. We ran it on Oct 23, 2026 through Supabase’s logs query API and got our test’s 42501s back, including the RLS insert refusal. API requests show up with user authenticator, not anon. We didn’t run it in the dashboard’s log explorer.

Lock down an existing table you don’t want on the API

revoke all on table public.your_table from anon, authenticated, service_role;

From Supabase’s changelog. Tested Oct 23, 2026: afterwards, logged-out and signed-in requests both got 42501. It revokes service_role too, so drop that role from the line if your server code reads the table through the API.

In the dashboard

Supabase points to the Data API exposure badge in the Table Editor and to Security Advisor, which it says will flag affected tables until October 30. In our test, Security Advisor’s checks (read through Supabase’s MCP server) flagged our RLS-off table as “RLS Disabled in Public” but showed nothing for the new table that had no grants. We didn’t check the dashboard badge. Security Advisor was our pick of 6 scanners in our security test.

Want the new behaviour before October 30?

Supabase’s opt-in SQL for existing projects changes what future tables get. Existing tables 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;

From Supabase’s changelog (it says “these four statements” but shows two; this is what it shows). Tested Oct 23, 2026: tables made before it kept working, and tables made after it returned 42501 until granted.

Supabase’s FAQ also has a rollback for early opt-ins. We didn’t test its bulk-grant step, and Supabase doesn’t say whether it works after October 30.

What we tested

On Oct 23, 2026 at about 2 PM ET, on our own Supabase test project (Free plan, Postgres 17, created Oct 15, 2026), AI agents ran the test and fact-checked this page against the test log and Supabase’s docs. Before the test, the project still had the old defaults: new tables were exposed to all three API roles. We made two tables, ran Supabase’s opt-in SQL, made new tables, then sent requests to the REST API (what supabase-js calls) with the publishable key, both logged out and as two signed-up test users. Afterwards we deleted the tables and test users and put the project’s defaults back.

RequestWhat came back
New table, logged out (publishable key)401, code 42501, hint “GRANT SELECT ON public.… TO anon”
New table, signed-in user403, code 42501, hint “GRANT SELECT ON public.… TO authenticated”
New table, logged-out insert401, code 42501, hint “GRANT INSERT … TO anon”
Tables made before the switch (RLS on, and RLS off)200, rows returned, logged out and signed in
After the fix: signed-in user A, then user B200, each saw only their own row
After the fix: user A asking for user B’s rows200, no rows
After the fix: logged out (no anon grant)401, code 42501
After the fix: user A inserts (no insert policy)403, code 42501, “new row violates row-level security policy”, no hint
Public-read table, anon policy but no anon grant401, code 42501
Same table after granting select to anon200 for reads; inserts still 401, code 42501

Not tested: the October 30 rollout itself (we switched our project over early with Supabase’s SQL, so we tested the opt-in path, not the enforcement), tables made in the dashboard’s Table Editor, the dashboard’s exposure badge, functions and views, GraphQL, direct Postgres connections, and whether an AI tool adds grants by itself.

What we couldn’t confirm

  • The exact time on October 30. Supabase gives the date only, with no hour or time zone, and doesn’t say whether it reaches every project at once. The May 30 rollout was “gradual over a few weeks”.
  • Existing tables after the real rollout. The changelog says existing tables keep their grants, but its rollout table says “tables without explicit grants stop being reachable”. Our test matches the first reading: tables created under the old defaults already have grants, and they kept working after the opt-in SQL. We’ll re-check on a project Supabase switches over on October 30.
  • Lovable Cloud. Nothing official from Supabase or Lovable says whether Lovable Cloud projects get this change.
  • Why our project still had the old defaults. It was created on Oct 15, 2026, after the May 30 date, and new tables were still exposed. Supabase doesn’t say which project types the May 30 default skips.
  • Functions. In one place, Supabase calls the setting “Automatically expose new tables and functions”, but the opt-in SQL covers only tables and sequences. We don’t know whether new functions throw 42501.
  • Switching back after October 30, and whether turning the dashboard setting back on restores the old defaults. Neither is documented in what we read.

Questions people ask

Why does my new Supabase table say “permission denied for table” (42501)?

Because nobody granted your app’s role access to it. Supabase says that once the change reaches your project, new tables in the public schema need an explicit GRANT before the Data API (supabase-js, /rest/v1/, GraphQL) can see them. In our test, a table created with SQL after the change returned 42501 to both a logged-out request and a signed-in user.

My old tables work but the new one doesn’t. Why?

Supabase says existing tables keep their current grants and stay reachable; only tables created after the change start with no API access. We saw exactly that: after switching our test project to the new behaviour, tables made before the switch still answered 200, and a table made after it returned 42501.

Does this affect my existing tables after October 30?

Supabase says no: “Existing tables are not affected in your project, they keep their current grants and stay reachable.” We confirmed it on a project we switched over early with Supabase’s opt-in SQL. We haven’t been able to test the October 30 rollout itself, because it hasn’t happened yet.

I use Lovable, Bolt or Cursor and the AI created the table. Is that why?

Probably. Supabase says the rule applies however the table is created: SQL editor, migrations, Management API, MCP, CLI or an AI coding tool. Supabase recommends updating your AI tool’s system prompt or using its agent skill, which includes the grants step.

Should I just run the GRANT from the error hint?

Only if that role should have that access. The hint names the role that made the failing request: a logged-out request says anon, a signed-in one says authenticated. If your app only shows this data to signed-in users, granting anon opens the table to everyone with your public key, limited only by your RLS policies.

Is it safe to grant anon?

Only for data logged-out visitors are meant to read, with RLS on and a policy that limits the rows. In our test, an RLS policy for anon wasn’t enough on its own: logged-out reads still got 42501 until we granted select to anon. Granting select alone still blocked logged-out inserts.

Does granting access turn off Row Level Security?

No. Grants decide whether a role can reach a table at all; RLS decides which rows. After our grant, a signed-in user still saw only their own row, and an insert with no insert policy was refused by RLS.

I got 42501 “new row violates row-level security policy”. Is that the same thing?

No. Same code, different cause: the role can reach the table, but no RLS policy allows that write. In our test it came back as 403 with no hint. Add an insert or update policy with a with check clause; a GRANT won’t fix it.

Does this affect storage buckets, auth or custom schemas?

Supabase says no: tables in storage, auth, realtime and custom schemas keep their current grants and defaults. We didn’t test those.

I use Prisma, Drizzle or a server connection string. Am I affected?

Supabase says apps that only use a direct Postgres connection (psql, an ORM, an app server with a connection string) aren’t affected. Only the Data API is. We didn’t test a direct connection.

How do I see which of my tables are exposed?

Run the two queries under “Check your own project” below: one lists which roles can reach a table, the other lists tables with RLS off that an API role can read. Supabase also points to the Data API exposure badge in the Table Editor and to Security Advisor.

Can I switch back to the old behaviour?

Supabase’s changelog gives a rollback for projects that opted in early. It doesn’t say whether the same rollback works after October 30, so we can’t promise it. Granting per table is the safer fix either way.

Sources

Related: which Supabase key goes in your front end · how Supabase compared to Neon and D1 · Vibe coding hub

General guidance from one small test project, not a security audit. The table names in the error example are from our test project.