You call supabase.from('your_table').select() and get this back:
{
"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;"
}42501 is Postgres's insufficient_privilege error. The database role making the request (anon, authenticated or service_role) has no privilege on the table. Postgres rejects the query before Row Level Security is even checked. Starting 30 October 2026, many more projects will run into it.
What changes on 30 October 2026
Supabase is rolling out a breaking change: new tables in the public schema are no longer exposed to the Data API (REST/GraphQL, and so supabase-js) automatically. According to the Supabase changelog:
- 28 Apr 2026: new projects could opt in to the setting at creation.
- 30 May 2026: it became the default for new projects (rolled out gradually).
- 30 Oct 2026: it applies to all existing projects.
After the change reaches your project, a new public table needs an explicit Postgres GRANT before the Data API can see it. Existing tables keep their current grants and still work. If you only connect over a direct Postgres connection (psql, an ORM, a server using a connection string), this change doesn't affect you.
You need to act if your migrations, AI coding tool, CLI scripts or Management API calls create tables without GRANT statements and expect clients to reach them right away. The Supabase CLI made the same change locally (supabase/cli#5524). There, [api].auto_expose_new_tables = true is deprecated, and the field is removed on 30 Oct 2026.
Common causes of 42501
- A new table created after the change without a GRANT. This is the most common cause from 30 Oct 2026 onward.
- The wrong role has the grant. For example, you granted to
authenticated, but the request runs asanonbecause the user isn't signed in or the JWT is missing or expired. - The grant is missing the operation you need. For example, you granted
select, but your code runsinsert,updateordelete. - Privileges were revoked by hand, or someone opted in early by changing default privileges.
- A table in a schema that isn't exposed, or a missing
USAGEgrant on a custom schema. - Sequences or functions. An insert that uses a serial column also needs privileges on the sequence, and RPC calls need
EXECUTE.
Not the same thing: if the grant exists but RLS has no matching policy, you usually get empty results on reads, or a "new row violates row-level security policy" error on writes. You won't get 42501.
How to check for it
1. Read the error hint. PostgREST's 42501 response includes a hint that gives the exact GRANT statement you're missing (Supabase docs).
2. List the grants on the table:
select grantee, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and table_name = 'your_table'
order by grantee, privilege_type;3. Find public tables with no grants for the API roles:
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 = 'r'
and not has_table_privilege('authenticated', c.oid, 'select')
and not has_table_privilege('anon', c.oid, 'select');4. Check RLS status:
select relname, relrowsecurity
from pg_class
where relnamespace = 'public'::regnamespace and relkind = 'r';5. Use Security Advisor. Supabase says Security Advisor flags affected tables ahead of the rollout.
Step-by-step fix: GRANT + RLS + policies
Supabase recommends doing all three steps together, in the same migration that creates the table:
Step 1: Grant only the privileges each role needs
-- Public read-only (only if the data is really public)
grant select on public.your_table to anon;
-- Signed-in users (RLS still applies)
grant select, insert, update, delete on public.your_table to authenticated;
-- Server-side code using the service role
grant select, insert, update, delete on public.your_table to service_role;If you use identity or serial columns and insert from the client, grant the sequence too:
grant usage, select on sequence public.your_table_id_seq to authenticated;For RPC functions:
grant execute on function public.your_function() to authenticated;Step 2: Enable Row Level Security
alter table public.your_table enable row level security;Step 3: Add policies
create policy "users can read their own rows"
on public.your_table for select to authenticated
using (auth.uid() = user_id);
create policy "users can insert their own rows"
on public.your_table for insert to authenticated
with check (auth.uid() = user_id);
create policy "users can update their own rows"
on public.your_table for update to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);Step 4: Verify
Run the query again from the client as the right role, and run the grants query above again.
What NOT to do
- Don't disable RLS just to make the error go away. A GRANT without RLS exposes every row to anyone who holds your anon key.
- Don't run
grant all ... to anonas a blanket fix. Grant per role and per operation. - Don't grant tables that should stay private. Leaving out the grant is now the safe default.
Template for new-table migrations
create table public.notes (
id bigint generated by default as identity primary key,
user_id uuid not null default auth.uid() references auth.users(id),
body text
);
grant select, insert, update, delete on public.notes to authenticated;
grant select, insert, update, delete on public.notes to service_role;
alter table public.notes enable row level security;
create policy "own notes" on public.notes
for all to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);Check what your live app exposes
Fixing 42501 often means adding grants, and every grant you add is a door. ReplayWatch's free scan looks at your live site from the outside and checks:
- page load
- security headers
- secrets exposed in HTML/JS
- public Supabase/Firebase reads (tables readable without signing in)
- broken links and assets
- SEO tags
- whether your signup page is reachable
It doesn't sign in or run login flows, so it shows you what an anonymous visitor can see. That's a useful check after you change grants. Run a free ReplayWatch scan →
Sources
- Supabase changelog: Breaking Change: Tables not exposed to Data and GraphQL API automatically
- Supabase docs: Securing your API
- Supabase CLI: PR #5524, flip auto_expose_new_tables default to false