Supabase · Security ·

Supabase “Permission Denied for Table”: the Data API Grants Change Scheduled for 30 Oct

Supabase's changelog schedules a change for 30 October 2026 on every existing project: new tables in the public schema stop being reachable through the Data API until you grant access to them explicitly. If a migration, a CLI script or an AI coding agent creates a table after that date and doesn't include a grant, your app's first request to it fails with 42501: permission denied for table.

This guide pairs with Ship-Ready AI Rules, and if you want the next step, read Supabase Service Role Key Exposed? How to Check, Rotate and Stop It Happening Again.

This guide covers exactly what changes, who is affected, SQL to check where your project stands today, the fix to put in every migration, and how to opt in early or roll back. Every Supabase-specific claim comes from Supabase's changelog announcement and Supabase CLI pull requests #5524 and #6337 (both checked 14 October 2026).

What Supabase has scheduled for 30 October 2026

Until now, when the postgres role created a table in public, Postgres default privileges handed select, insert, update and delete on it to anon, authenticated and service_role automatically. That's why a new table "just worked" from supabase-js, and why Row Level Security was the only thing standing between it and the internet.

After the change, those default privileges are revoked for future objects in public. The rollout, per Supabase:

What Supabase has scheduled for 30 October 2026
Date Scheduled (per changelog)
28 April 2026 Announced; opt-out of "Automatically expose new tables and functions" available when creating a project
30 May 2026 New behaviour scheduled to become the default for new projects (gradual rollout)
30 October 2026 New behaviour scheduled to be enforced on all existing projects

