Skip to content

Guides

Supabase 42501 "permission denied for table": Causes and Fixes

Niall Hackett ·

You call supabase.from('your_table').select() and get this back:

JSON
{
  "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

  1. A new table created after the change without a GRANT. This is the most common cause from 30 Oct 2026 onward.
  2. The wrong role has the grant. For example, you granted to authenticated, but the request runs as anon because the user isn't signed in or the JWT is missing or expired.
  3. The grant is missing the operation you need. For example, you granted select, but your code runs insert, update or delete.
  4. Privileges were revoked by hand, or someone opted in early by changing default privileges.
  5. A table in a schema that isn't exposed, or a missing USAGE grant on a custom schema.
  6. 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:

SQL
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:

SQL
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:

SQL
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

SQL
-- 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:

SQL
grant usage, select on sequence public.your_table_id_seq to authenticated;

For RPC functions:

SQL
grant execute on function public.your_function() to authenticated;

Step 2: Enable Row Level Security

SQL
alter table public.your_table enable row level security;

Step 3: Add policies

SQL
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 anon as 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

SQL
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