PreflightHow it worksPricingSampleGuides

Supabase 42501 “permission denied for table” after 30 Oct

On 30 Oct 2026 Supabase stops exposing new public tables to the Data API by default. A new table your AI tool creates returns error 42501 until you grant access. Here is the fix that keeps your data private, and the one that quietly opens it.

The free scan is read-only. Nothing is written to your app. See a sample report →

Free scan: GET and HEAD requests only, after you verify you own the domain. The two-account cross-user test is the $29 Proof Report. Updated 25 Oct 2026

In short

  • ✓From 30 Oct 2026, on every existing Supabase project, a new table in public is not reachable through supabase-js, /rest/v1/ or /graphql/v1 until you run a GRANT (changelog, 28 Apr 2026; Securing your API). New projects picked this up from 30 May 2026, rolled out over a few weeks.
  • ✓Tables you already have keep their grants. Nothing that works today breaks on the day; the next new table does. Custom schemas, auth, storage, realtime and direct Postgres connections are out of scope.
  • ✓The safe fix is three steps together: grant only what each role needs, enable RLS, add owner-scoped policies.
  • ✓The risky fix is the bulk grant (grant … on all tables in schema public to anon, authenticated). It re-opens every table, including ones you meant to keep private.
  • ✓Grants decide whether a role can reach a table at all. RLS still decides which rows. You need both.

01What Supabase changed on 30 Oct 2026

Until 30 Oct 2026, an existing Supabase project granted select, insert, update and delete on every new public table to anon, authenticated and service_role. Create a table and it was on the Data API at once, before anyone had written a policy. Securing your API (checked 25 Oct 2026) still describes that as the default on older projects; the platform default is moving to opt-in.

Supabase announced the change on 28 Apr 2026 (changelog: “Tables not exposed to Data and GraphQL API automatically”). Dates from that post:

DateWhat happens
28 Apr 2026Opt-in at project creation: uncheck “Automatically expose new tables”.
30 May 2026Becomes the default for all new projects, rolled out over a few weeks.
30 Oct 2026Enforced on all existing projects. A new public table without an explicit GRANT is invisible to PostgREST, GraphQL and supabase-js.

The changelog’s reason: agents, CLI scripts and AI platforms now create tables without a human reviewing the diff, and the old default exposed tables “a developer forgot to protect”. Grants are a separate layer from RLS: they decide whether a role can reach the table at all; RLS then decides which rows. A missing grant returns 42501, not an empty array.

02Who is affected (and who is not)

You are in scope if any of these is true (quoted from the changelog):

  • The app reads or writes public tables through the Data API: PostgREST, GraphQL, supabase-js, the other client libraries, or HTTP to /rest/v1/ or /graphql/v1.
  • Migrations or a provisioning flow create tables in public without a GRANT.
  • An AI coding tool, CLI script or Management API call creates a table and expects it on the API the moment it exists.

That is every typical Lovable, Bolt.new, Cursor or v0 app on Supabase: the browser talks to /rest/v1/ with the public key. A new “orders” table the agent added last night is the one that 42501s. Tables that already exist keep working, which is why this is easy to miss — the dashboard still looks fine, the new screen does not load.

You can ignore the change if you only talk to Postgres over a direct connection (psql, an ORM, a server with a connection string). Tables in auth, storage, realtime and any custom schema keep their current grants and their current defaults. Functions are not covered: new functions stay executable by every role until you revoke EXECUTE (see below).

You do not have to wait for 30 Oct. The changelog’s “Opting in on existing projects” block is two alter default privileges … revoke statements in the SQL Editor. Existing objects keep their grants. From then on, new public tables need an explicit GRANT. The dashboard also has a Data API setting, “Automatically expose new tables”, and a per-table exposure badge in the Table Editor. The Security Advisor is supposed to flag tables worth reviewing during the run-up; after 30 Oct the failure mode is 42501 on the next new table, even if Advisor is clean today.

03What it looks like in a Lovable or Bolt.new app

Lovable and Bolt.new apps call Supabase from the browser with supabase-js, so they are in scope. After 30 Oct, when you ask the agent for a feature that needs a new table and its migration has no GRANT, you will see one of these:

  • A list or dashboard that loads empty, or a toast like “Failed to load”.
  • In DevTools → Network, a /rest/v1/your_table request answering 401 or 403 with this body:
{
  "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 literal: it names one role and one privilege. Pasting it as-is is fine for a table that really is public. For anything that belongs to a user, granting anon is the wrong move, and the AI tool reading the error doesn’t know which kind of table it is.

Old tables keep working. That makes this easy to miss: the bug shows up only in the feature you are building that week. If the agent “fixes” 42501 with a bulk GRANT, yesterday’s private tables can open too — that is the next section.

04The safe fix: grant, RLS and policies together

Supabase’s own guidance is to treat these three steps as a unit. For a table that holds one user’s rows (notes, orders, messages, profiles):

-- 1. Grant only what each role needs. Signed-out visitors get nothing.
grant select, insert, update, delete on public.notes to authenticated;
grant select, insert, update, delete on public.notes to service_role;

-- 2. Turn on RLS (a grant without RLS means every signed-in user sees every row)
alter table public.notes enable row level security;

-- 3. Owner-only policies
create policy "own notes: read"   on public.notes for select to authenticated using (auth.uid() = user_id);
create policy "own notes: insert" on public.notes for insert to authenticated with check (auth.uid() = user_id);
create policy "own notes: update" on public.notes for update to authenticated using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "own notes: delete" on public.notes for delete to authenticated using (auth.uid() = user_id);

Grant select to anon only for data every visitor should see, such as published blog posts or a public price list, and still keep RLS on with a policy that limits it to those rows.

A prompt you can paste into Lovable, Cursor or Bolt.new:

The new table public.notes returns error 42501. Write one migration that:
1) grants select, insert, update, delete on public.notes to authenticated and service_role only (no grant to anon),
2) enables row level security on public.notes,
3) adds select, insert, update and delete policies for authenticated that require auth.uid() = user_id (with check on insert and update).
Do not grant anything on other tables. Do not use "grant ... on all tables". Don't change anything else.