Status check (14 Oct 2026): a Supabase CLI maintainer noted in August (PR #6337) that newly created hosted projects were still auto-granting. Run query 1 below to see what your own project does, and check the changelog before relying on these dates.

Supabase's stated reason is that tables are increasingly created by agents and CLI tools without a human reviewing them, so exposure should be a deliberate step rather than a side effect.

What stays the same:

  • Existing tables keep their current grants, so your running app stays reachable.
  • RLS behaviour doesn't change. You still enable RLS and write policies exactly as before.
  • Direct Postgres connections (psql, an ORM, a connection string) are not affected.
  • storage, auth, realtime and custom schemas keep their current grants and defaults; only public changes.
  • Supabase's changelog lists self-hosted as not affected.

Who needs to act

According to the changelog, you need to act if any of these is true:

  • Your app reads or writes public tables through the Data API: PostgREST, GraphQL, supabase-js or any client library, or direct HTTP to /rest/v1/ or /graphql/v1.
  • Your migrations or provisioning flow create tables in public without explicit GRANT statements.
  • An AI coding tool, CLI script or Management API call creates tables and expects them to be reachable straight away.

If you only ever talk to your database over a direct Postgres connection, you can stop reading.

What the failure looks like

A request to a table with no grant fails before RLS is evaluated, with Postgres error code 42501 and the message "permission denied for table …". Supabase says the Data API response includes a hint naming the missing grant, which is also what lets agents self-correct.

Two points catch people out:

  1. The secret key doesn't fix it. service_role also loses its default grant on new tables. Swapping in the admin client, a tempting "fix" we covered in what to do if your Supabase service role key is exposed, won't help here and adds risk.
  2. It isn't always an RLS problem, and the error code alone won't tell you. RLS violations also return 42501, so read the message: "permission denied for table X" means a missing grant, while "new row violates row-level security policy for table X" means RLS blocked an insert or update. A read that RLS filters out usually returns an empty result rather than an error. Check the message before rewriting policies.

Check your project: three SQL queries

Run these in the Supabase SQL Editor. They only read catalogue tables.

1. Is your project still auto-granting new tables?

This lists the future-table privileges the postgres role hands to the API roles in public:

select pg_get_userbyid(d.defaclrole) as owner, a.grantee::regrole as role,
       string_agg(a.privilege_type, ', ' order by a.privilege_type) as future_privileges
from pg_default_acl d
join pg_namespace n on n.oid = d.defaclnamespace
cross join lateral aclexplode(d.defaclacl) a
where n.nspname = 'public' and d.defaclobjtype = 'r'
  and a.privilege_type in ('SELECT','INSERT','UPDATE','DELETE')
  and a.grantee::regrole::text in ('anon','authenticated','service_role')
group by 1, 2 order by 1, 2;

Rows for anon, authenticated and service_role mean the old behaviour is still active. No rows means new tables already need explicit grants.

2. Which tables are reachable, and by whom?

select table_name, grantee, string_agg(privilege_type, ', ' order by privilege_type) as privileges
from information_schema.role_table_grants
where table_schema = 'public'
  and grantee in ('anon', 'authenticated', 'service_role')
  and privilege_type in ('SELECT', 'INSERT', 'UPDATE', 'DELETE')
group by table_name, grantee
order by table_name, grantee;

Use this as your baseline. It's also a good moment to spot tables that anon can write to and shouldn't.

3. Which tables have no API grants at all?

These are the tables that will return "permission denied for table" to your app:

select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p')
  and not exists (
    select 1 from information_schema.role_table_grants g
    where g.table_schema = 'public' and g.table_name = c.relname
      and g.grantee in ('anon', 'authenticated', 'service_role')
  )
order by 1;

An empty result today is expected on most existing projects. Run it again after your first post-30-October migration.

Supabase also says the Security Advisor flags affected tables with remediation SQL, and the Table Editor shows a Data API exposure badge. Its schedule lists emails to owners and admins of active projects on 23 September and 23 October 2026.

The fix: grant, enable RLS, add policies, in one migration

Supabase's guidance is to treat these three steps as a unit. Grant only what each role needs:

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 what each role needs
grant select, insert on public.notes to authenticated;
grant select, insert, update, delete on public.notes to service_role;
-- (no anon grant: signed-out users have no business here)

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

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

authenticated gets only select and insert because those are the only policies; if your app later updates or deletes notes, add the grant and a matching for update / for delete policy together. service_role keeps full access for server-side jobs. A grant without RLS exposes every row to that role, so never ship step 1 on its own. And only grant anon on tables that signed-out visitors genuinely need to read.

If you use the Supabase CLI locally, check [api].auto_expose_new_tables in config.toml. The CLI briefly made revoke-by-default the local default (PR #5524, June 2026), then switched back to auto-expose to match hosted projects (PR #6337, August 2026). To make local development behave like the new platform default, set it to false explicitly.

Tell your AI coding agent

Agents creating tables are the reason for this change, so put the rule where they'll read it (AGENTS.md, .cursor/rules, CLAUDE.md):

When creating a table in the public schema, the same migration must:
1. grant only the privileges each role needs (anon only if public read is intended),
2. enable row level security,
3. add policies for each operation the app uses.
On "permission denied for table" (42501), add the missing grant.
On "new row violates row-level security policy" (also 42501), fix the policy, not the grants.
Never switch to the service role / secret key to work around it.

Supabase also points to its agent skill, which includes the grants step.

Opt in early (and roll back if needed)

You don't have to wait for 30 October. Supabase documents running the same revokes the dashboard runs at project creation:

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;

This only affects future tables and sequences; existing ones keep their grants. Doing it now means you find missing grants on your schedule, not after the enforcement date.

If something breaks, Supabase's rollback restores the old defaults and grants in bulk:

alter default privileges for role postgres in schema public
  grant select, insert, update, delete on tables to anon, authenticated, service_role;
alter default privileges for role postgres in schema public
  grant usage, select on sequences to anon, authenticated, service_role;

grant select, insert, update, delete on all tables in schema public to anon, authenticated, service_role;
grant usage, select on all sequences in schema public to anon, authenticated, service_role;

The bulk grant also re-exposes tables you meant to keep private, so revoke those individually afterwards:

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

FAQ

Will my existing app break on 30 October? If the change goes ahead as scheduled, existing tables won't break: they keep their grants. Breakage comes from tables created after the change reaches your project without a grant.

I get "permission denied for table" on a new table. Is RLS wrong? Probably not. That message means a missing grant: add the grant the hint names, and keep RLS enabled. 42501 on its own isn't enough to tell, because RLS violations use the same code with a different message ("new row violates row-level security policy for table …"). That one means a policy problem, not a grant problem.

Can I just use the service role key? No. service_role needs a grant on new tables too, and using the secret key in app code bypasses RLS. If it has leaked anywhere, read our service role key exposure guide.

Does this affect storage, auth or custom schemas? No. Only default privileges in public change.

Does it affect sequences and functions? The opt-in SQL in the changelog revokes defaults on tables and sequences. The CLI change also covers functions in public. Sequence grants only matter for serial/bigserial columns; generated … as identity columns don't need them. If a serial insert ("permission denied for sequence …") or an RPC call fails after a migration, check those grants too.

Can I make one table private on purpose? Yes. That's the point of the change. Leave a new table without API grants, or revoke them on an existing one:

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

I'm self-hosting. Does this apply? Supabase's changelog lists self-hosted as not affected. Check your own project with query 1 below.

Disclosure: Launch Audit and AI Rules are our own paid products. Launch Audit runs 28 checks across a Next.js, Supabase and Stripe codebase (RLS, keys, auth, Stripe webhooks). It doesn't check Data API grants yet, so use the SQL above for that. Our free, MIT-licensed checker flags public tables with no explicit GRANT (check R5): nextjs-supabase-stripe-security-checker. AI Rules gives your coding agent rules for migrations, RLS and keys. They reduce risk; they don't replace code review.