HomeGuidesSupabase 42501 permission denied after Oct 30: safe fix
Guide
Supabase 42501 permission denied after Oct 30: safe fix
From Oct 30, 2026, new Supabase tables need explicit GRANTs or the API returns 42501. Grant only what each role needs, keep RLS on, and never grant all to anon.
Independent notes from VibeLocked. Not a pentest, and not affiliated with the tools named below.
Starting October 30, 2026, Supabase stops granting new public tables to the Data API on every existing project. Create a table in a migration, in the SQL editor, or through an AI coding tool after that date without a GRANT, and your app gets 42501 permission denied for table your_table, even if the table has Row Level Security policies. Tables that already exist are not touched.
The fix is a grant, but the wrong grant turns a broken feature into a data leak. Grant each role only what it needs (usually authenticated, often not anon), keep RLS enabled with policies on every exposed table, and never fix 42501 with grant all ... to anon on a table without RLS. This guide shows the change, how to check your project, a migration template that works, and the "fixes" that open your data, each tested.
The policy side is covered in the Supabase RLS checklist. Functions have their own grants problem, covered in SECURITY DEFINER functions anyone can call via rpc.
Checked and tested on October 23, 2026 against Supabase's breaking change announcement (last edited October 15, 2026), the Securing your API, Row Level Security, 42501 troubleshooting, and supabase-js error handling docs, and the PostgreSQL docs linked below. We ran every SQL block on PostgreSQL 17.11 behind PostgREST 16.5 (the API layer Supabase uses) with @supabase/supabase-js 2.109.0 and Supabase-style roles and default grants, applying the exact opt-in statements from Supabase's announcement: 26 checks, all passing. VibeLocked is independent and not affiliated with Supabase.
The short version
- What changes: new tables in
publicon existing projects no longer getselect,insert,update,deletegranted toanon,authenticated, andservice_roleby default. No grant means no Data API access. - What does not change: existing tables keep their grants, RLS works the same, and direct Postgres connections (psql, an ORM, a server with a connection string) are not affected.
- The symptom:
42501 permission denied for table ...from supabase-js or/rest/v1/, for signed-out users, signed-in users, and even your server'sservice_rolecalls. - The safe fix: in the same migration as the
create table, grant the minimum per role, enable RLS, and add policies. Treat those three steps as one unit. - Never:
grant all on ... to anonon a private table, disable RLS to make the error go away, or bulk grant every table inpublicto every role. - Also check sequences for
serialcolumns, new views, and functions, which this change does not lock down.
What Supabase is changing on October 30
This is Supabase's announcement, not a rumor. The breaking change post (also published as GitHub discussion 45329) lists the dates:
| Date | What happens |
|---|---|
| April 28, 2026 | Opt-in toggle at project creation |
| May 30, 2026 | New behavior becomes the default for new projects (gradual rollout) |
| October 30, 2026 | New behavior enforced on all existing projects |
Until now, Supabase's default privileges granted every new table in public to anon, authenticated, and service_role. Supabase's Row Level Security guide says that on existing projects a new table "starts with every privilege already granted to all three roles." That is why a table an AI tool created was reachable over the API the moment it existed, before anyone wrote a policy. In our test with those legacy defaults, a brand new table without RLS returned its rows to a signed-out request immediately.
After the change, the announcement says, new tables "require an explicit opt-in (via a Postgres grant) before the Data API can see them," and "Existing tables are not affected in your project, they keep their current grants and stay reachable." Supabase's reason is the same one this site keeps writing about: agents, CLI scripts, and AI platforms now create tables, and "many of those operations do not have a human reviewing the diff." The old default grants "expose every table in public over the Data API on creation, including tables a developer forgot to protect."
You need to act if your app talks to public tables through supabase-js, any Supabase client library, /rest/v1/, or /graphql/v1, and anything in your workflow creates new tables without GRANT statements. That includes old migration files you replay into a fresh project or a preview branch, because those files were written when grants were implicit.
One honest caveat about timing: Supabase called the new-project rollout gradual, and in August 2026 a Supabase CLI maintainer noted that a freshly created platform project was still auto-exposing new tables at the time. The announcement, edited October 15, still lists October 30 for existing projects. Do not guess what your project does: the query below tells you.
What the error looks like
After we applied Supabase's opt-in statements and created a table with RLS and owner-only policies but no grants, every API role failed:
| Request | Result |
|---|---|
Signed out, GET /rest/v1/notes |
401, 42501, permission denied for table notes |
| Signed in as Alice | 403, 42501 |
Server with the service_role key |
403, 42501 |
supabase.from('notes').select('*') |
data: null, error.code: '42501' |
The policies never ran. Supabase's RLS guide explains why: "A missing grant raises a 42501 error before any policy runs. When a request fails that your policy should allow, check the grants before you change the policy." The 401 versus 403 split is PostgREST's: it maps 42501 to 401 for the anonymous role and 403 for everyone else.
On Supabase's hosted API the error also carries a hint with the exact statement to run, such as GRANT SELECT ON public.your_table TO anon;, and the supabase-js error handling guide tells you to read error.hint first. Read it, but do not paste it blindly: the hint names the role that failed, and if that role is anon on a table of private user rows, the right fix is usually to sign the user in, not to grant anon. (Our stock PostgREST 16.5 returned hint: null, so the hint comes from Supabase's platform.)
The same error code has other causes, like column grants, a custom schema that is not exposed, or an RLS with check failure. Supabase's 42501 troubleshooting page lists them. The next section shows how to tell the grant case from the policy case, and from the case with no error at all.
42501 permission denied vs new row violates row-level security policy
After October 30 you will see two errors with the same code, and a third failure that has no error. They need different fixes, and mixing them up is how people end up with grant all to anon or using (true). Supabase's RLS guide sums it up: "Only two of the three raise an error." In its table, a missing grant raises 42501, a with check violation raises 42501, and a using clause that filters the row out "raises nothing" and matches zero rows.
We reproduced all three on October 26, 2026 with the same setup as above (PostgreSQL 17.11, PostgREST 16.5, supabase-js 2.109.0, Supabase style roles with the new default, so no automatic grants) on a notes table with RLS on: 21 checks, all passing. This is what supabase-js returned:
| What you did | HTTP status | error.code |
error.message |
Cause |
|---|---|---|---|---|
Signed out select, no grant |
401 | 42501 |
permission denied for table notes |
Missing GRANT |
Signed in select, insert, or update, no grant |
403 | 42501 |
permission denied for table notes |
Missing GRANT |
Signed in insert with another user's user_id |
403 | 42501 |
new row violates row-level security policy for table "notes" |
WITH CHECK failed |
Signed out insert, insert granted to anon, no anon policy |
401 | 42501 |
same new row violates ... message |
No policy for that role |
Owner sets user_id to another user |
403 | 42501 |
same new row violates ... message |
UPDATE WITH CHECK failed |
Signed in select, grant but no SELECT policy |
200 | none | none, data: [] |
Silent, no matching policy |
update or delete on a row no policy lets you touch |
204 (200 with [] if you add .select()) |
none | none, row unchanged | Silent, USING filtered it out |
Two things stand out. The HTTP status does not tell you the cause: PostgREST's error docs map every 42501 to 403 if the request is authenticated and 401 if not, so the grant case and the policy case look the same at the status level. And error.code is 42501 in both. Read error.message. In our stock PostgREST, details and hint were null for all of them. That holds for direct table requests under RLS. A write that goes through a security definer function can put another user's row in details, so never show the raw error to users (error details that leak hidden rows). On Supabase's hosted API a missing grant also comes with a hint naming the GRANT to run, as covered above.
Quick decision table
| You see | It means | Fix |
|---|---|---|
permission denied for table X |
The role has no privilege for that command on the table. Policies never ran | GRANT the one privilege that role needs. Do not touch the policies |
permission denied for sequence X or for view X |
Same, on a sequence or view | Grant on that object (see sequences and views below) |
new row violates row-level security policy for table "X" |
The grant is fine. The row being written fails a policy | Fix the with check, the values you send, or the SELECT policy (see below). Do not add grants |
No error, data: [], or a write that changes nothing |
Grant is fine, but no policy matches that row for that command | Add or repair the policy's using for that command |
Fix 1: permission denied for table means a missing GRANT
Grant only the privilege the failing call needs, to the role that made it:
grant select, insert, update, delete on table public.notes to authenticated;
Supabase's RLS guide says "A missing grant raises a 42501 error before any policy runs," so loosening a policy here changes nothing. If the role in the error is anon and the table holds private rows, the fix is usually to sign the user in, not to grant anon.
Fix 2: new row violates row-level security policy means a policy rejected the row
The PostgreSQL CREATE POLICY docs explain the two halves of a policy: "Existing table rows are checked against the expression specified in USING, while new rows that would be created via INSERT or UPDATE are checked against the expression specified in WITH CHECK." A failing using hides rows quietly. A failing with check is an error. Check these in order:
- The values you send. In our test, an insert that set
user_idto another user, and an update that moved a note to another user, both failed this way, as they should. Let the column default fill it (user_id uuid not null default auth.uid()) instead of sending it from the client. - A policy for this role and command exists. With
insertgranted toanonbut only ato authenticatedpolicy, a signed-out insert got 401 withnew row violates, notpermission denied. If signed-out users should not insert, revoke the grant. If they should, write a narrowto anonpolicy. - UPDATE policies without
with check. The docs say that if onlyusingis given, "that clause will be used for both USING and WITH CHECK cases." Our update policy with onlyusing ((select auth.uid()) = user_id)blocked an owner from giving a row away with the same error. Write both clauses on purpose. - The new row must still be readable. This is the one that surprises people. If the request reads columns (a
whereorreturning), the new row must also pass the table's SELECT policies. The CREATE POLICY docs say that if a newly inserted or updated row "does not satisfy the relation's SELECT policies, an error will be thrown".
The classic case is a soft delete. With a SELECT policy of using ((select auth.uid()) = user_id and deleted_at is null) and a correct owner UPDATE policy, Alice running supabase.from('notes').update({ deleted_at: now }).eq('id', 1) got 403, new row violates row-level security policy for table "notes", with and without .select(), and the row stayed unchanged. The same update ... where id = 1 failed in psql too, because the where reads the row. The UPDATE policy was fine. The problem was that after the update, no SELECT policy let her see the row. The fix is a separate owner read policy, and filtering deleted rows in the app or a view:
create policy "owner reads own rows incl deleted" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id);
After that, the soft delete returned 204 and set deleted_at. The same rule bites insert(...).select(): with an INSERT policy but no SELECT policy, a plain insert returned 201, while .insert(...).select() failed with new row violates. Soft deletes and DELETE lockdown are covered in depth in recover deleted Supabase rows and lock DELETE down.
Fix 3: no error but nothing happens means no policy matched
The PostgreSQL docs say: "Typically, no error occurs when a row is not visible." With RLS on and no matching policy, they describe a "default deny" policy, "so that no rows will be visible or updatable." In our test, with grants but no policies, select returned 200 with [], update returned 204 with the row unchanged, and delete({ count: 'exact' }) returned 204 with count: 0. With owner policies, Alice's update and delete aimed at Bob's note also returned no error and changed nothing. That is RLS working, but it is easy to miss in code.
Add .select() to writes, or pass { count: 'exact' }, and treat 0 rows as a failure in your app. Then add a policy for the missing command with a using that matches the rows the user should touch. Do not reach for using (true) to make the empty result go away. RLS on but using (true) shows how that hands every row to everyone. Upserts are the exception to the silent case: with on conflict do update, a row that fails using raises new row violates row-level security policy (USING expression). Supabase upsert new row violates row-level security covers that error and the grants and policies an upsert needs.
Diagnostic query: grants and policies for one table
Run this read-only query in the SQL editor, replacing public.notes and notes with your table. The first part shows which roles hold which privileges, using PostgreSQL's has_table_privilege. The second lists every policy from the pg_policies view, with its using and with check.
select r.rolname as role,
has_table_privilege(r.rolname, 'public.notes', 'select') as can_select,
has_table_privilege(r.rolname, 'public.notes', 'insert') as can_insert,
has_table_privilege(r.rolname, 'public.notes', 'update') as can_update,
has_table_privilege(r.rolname, 'public.notes', 'delete') as can_delete
from pg_roles r
where r.rolname in ('anon', 'authenticated', 'service_role')
order by r.rolname;
select policyname, permissive, roles, cmd,
qual as using_expr, with_check
from pg_policies
where schemaname = 'public' and tablename = 'notes'
order by cmd, policyname;
How to read it:
falsefor the role and command that failed: a missing grant. Fix 1.true, but no policy row for thatcmdand role (orALL): the silent case for reads, updates, and deletes, ornew row violatesfor inserts. Add a policy.- A policy row exists, but
with_checkis empty on an UPDATE: theusing_expris reused as the check. Make sure that is what you want. - A SELECT policy that hides some rows (like
deleted_at is null): updates that move a row out of that set fail withnew row violatesunless another SELECT policy still covers the row.
When we ran it against our insert only test table, it showed authenticated with can_select and can_insert true and only one policy, INSERT with a with_check. That is exactly why .insert(...).select() failed there: there was no SELECT policy.
Check what your project does today
Run this read-only query from Supabase's Securing your API guide in the SQL editor. It shows what new objects that postgres creates in public will receive:
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;
tablesrows withSELECT, INSERT, UPDATE, DELETE: the old behavior. Every new table is reachable over the API as soon as it exists, so RLS must be in the same migration.tablesrows withoutSELECT,INSERT,UPDATE,DELETE(for example onlyMAINTAIN, REFERENCES, TRIGGER, TRUNCATE), or notablesrows: the new behavior. New tables need explicit grants.
In our test, after Supabase's published opt-in statements, the table rows dropped to MAINTAIN, REFERENCES, TRIGGER, TRUNCATE (MAINTAIN exists on Postgres 17), sequences kept UPDATE, and functions kept EXECUTE. The Data API does not use those leftover table privileges, but the docs note that SQL running as anon or authenticated, such as a database function, can.
You can adopt the new behavior now instead of waiting, which is the better way to find broken migrations on your schedule rather than Supabase's. The announcement gives the statements:
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;
Existing tables keep their grants. Only tables created afterward need explicit grants. Try it on a branch or a local database first.
The safe fix: one migration, three steps
For every new table your app reads or writes through supabase-js, put the grant, RLS, and policies together:
create table public.notes (
id bigint generated always as identity primary key,
user_id uuid not null default auth.uid(),
body text not null
);
-- 1. Grants: only what each role needs
grant select, insert, update, delete on table public.notes to authenticated;
grant select, insert, update, delete on table public.notes to service_role;
-- no grant to anon: signed-out visitors have no business here
-- 2. RLS
alter table public.notes enable row level security;
-- 3. Policies, one per operation
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);
create policy "update own notes" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "delete own notes" on public.notes
for delete to authenticated using ((select auth.uid()) = user_id);
Results: a signed-out request still got 401 with 42501, Alice saw only her 1 note, Bob saw only his 2, Alice could insert her own note, an insert with Bob's user_id was rejected by RLS, and a delete aimed at Bob's rows changed 0 rows. The service_role key read everything, which is why it stays on your server (anon key vs service role key explains why).
How to decide what each role gets:
| Role | Who it is | Grant |
|---|---|---|
anon |
Anyone with your publishable key, which is in your frontend | Nothing, or select only on data that is truly public, like published posts or a price list |
authenticated |
Anyone who signed up | Only the operations your UI performs. A read-only feed needs select, not delete |
service_role |
Your server, webhooks, cron jobs | What your backend code needs. It bypasses RLS, so keep its key server-side |
Supabase's own example grants select to anon. That fits a public table. Copying it onto a table of user data is how private rows end up on the open internet. If a table must be readable signed-out, grant select only, keep RLS on, and write a to anon policy that limits the rows (for example using (published)). If only some columns are public, look at column-level grants.
The "fixes" that open your data
When an AI tool hits 42501, it tends to reach for the biggest hammer. We tested each one on the same table of private notes:
| What the tool does | What happened |
|---|---|
grant all on public.notes to anon with RLS on and no anon policy |
Signed out got 0 rows. Not a leak yet, but the grant is waiting for the next mistake |
The same grant, plus alter table ... disable row level security |
Signed out read all 3 private notes from 2 users, and a signed-out DELETE removed Bob's 2 notes |
grant select to authenticated only, RLS off |
Alice read Bob's notes. Anyone who signs up is "authenticated" |
grant ... on all tables in schema public to anon, authenticated, service_role |
A signed-out request read an internal admin_audit table (RLS off) that had never been exposed before |
That last one deserves attention. Supabase's announcement includes the bulk grant in its FAQ, as a rollback for people who opted in early and broke their app. It restores the old behavior for every table at once, including internal tables that the new default was protecting. If you use it, follow Supabase's own advice in the same FAQ and revoke the private tables right after, then run the audit below.
The other common move is to disable RLS "for now." Supabase's Securing your API guide is clear: "Tables and views exposed through the Data API without RLS can be accessed by any role with matching grants." A grant decides whether a role can touch the table. RLS decides which rows. You need both. If a client role already deleted rows you need back, see recover deleted Supabase rows and lock DELETE down.
Sequences, views, and functions
Serial columns. A serial or bigserial id draws from a sequence, and an insert needs usage (or update) on it. In our test, an identity column needed no sequence grant. A serial column still inserted after Supabase's opt-in statements, because new sequences keep update, but after the stricter revoke all on sequences from the Securing your API guide, inserts failed with permission denied for sequence todos2_id_seq until we ran:
grant usage on sequence public.todos2_id_seq to authenticated;
Prefer generated always as identity for new tables and you skip this step.
Views. A new view is a new object, so it needs its own grant. Ours failed with permission denied for view my_notes until we granted select to authenticated. Create views with (security_invoker = true) so the table's RLS applies to the person asking; ours then returned only Alice's rows to Alice. Views and functions that bypass RLS covers why the default view mode is risky.
Functions. The October 30 change does not cover them. The Securing your API guide says: "New functions are callable by every role." In our test, a function created after the opt-in statements was still callable by a signed-out request. Revoke EXECUTE yourself, from public and anon together, as shown in the SECURITY DEFINER functions guide.
Audit every table in one query
Run this read-only query in the SQL editor. It lists every table and view in public, whether RLS is on, and what signed-out and signed-in users can do. has_any_column_privilege also catches column-level grants that a table-level check would miss.
select c.relname as table_name,
case c.relkind when 'v' then 'view' when 'm' then 'matview' else 'table' end as kind,
c.relrowsecurity as rls_on,
has_any_column_privilege('anon', c.oid, 'select') as anon_read,
has_table_privilege('anon', c.oid, 'insert,update,delete') as anon_write,
has_any_column_privilege('authenticated', c.oid, 'select') as signed_in_read,
has_table_privilege('authenticated', c.oid, 'insert,update,delete') as signed_in_write
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind in ('r', 'p', 'v', 'm')
order by (has_any_column_privilege('anon', c.oid, 'select') and not c.relrowsecurity) desc,
c.relname;
How to read it:
anon_read = trueandrls_on = false(sorted to the top): anyone with your publishable key can read every row. Fix today.anon_write = true: signed-out users can change data, limited only by your policies. Rarely intended.signed_in_write = trueon a table your UI only reads: revoke the write privileges.- Views show
rls_on = falsebecause RLS lives on the underlying table. Check they aresecurity_invoker.
In our test, after the bulk grant this query put admin_audit at the top with every column true and rls_on = false. After the cleanup it showed anon_read = false everywhere and RLS on for every table. The Security Advisor also flags affected tables until the cutover, per the announcement; Security Advisor warnings explained covers the rest of its list.
Local development and old migrations
- Supabase CLI: the
[api]section ofsupabase/config.tomlhas anauto_expose_new_tablessetting. Its default has changed between CLI releases, so setauto_expose_new_tables = falseexplicitly if you want your local stack andsupabase db resetto behave like production after October 30. - Replaying old migrations into a new project or preview branch recreates tables without grants. Add a migration that grants the tables your app uses, role by role, rather than the bulk grant.
- Put grants in a test. Testing Supabase RLS in CI shows pgTAP tests that assert
anongets42501on private tables and signed-in users see only their own rows.
Test it from your terminal
After the migration, test from your own terminal against your own project with only the publishable key (or the legacy anon key). Do not use the secret or service_role key here, because it bypasses RLS and makes everything look fine.
export SUPABASE_URL="https://<project-ref>.supabase.co"
export SUPABASE_PUBLISHABLE_KEY="sb_publishable_..."
# A private table, signed out: expect 401 with code 42501, or 200 with []
curl -i "$SUPABASE_URL/rest/v1/notes?select=*" \
-H "apikey: $SUPABASE_PUBLISHABLE_KEY"
A 401 with 42501 means anon has no grant, which is right for private data. A 200 with [] means anon has a grant and RLS is filtering, which works but is one policy mistake away from a leak. A 200 with rows on a private table means fix it now. For the signed-in side, sign in as a test user in your app and confirm you see your own rows and not another test user's.
To check what a stranger can reach from the outside, run your live URL through LeakCheck. It finds the Supabase key your app ships and checks, read only, whether that key can list rows from your tables.
Common AI-generated mistakes
- Creating tables with no grants after October 30, then "fixing" the 42501 by disabling RLS.
- Copying the error hint that says
GRANT SELECT ... TO anononto a table of private user data. grant allwhen the app only reads.allincludestruncate,references, andtriggertoo.- Granting
anonandauthenticatedthe same privileges by habit. They are different audiences. - The bulk grant on all tables in
public, which re-exposes every internal table at once. - Loosening a policy to
using (true)when the real problem was a missing grant. RLS on but using (true) shows why that leaks. - Forgetting the sequence on
serialcolumns, or the grant on a new view. - Testing as
postgresin the SQL editor or with the secret key, which proves nothing about what users can do.
Checklist
- The default privileges query shows whether new tables are auto-granted in your project
- Every migration that creates a
publictable also has its grants,enable row level security, and policies -
anonhas no grant on any table of private user data -
authenticatedhas only the operations your UI performs -
service_rolehas what the backend needs, and its key never reaches the browser - The audit query shows no table with
anon_read = trueandrls_on = false - New views use
security_invoker = trueand have their own grant -
serialcolumns haveusageon their sequence, or you switched to identity columns - Functions have
EXECUTErevoked frompublicandanonunless meant to be public - curl with only the publishable key gets 401 or
[]on private tables -
auto_expose_new_tablesis set explicitly insupabase/config.toml
Copy-paste prompt for your AI coding tool
Paste this into Cursor, Claude Code, Lovable, or Bolt. Share code and table names only, never keys, passwords, or production data.
Read every SQL migration in supabase/migrations. From October 30, 2026, Supabase no longer grants new public tables to anon, authenticated, and service_role automatically, so every table the app uses through supabase-js needs explicit GRANTs. For each table and view in public, tell me: which supabase-js calls use it (from the frontend and the server), whether it has explicit grants in a migration, whether RLS is enabled, and which policies exist. Then write one new migration and show it to me before running anything: 1) Grant each role only what the code actually uses. Grant anon nothing unless the data is meant to be public, and then only select. Grant authenticated only the operations the UI performs. Grant service_role what the server code needs. 2) Enable row level security on every table that gets a grant, and add one policy per operation using (select auth.uid()). 3) Grant usage on the sequence for any serial column that authenticated inserts into. 4) Create views with security_invoker = true and grant them separately. Do not use grant all, do not grant on all tables in schema public, do not disable RLS, and do not use using (true). Finally, list any table where anon would be able to read rows after this migration and explain why.
Read the migration before it runs, try it on a branch or local database, then run the curl test above. More prompts are in the security review prompt guide.
Sources
- Supabase changelog: Breaking change, tables not exposed to Data and GraphQL API automatically (dates, opt-in SQL, FAQ)
- Supabase GitHub discussion 45329 (same announcement, last edited October 15, 2026)
- Supabase: Securing your API (grants and RLS, default privileges, check and revoke queries)
- Supabase: Row Level Security (grants and policies, 42501 before any policy runs, only two of three denials raise an error)
- Supabase: Database API 42501 errors (troubleshooting)
- Supabase: Handling errors in supabase-js (read error.hint first)
- Supabase: API keys (publishable key maps to anon)
- Supabase: Postgres roles
- Supabase: Column level security
- Supabase CLI PR 5524: auto_expose_new_tables config setting
- Supabase CLI PR 6337: auto_expose_new_tables default changed back to match the platform
- PostgREST: Errors (42501 maps to 401 or 403)
- PostgreSQL: Privileges
- PostgreSQL: GRANT
- PostgreSQL: ALTER DEFAULT PRIVILEGES
- PostgreSQL: Sequence functions (nextval needs USAGE or UPDATE)
- PostgreSQL: System information functions (has_table_privilege, has_any_column_privilege)
- PostgreSQL: CREATE POLICY (USING vs WITH CHECK, policies applied by command type)
- PostgreSQL: Row security policies (default deny, no error for rows that are not visible)
- PostgreSQL: pg_policies view
Questions
What changes in Supabase on October 30, 2026?
Supabase applies its new default to all existing projects: tables you create in the public schema after the change are no longer granted to anon, authenticated, and service_role automatically, so the Data API (supabase-js, REST, GraphQL) cannot reach them until you add an explicit GRANT. Tables that already exist keep their current grants, and apps that only use a direct Postgres connection are not affected.
Why do I get 42501 permission denied for table on a table that has RLS policies?
Because Postgres checks grants before it looks at policies. If the role making the request has no SELECT, INSERT, UPDATE, or DELETE privilege on the table, the query fails with 42501 before any RLS policy runs. Adding or loosening policies cannot fix a missing grant. Grant the privilege the role needs, then let RLS decide which rows it sees.
What does new row violates row-level security policy mean in Supabase?
The role has the GRANT, but the row it tried to write fails a policy. It comes from an INSERT whose row fails the WITH CHECK, an UPDATE whose new row fails the WITH CHECK (or the USING, if the policy has no WITH CHECK), or a write whose new row the role cannot read back through a SELECT policy, such as a soft delete that hides deleted rows. It has the same 42501 code and the same 401 or 403 status as a missing grant, so read error.message to tell them apart. Fix the policy, not the grant.
Why does my Supabase update or delete return no error but change nothing?
Because RLS filters rows with the USING expression, and Postgres does not raise an error for rows a role cannot see. If no policy matches, or the USING clause excludes the row, the update or delete matches 0 rows and supabase-js returns error: null. In our test the update returned 204 and the row was unchanged. Add .select() or { count: 'exact' } so your code can see that 0 rows changed, then add or fix the policy for that command.
Is it safe to fix 42501 with grant all on the table to anon?
No. anon is everyone with your publishable key, which ships in your frontend. In our test, granting all to anon on a table with RLS turned off let a signed-out request read every user's private notes and delete another user's rows. Grant anon only SELECT on data that is meant to be public, grant signed-in operations to authenticated, and keep RLS on with policies for every exposed table.
Do I need to grant the sequence too?
It depends on the column. An identity column (generated always as identity) inserted fine with only table grants in our test. A serial column uses a sequence: with Supabase's published opt-in revoke it still worked, because new sequences keep UPDATE, but after the full revoke in the Securing your API docs the insert failed with permission denied for sequence until we ran grant usage on the sequence.
Does the October 30 change protect my database functions?
No. It covers tables and sequences. Supabase's Securing your API guide says new functions are still callable by every role, because Postgres grants EXECUTE to PUBLIC by default. In our test, a new function created after the change was still callable by a signed-out request. Revoke EXECUTE on functions yourself.