05The quick fix that leaks your data

Searching the error or asking an AI tool often turns up one of these. Each makes the error go away. Each can also open tables you never meant to publish:

“Fix”What it really does
grant all on all tables in schema public to anon, authenticated;Gives the public key every privilege on every table, including truncate. Any table with RLS off is now readable and writable by anyone.
alter default privileges … grant select, insert, update, delete on tables to anon, authenticated…Restores the old default. Every future table is public again the moment it is created, before it has a policy.
grant select on public.notes to anon; (pasted from the hint)Right for public data. For user data it means the only thing between a stranger and the rows is your RLS policy.
alter table public.notes disable row level security;Doesn’t fix 42501 (that is a grant error) but often gets tried next. With grants in place, every row is open.

If you already ran a bulk grant, take back what each table doesn’t need, starting with anon:

revoke all on table public.notes from anon;
-- repeat per table that signed-out visitors should not reach

06How to check your project in five steps

1. List what the public roles can reach. In the SQL editor:

select g.table_name, g.grantee,
       string_agg(g.privilege_type, ', ' order by g.privilege_type) as privileges,
       t.rowsecurity as rls_on
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 in ('anon', 'authenticated')
group by g.table_name, g.grantee, t.rowsecurity
order by rls_on, g.table_name, g.grantee;

Any row with rls_on = false is reachable by that role with no row filter. Any anon row is something a stranger can try with your public key.

2. Find new tables with no grants yet (the ones that will throw 42501):

select t.tablename
from pg_tables t
where t.schemaname = 'public'
  and not exists (select 1 from information_schema.role_table_grants g
                  where g.table_schema = 'public' and g.table_name = t.tablename
                    and g.grantee in ('anon', 'authenticated'));

3. Try a table signed out. With only your public key:

curl -s "https://YOUR_REF.supabase.co/rest/v1/notes?select=*&limit=1" \
  -H "apikey: YOUR_PUBLIC_KEY" -H "Prefer: count=exact" -i | grep -iE "^HTTP|content-range|42501"
# 42501 or "*/0": a stranger gets nothing. "0-0/214": 214 rows are public.

4. See what new objects will get by default (the query from Securing your API). No rows means new tables grant nothing to Data API roles until you GRANT:

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;

5. Test as two users. A grant to authenticated plus a loose policy means any signed-in user can read every other user’s rows. Sign in as two test accounts and request user B’s rows with user A’s token: How to test RLS with two users. Supabase’s Table Editor can impersonate a user, and the RLS Tester preview runs a SELECT as any role. The Security Advisor will not catch a SELECT … USING (true) leftover after you GRANT.

07Database functions are not covered by the change

The revoke statements in the changelog cover tables and sequences. Supabase’s Securing your API page notes that new functions are still callable by every role, because Postgres grants EXECUTE to PUBLIC by default. A function your AI tool adds, say spend_credits, can be called at /rest/v1/rpc/spend_credits by anyone with the public key unless you revoke it:

revoke execute on function public.spend_credits(int, text) from public, anon;
grant execute on function public.spend_credits(int, text) to authenticated;

08What Preflight checks

The free scan finds the Supabase URL and public key in your deployed JavaScript and asks each table your bundle names what a signed-out visitor gets back. A table that answers with rows is reported with the row count and the request we sent, so you can run it again yourself. It does not sign in and writes nothing.

The $29 Proof Report goes further. It signs in as two test accounts you create, tries to read and change one account’s rows from the other, tries privilege columns such as role or credits, and calls database functions signed out. It creates a few clearly marked test rows and deletes them at the end; anything it cannot delete gets a one-line DELETE in the report. Each finding comes with the request you can re-run, the fix SQL and a prompt for your AI tool.

?Questions people ask

  • Why aren’t my new Supabase tables exposed to the Data API?

    From 30 Oct 2026, new public tables need an explicit GRANT before PostgREST, GraphQL or supabase-js can see them. Existing tables keep their grants. Check pg_default_acl and role_table_grants (steps above). Missing grant → 42501, not an empty list.
  • Will my Lovable or Bolt.new app break on 30 Oct 2026?

    Existing tables keep their grants, so what works today keeps working. The first new table created after the change returns 42501 until it gets a GRANT. Direct Postgres connections are unaffected.
  • Is it safe to paste the GRANT from the 42501 hint?

    Only for data every visitor should see. For user data, grant to authenticated (not anon), turn on RLS and add owner-only policies in the same migration.
  • Does this replace row level security?

    No. Grants decide whether a role can reach a table at all; RLS decides which rows. A grant to authenticated with RLS off lets every signed-in user read every row.
  • Can I switch to the new behaviour before 30 Oct?

    Yes. The changelog lists the alter default privileges … revoke statements to run in the SQL editor. Existing tables keep their grants.
  • Does it affect storage buckets or auth?

    No. Tables in the storage, auth and realtime schemas keep their current grants, and so do custom schemas. Storage access is still governed by policies on storage.objects.

What an outside scan can't tell you

Can one user read another user's data?

Preflight signs in as two test accounts you create and tries to read one account's rows and files from the other. That cross-user test is the $29 Proof Report for Supabase apps. Start with the free scan: it checks your live app from outside with GET and HEAD requests only.