
Supabase Permission Denied for Table: The Oct 30 Change
If a new Supabase table returns "permission denied for table" (code 42501), the table has no GRANT for the role calling it, and the fix is the exact GRANT printed in the error's hint. On October 30, 2026 that becomes normal on every existing project: Supabase stops auto-granting new tables in public to anon, authenticated and service_role. Your current tables keep working. Anything that creates a table and expects the API to see it breaks. I reproduced it on a live project this morning, and found two holes the change doesn't close.
What Changes on October 30, 2026?
Until now, every Supabase project shipped with Postgres default privileges that gave select, insert, update, delete on any new public table to the three API roles. Create a table in the SQL editor and supabase-js could hit it a second later. RLS was the only thing between that table and the internet.
Supabase announced the switch in its April 28 breaking-change notice, and rolled it out in three steps:
- April 28, 2026: opt-in checkbox at project creation.
- May 30, 2026: default for new projects, rolled out gradually.
- October 30, 2026: enforced on all existing projects. New tables need an explicit
GRANTbefore the REST or GraphQL API can see them.
It only touches the Data API. If your app talks to Postgres over a connection string (Prisma, Drizzle, psql, an n8n Postgres node), nothing changes. And it only touches the public schema, not auth, storage or custom schemas.
Takeaway: nothing you have today breaks on Friday. The next table you create does.
How I Tested It Without Breaking a Live Project
I didn't want to flip the default on one of my real projects and wait for something to break. So I used a small trick: one transaction that revokes the default privileges, creates the test objects, then restores the defaults and commits. Other sessions never see the revoked default, but the objects created inside it get exactly the grants a post-October-30 table would get.
On October 27, 2026, on one of my own Supabase projects (Postgres 17.6), I created two tables (one with an identity column, one with bigserial), a view, and two functions, one SECURITY DEFINER and one SECURITY INVOKER. Then I hit the REST API with the publishable key (anon) and the secret key (service_role), added grants one at a time, and dropped everything. I compared pg_default_acl before and after to confirm the project ended exactly where it started.
| Request | Role | Result |
|---|---|---|
| GET ungranted table | anon | 401, 42501, hint with GRANT |
| GET ungranted table | service_role | 403, 42501, hint with GRANT |
| POST insert, ungranted table | anon | 401, 42501, GRANT INSERT hint |
| GET view over that table | anon | 401, 42501, hint: null |
| GET table that doesn't exist | anon | 404, PGRST205 |
| rpc() SECURITY INVOKER function | anon | 401, 42501 |
| rpc() SECURITY DEFINER function | anon | 200, returned the row count |
| GET right after GRANT | service_role | 200 in 0.2 s, no reload |
| GET with grant, RLS on, no policy | anon | 200, empty array |
| GET granted view, RLS on, no policy | anon | 200, every row |
What Does the 42501 Error Look Like?
Here are the actual response bodies:
GET /rest/v1/zz_gt_ident (publishable key, role anon)
HTTP 401
{
"code": "42501",
"details": null,
"hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.zz_gt_ident TO anon;",
"message": "permission denied for table zz_gt_ident"
}
GET /rest/v1/zz_gt_ident (secret key, role service_role)
HTTP 403
{ "code": "42501", "hint": "... GRANT SELECT ON public.zz_gt_ident TO service_role;", ... }Three details matter when you're debugging this at 11 p.m.:
- 401 vs 403 tells you the role. Same 42501, but anon gets 401 and service_role gets 403. If your server code gets a 403, the secret key is fine. The table is missing a grant.
- Views have no hint.A view without a grant says "permission denied for view" with
hint: null, so you have to write the GRANT yourself. - 404 PGRST205 is a different bug."Could not find the table in the schema cache" means PostgREST doesn't know the table exists (wrong name, wrong schema, or stale cache). An ungranted table is still in the cache and returns 42501.
One more trap: service_rolebypasses RLS, not grants. It's easy to assume the secret key can read anything. After Friday it can't read a table nobody granted it. And if your secret-key calls come back 401 with "Forbidden use of secret API key in browser" instead, that's Supabase blocking browser user agents, which I covered in the Supabase API keys migration post. My first test run hit exactly that because my script sent a Mozilla/5.0 UA.
How Do I Fix Permission Denied for Table in Supabase?
Run the GRANT from the hint. That's it for the emergency. In my test the table answered 200 on the very first request, 0.2 seconds after the GRANT, with no NOTIFY pgrst and no schema reload. Postgres checks privileges at query time.
The real fix is in your migrations. Supabase's own advice is to treat grant, RLS and policy as one unit, and I agree. But don't copy the blanket "grant everything to everyone" you'll see in a lot of answers. The whole point of the change is that each role gets only what it needs:
create table public.leads (
id bigint generated always as identity primary key,
owner_id uuid not null default auth.uid(),
email text not null,
created_at timestamptz not null default now()
);
-- 1. Grants: only what each role needs
grant select, insert on public.leads to authenticated;
grant select, insert, update, delete on public.leads to service_role;
-- anon gets nothing: this table is never public
-- 2. RLS
alter table public.leads enable row level security;
-- 3. Policies
create policy "owners read their leads" on public.leads
for select to authenticated using (owner_id = auth.uid());
create policy "owners insert their leads" on public.leads
for insert to authenticated with check (owner_id = auth.uid());About sequences: the changelog's opt-in SQL revokes usage, select on sequences, and I expected my bigserialinsert to fail without a sequence grant. It didn't. The revoke leaves UPDATE on new sequences, and Postgres accepts USAGE or UPDATE for nextval(). Identity columns don't need a sequence grant at all. I'd still use generated always as identity for new tables and stop thinking about it.
Same story on tables: the revoke only removes arwd. New tables still carry TRUNCATE, REFERENCES, TRIGGER and MAINTAIN for the API roles. PostgREST can't issue any of those, so it's not an API hole, but if you want a clean slate on a sensitive table, revoke all ... from anon, authenticated, service_role and grant back what you need.
What the Grant Change Doesn't Protect
This is the part I'd want someone to tell me. Supabase frames the change as "new tables aren't exposed until you say so". That's true for tables. It isn't true for the other two ways the API reaches data.
Leak 1: SECURITY DEFINER functions. The opt-in SQL revokes tables and sequences, not functions, and Postgres grants EXECUTE on every new function to PUBLIC anyway. My definer function counted rows in a table anon had zero grants on, and POST /rest/v1/rpc/... with the publishable key returned 200 and the count. The invoker version correctly returned 42501.
Leak 2: views. I granted anon select on the table, turned on RLS and wrote no policy. Direct reads returned [], as they should. Then I granted anon selecton a plain view over that table, and it returned every row, including the one labeled "row that anon should never see". A view owned by postgresruns with the owner's rights, and postgres bypasses RLS.
Both have one-line fixes, and I tested both:
-- Views: run with the caller's privileges and RLS, not the owner's
create view public.lead_summary with (security_invoker = true) as
select id, created_at from public.leads;
-- SECURITY DEFINER functions: Postgres grants EXECUTE to PUBLIC by default
revoke execute on function public.move_credits(uuid, int) from public, anon, authenticated;
grant execute on function public.move_credits(uuid, int) to service_role;With security_invoker = true the same view returned []to anon. With EXECUTE revoked, the definer function returned 401 "permission denied for function" to anon and still worked for service_role. The Postgres CREATE VIEW docs cover security_invoker, and Supabase's Security Advisor has a lint for definer functions anon can execute. Run it.
Takeaway: grants close the front door. RPC and views are side doors, and the October 30 change leaves them exactly as they were.
Is My Project Already on the New Default?
Don't assume. I checked the default privileges on the ten active projects in my account. Seven still auto-grant everything to all three roles, including three created in September 2026, months after the new-project default supposedly kicked in. The other three have a tighter custom default. So the creation date tells you nothing. Query it:
-- 1. Is the new default active? Look at the 'r' (tables) row for postgres.
-- anon/authenticated with arwd... = old default, still auto-granting.
select pg_get_userbyid(d.defaclrole) as owner, d.defaclobjtype as type, d.defaclacl
from pg_default_acl d
join pg_namespace n on n.oid = d.defaclnamespace
where n.nspname = 'public'
order by 1, 2;
-- 2. Which tables and views can each API role actually read?
select c.relname,
has_table_privilege('anon', c.oid, 'select') as anon_select,
has_table_privilege('authenticated', c.oid, 'select') as auth_select,
has_table_privilege('service_role', c.oid, 'select') as service_select
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'v', 'm', 'p')
order by 1;
-- 3. SECURITY DEFINER functions anon can call through /rpc
select p.proname
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
and p.prosecdef
and has_function_privilege('anon', p.oid, 'execute');Query 1 tells you which behavior you have today. Query 2 is the one to run after Friday when something 42501s: any false in a column your app needs is your bug. Query 3 lists the side doors from the previous section.
Want to find out before Friday instead of during it? Run the changelog's two alter default privileges ... revoke statements on a staging project, run your tests against the REST API (not the SQL editor, which runs as postgres and sees everything), and see what falls over. The changelog also has the rollback SQL if you need it.
Where Tables Get Created Without Anyone Looking
Supabase says this change exists because agents and scripts create tables with no human reviewing the diff. That's also where you'll get bitten. Grep for create table in:
- Migration folders, especially old ones that relied on the defaults. Preview branches and fresh environments replay them from scratch, on the new default.
- Provisioning scripts that spin up a schema per client or per tenant.
- n8n workflows that run SQL through the Postgres node and then read through the Supabase node. The first uses a connection string, the second uses the API, so the table gets created and then 42501s. If you followed my Supabase + n8n lead database guide on a new project, add the grants to its
create tablestep. - AI coding agents. Claude Code, Cursor and the Supabase MCP server will happily create a table and wire the client to it. Put the grant rule in your project instructions or a skill. My notes on writing one SKILL.md that works across agents apply here.
The 42501 hint is precise enough that an agent can fix itself. Precise enough, too, that a sloppy one will grant anon full write access just to make the error go away. Review those diffs. Same principle as my MCP security checklist: the agent proposes, a human approves anything that widens access.
Supabase Grants FAQ
Why does Supabase say permission denied for table on a new table?
Because the table has no GRANT for the role making the request. Since May 30, 2026 new projects don't grant new public tables to anon, authenticated and service_role, and from October 30, 2026 existing projects behave the same way. Run the GRANT shown in the error's hint field.
Will my existing Supabase tables stop working on October 30?
No. Supabase only changes the default privileges for objects created after the switch. Tables that exist today keep their current grants. What breaks is any code that creates a new table and expects the Data API to see it immediately.
Does the service_role key bypass a missing grant?
No. service_role bypasses row level security, not table privileges. In my test the secret key got HTTP 403 with code 42501 on an ungranted table, and its hint asked for GRANT SELECT ... TO service_role.
Do I need to reload the PostgREST schema cache after a GRANT?
No. In my test the table answered 200 on the first request, 0.2 seconds after the GRANT. Privileges are checked by Postgres at query time. A 404 with code PGRST205 is a different problem: the table isn't in the schema cache at all.
Does the Supabase grant change protect RPC functions and views?
No. Postgres still grants EXECUTE on new functions to PUBLIC, so a SECURITY DEFINER function can return data from a table anon has no grant on. A view owned by postgres runs with the owner's rights and ignores the caller's RLS unless you create it with security_invoker = true.
Want a Second Pair of Eyes on Your Supabase Project?
I build Next.js and Supabase apps and the n8n and AI agent pipelines that write into them. I can audit your grants, RLS policies and definer functions, and fix your migrations before Friday instead of after.
Related Posts
Full-Stack
Supabase Legacy API Keys Deprecated: How to Migrate
Supabase retires the legacy anon and service_role keys by the end of 2026. Tested on a live project: Bearer-only calls fail for old and new keys alike, supabase-js works unchanged, verify_jwt accepts any valid project key, and a bare Mozilla/5.0 user agent blocks the secret key.
Full-Stack
Docker on macOS: Why Bind Mounts Corrupt Your Databases
Docker Desktop runs your containers in a Linux VM, and every bind-mounted host path crosses that boundary over a file-sharing protocol that does not honor fsync, ordering or locking. That is how a Mongo data file on my external SSD ended up internally inconsistent. Why named volumes are the only safe home for a storage engine, how to migrate without data loss, and how to put Docker itself on an external drive the right way.
Full-Stack
Vercel Node.js 20.x Discontinued Error: How to Fix It
Since October 1, 2026 Vercel refuses new builds on Node.js 20. Tested on a throwaway project: engines 20.x, ^20.11.0 and >=18 <21 fail in under a second, >=20 quietly runs Node 24, engines overrides Project Settings, .nvmrc is ignored, and the API still accepts 20.x.