On October 30, 2026, Supabase stops granting anon, authenticated and service_role access to new tables in public on existing projects (changelog). A table created without an explicit GRANT returns 42501 permission denied through the Data API (PostgREST, GraphQL, supabase-js), even for service_role.
Most write-ups stop at "add GRANTs to your migrations". This post is about what happens next:
- A common quick fix for
42501, one your search results show and an AI coding tool may suggest, re-opens the hole this change closes. - Tables you already have are not fixed by this change. In my own production database, one of them was readable by anyone holding the anon key: all 4,389 rows.
Timeline
| Date | What happens |
|---|---|
| 2026-04-28 | Opt-in toggle when creating a project |
| 2026-05-30 | Default for new projects |
| 2026-10-30 | Applied to all existing projects |
Supabase says existing tables "keep their current grants and stay reachable". Your production app won't break on Oct 30. What breaks is any table you create after that date, and any environment you rebuild from migrations (a new project, a preview branch, supabase db reset).
Why anon is the role to worry about
In Supabase, your frontend calls the Data API directly, using the anon key. The Data API looks at the key and token on each request to decide which Postgres role runs the SQL:
-
Not signed in: there's no user token, so the request runs as the
anonrole. (The legacy anon key is a JWT whose payload saysrole: anon. The newer publishable key,sb_publishable_…, maps toanonas well.) In other words,anonis the role for unauthenticated visitors. -
Signed in: supabase-js sends the user's JWT, which says
role: authenticated, so the request runs asauthenticated. -
Server-side code: uses the service_role key and runs as
service_role.
One thing to watch: users created with Supabase's anonymous sign-in (signInAnonymously()) are authenticated, not anon. Their JWT carries an is_anonymous claim (docs).
The anon key ships in your frontend bundle. Anyone can pull it out with the browser's dev tools. So whatever anon is allowed to do, anyone on the internet is allowed to do. If anyone can sign up, authenticated is nearly the same. service_role lives only on your server and already bypasses RLS.
That's why "dangerous" in this post always means: what can anon (or authenticated) do?
GRANT and RLS are two separate layers
- GRANT decides whether a role can touch the table at all.
- RLS (Row Level Security) decides which rows it sees once it can.
| GRANT to anon | RLS | Policies | What the anon key can see |
|---|---|---|---|
| yes | off | — | 🔴 every row, and it can write and delete |
| yes | on | none | nothing |
| yes | on |
to authenticated only |
nothing |
| no | any | — | 42501 permission denied |
Only the first row is dangerous. A table created by a migration starts with RLS off. That's the Postgres default. The dashboard's Table Editor opens its create-table form with RLS already checked (docs), and since April 2026 the SQL Editor warns before running a CREATE TABLE without RLS. Migrations and client-side SQL get no such warning. ("Enable automatic RLS" exists, but it's an opt-in checkbox at project creation.) Until now, a table like that landed in the first row the moment it was created, unless you remembered RLS.
The Oct 30 change moves new tables to the last row. Nothing can reach them until you grant access. For new tables, this is a safety improvement.
This matters because the first row is common. UpGuard reported on 2026-09-25 that out of roughly 300,000 domains showing signs of Supabase, 16,326 databases exposed readable tables. That's an RLS problem, not something caused by this change. It shows how often the first row happens in practice.
Trap 1: "fixing" 42501 with GRANT ALL
After Oct 30 you create a table, your app gets 42501, and a common fix you'll find (and one an AI tool may well suggest) is:
-- 🔴 the common fix. Dangerous on its own
grant all on table public.notes to anon, authenticated, service_role;
The error goes away. If RLS isn't enabled on that table, you have just built a table that anyone with your anon key can read, update and delete. The anon key ships in your frontend bundle, so that means anyone.
If you build with Lovable, Bolt or an AI coding agent, check any fix it proposes for 42501. I haven't tested what these tools actually suggest. But if one of them grants everything to all three roles, the error will disappear and the fix will look correct.
Even the example in Securing your API (select for anon, full CRUD for authenticated) assumes RLS and policies are in place. Copy the RLS along with it.
When you see 42501, grant only what that role actually needs. The safest habit is to put the RLS in the same SQL file as the GRANT (see the template below).
Trap 2: before Oct 30, adding a GRANT doesn't remove anon's
If you're adding GRANTs to your migrations now, to get ahead of the change, order matters until Oct 30.
On an existing project today, CREATE TABLE already gives anon full CRUD. Adding grant select ... to authenticated doesn't take anything away from anon. GRANT only adds.
So revoke first, then grant only what you need. The same SQL is then safe before and after Oct 30:
create table public.notes ( ... );
-- 1. Remove whatever was auto-granted (a no-op after Oct 30; idempotent)
revoke all on table public.notes from public, anon, authenticated;
-- 2. Enable RLS and write the policies
alter table public.notes enable row level security;
create policy "own rows" on public.notes
for select to authenticated using (user_id = (select auth.uid()));
-- 3. Grant only what the app needs
grant select on table public.notes to authenticated;
grant select, insert, update, delete on table public.notes to service_role;
Until Oct 30, remember that a REVOKE applies to the object as it exists right now. If you drop and re-create a table or view, the default privileges fire again and anon gets its grants back. I reproduced this in a throwaway Postgres 17 container: I ran revoke all on a view, re-created it, and every row became readable again. Keep step 1 in any script that re-creates objects.
Trap 3: Oct 30 doesn't close the holes you already have
"Existing tables keep their grants" means nothing breaks. It also means every table that is open today stays open.
This is a table from my own production database. The SQL that created it looked like this (table name changed; the comment is original):
GRANT ALL ON public.rag_documents TO service_role;
-- no grants to anon / authenticated
The comment is true: the script granted nothing to them. It also revoked nothing. Supabase's default privileges had already done the granting. RLS was off, so the table sat in the first row of the matrix.
During a permission audit in August 2026, a request to the Data API with only the anon key returned 200, and all 4,389 rows were readable.
- The rows were document embeddings for an AI chat feature. No personal data. The same documents live in the table that replaced it.
- It hadn't been updated since March 2026, and no code referenced it anymore. An unused table was publicly readable.
- I haven't been able to confirm whether anyone read it.
- I dropped the table and deleted the SQL file that created it. If the file stays, someone re-running the scripts in order brings the table back, hole included.
The most reliable check is to use the anon key yourself:
curl -s -o /dev/null -w '%{http_code}\n' \
-H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY" \
"https://<project-ref>.supabase.co/rest/v1/<table>?select=*&limit=1"
# 200 → readable with the anon key / 401 (42501) → no grant / 404 → no such table
Trap 4: the audit query can report "no grants anywhere"
Many audit guides use information_schema.role_table_grants:
select table_name, grantee, string_agg(privilege_type, ', ')
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated', 'service_role')
group by 1, 2;
From the SQL Editor (as postgres) this lists the grants correctly. From a read-only role it returns zero rows. That view only shows privileges where the current role is the grantor, the grantee, or a member of the grantee (PostgreSQL docs).
I ran it through the Supabase MCP server, which connects as a read-only role, and all 50 tables appeared to have no grants. Hand that result to an AI agent and it may well suggest "add grants to every table", which leads straight back to Trap 1.
has_table_privilege() returns the same answer no matter which role runs it. This query finds tables that anon can read while RLS is off, the first row of the matrix:
select c.relname
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');
Any row it returns is a table that is readable with your anon key. Views don't have RLS, so check relkind = 'v' separately.
Switching early
Securing your API gives the SQL to opt an existing project into the new default now:
alter default privileges for role postgres in schema public
revoke select, insert, update, delete on tables from anon, authenticated, service_role;
-- the same page has the statements for functions and sequences
Keep for role postgres. Without it, the statement only covers tables created by the role running it. Afterwards, check pg_default_acl to confirm that nothing still grants to anon or authenticated:
select pg_get_userbyid(defaclrole) as grantor,
defaclnamespace::regnamespace as schema,
defaclobjtype,
defaclacl
from pg_default_acl;
Summary
- The Oct 30 change makes new tables safe by default. Fixing
42501withGRANT ALLgives that safety back. - Until Oct 30, write revoke → RLS and policies → minimal grant. The same SQL keeps working after Oct 30.
-
Existing tables are not fixed. Look for tables that anon can read while RLS is off: use the anon key, or
has_table_privilege(). - If you audit with
information_schema, the role you run it as changes the answer.
If you'd like someone to review the GRANTs and RLS in your migrations, get in touch via x3blank.com.
Top comments (0)