LeakyVibe

Free scan · about 15 seconds · no sign-up

Passive, read-only checks. Only scan apps you own or have permission to test.

Guides · Updated 2026-10-20

Supabase error 42501 "permission denied for table": how to fix it safely

Short answer: 42501 is the Postgres error code for insufficient_privilege: the database role your request runs as (usually anon for signed-out visitors or authenticated for signed-in users) is not allowed to do that operation. In Supabase it most often means the table has no GRANT for that role — which becomes common for new tables in the public schema after Supabase's October 30, 2026 Data API change — or that a Row Level Security (RLS) policy rejected an insert or update. The safe fix is to grant only the privileges each role needs, keep RLS enabled and add policies scoped to the user (auth.uid() = user_id, with WITH CHECK on inserts and updates). Don't "fix" it by disabling RLS, granting everything to anon or putting the service_role / secret key in your frontend.

What error 42501 means

PostgreSQL's list of error codes defines 42501 as insufficient_privilege (https://www.postgresql.org/docs/current/errcodes-appendix.html). Every request your app makes through Supabase's Data API — supabase-js, the other client libraries, or /rest/v1/ — runs as a Postgres role: anon when the visitor isn't signed in, authenticated when they are, and service_role for server-side code using the secret key. If that role doesn't have the privilege for the operation, Postgres refuses.

In supabase-js the call doesn't throw; it returns the error. It looks like this:

{
  "code": "42501",
  "message": "permission denied for table your_table",
  "details": null,
  "hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;"
}

Supabase notes that clients often report 42501 as a 401 or 403 HTTP error (https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors). Read error.hint first: when a grant is missing it names the role, the privilege and the exact GRANT statement (https://supabase.com/docs/guides/api/handling-errors-in-supabase-js). Log the whole error object, not only error.message, or you'll miss the hint.

Why you'll see it more after October 30, 2026

Until now, new tables in a Supabase project's public schema automatically got select, insert, update and delete privileges for the anon, authenticated and service_role roles. According to Supabase's changelog (https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically), that default became opt-in for new projects from May 30, 2026 (rolled out gradually) and is applied to all existing projects on October 30, 2026. After that, a new table in public needs an explicit GRANT before the Data API can see it. Existing tables keep their current grants.

So the typical story is: you ask your AI builder for a new feature, it creates a new table, and the new screen fails with 42501 while everything else keeps working. Our guide to the change itself: https://leakyvibe.com/guides/supabase-october-30-data-api-change

Common causes of 42501 in Supabase

  • Missing GRANT on the table (the most common one): the message says "permission denied for table your_table". Typical for tables created after the October 30 change, or tables whose grants were revoked. Supabase's docs point out that a missing grant raises 42501 before any RLS policy runs — so if a request your policy should allow still fails, check the grants before touching the policy.
  • An RLS policy blocks an insert or update: if anon or authenticated tries to insert or update rows without an RLS policy that allows it, or the new row fails the policy's WITH CHECK, Postgres returns 42501. (A select filtered out by RLS doesn't error — it just returns no rows.)
  • The request runs as the wrong role: for example the user isn't signed in (or the session expired), so the request runs as anon, but you only granted access or wrote policies for authenticated. For signed-out requests auth.uid() is null, so a policy like auth.uid() = user_id never matches.
  • A custom schema without USAGE: tables outside public need the schema added to "Exposed schemas" in the API settings plus GRANT USAGE ON SCHEMA for the API roles (https://supabase.com/docs/guides/api/using-custom-schemas). In PostgreSQL, USAGE on a schema is what allows access to the objects inside it (https://www.postgresql.org/docs/current/ddl-priv.html). The auth and vault schemas are intentionally off-limits to API roles.
  • Column-level restrictions: if you limited which columns a role may read, select * fails with 42501 because it expands to the forbidden columns too.
  • Functions and sequences: functions you call with supabase.rpc() need EXECUTE granted to the calling role (https://supabase.com/docs/guides/api/securing-your-api), and PostgreSQL's GRANT docs note that table privileges don't extend to sequences the table uses, such as SERIAL columns — those need their own grant (https://www.postgresql.org/docs/current/sql-grant.html).

How to check what's wrong (copy-paste SQL)

Open your Supabase dashboard → SQL Editor and run these, replacing your_table with the table from the error message. They only read settings; they don't change anything.

-- 1. Which roles have which privileges 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;

-- 2. Quick yes/no for the roles your app uses
select
  has_table_privilege('anon', 'public.your_table', 'select') as anon_select,
  has_table_privilege('authenticated', 'public.your_table', 'select') as authenticated_select,
  has_table_privilege('authenticated', 'public.your_table', 'insert') as authenticated_insert;

-- 3. Is RLS enabled on the table?
select relname, relrowsecurity as rls_enabled
from pg_class
where oid = 'public.your_table'::regclass;

-- 4. Which policies exist, for which operation and role?
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public' and tablename = 'your_table';
  1. No row for anon or authenticated in query 1 (or false in query 2) → missing grant. Go to the fix below.
  2. The grant is there but inserts or updates fail → look at query 4: is there a policy for that operation (cmd INSERT / UPDATE) and that role, and does its with_check match the row you're writing?
  3. rls_enabled is false → turn RLS on and add policies before granting anything (see the fix).

To see recent 42501 errors from your project's Postgres logs, Supabase documents this query for the SQL Editor:

select
  timestamp,
  event_message,
  log_attributes['parsed.error_severity'] as error_severity,
  log_attributes['parsed.user_name'] as user_name,
  log_attributes['parsed.query'] as query,
  log_attributes['parsed.detail'] as detail,
  log_attributes['parsed.hint'] as hint
from logs
where
  source = 'postgres_logs'
  and log_attributes['parsed.error_severity'] in ('ERROR', 'FATAL', 'PANIC')
  and log_attributes['parsed.sql_state_code'] = '42501'
order by timestamp desc
limit 100;

The Security Advisor (Database → Advisors) and the Data API exposure badge in the Table Editor also show which tables are affected by the October 30 change.

The safe fix: GRANT + RLS + scoped policies

Supabase recommends treating three steps as one unit, ideally in the same migration: grant only what each role needs, enable RLS, and add policies. Example for a table where each signed-in user manages only their own rows (it has a user_id column):

-- 1. Grant only what each role needs
grant select, insert, update, delete on table public.your_table to authenticated;
grant select, insert, update, delete on table public.your_table to service_role; -- server-side code only
-- Only if signed-out visitors must READ this data (truly public content):
-- grant select on table public.your_table to anon;

-- 2. Keep RLS on
alter table public.your_table enable row level security;

-- 3. One policy per operation, scoped to the signed-in user
create policy "Users read own rows" on public.your_table
  for select to authenticated
  using ( (select auth.uid()) = user_id );

create policy "Users create own rows" on public.your_table
  for insert to authenticated
  with check ( (select auth.uid()) = user_id );

create policy "Users update own rows" on public.your_table
  for update to authenticated
  using ( (select auth.uid()) = user_id )
  with check ( (select auth.uid()) = user_id );

create policy "Users delete own rows" on public.your_table
  for delete to authenticated
  using ( (select auth.uid()) = user_id );

WITH CHECK on insert and update is what stops a user from creating a row for someone else or changing user_id to another person's id. Supabase also notes that updates need a matching select policy to work as expected (https://supabase.com/docs/guides/database/postgres/row-level-security).

If the error is about a function or a sequence, grant just that object to the role that needs it:

-- A function your app calls with supabase.rpc()
grant execute on function public.your_function() to authenticated;

-- A sequence behind a SERIAL column (use your real sequence name)
grant usage, select on sequence public.your_table_id_seq to authenticated;

RLS doesn't apply to functions, so only grant EXECUTE to roles that should call them. For a custom schema, add it to "Exposed schemas" in the API settings, run grant usage on schema your_schema to the roles that need it, and then grant table privileges and add RLS the same way.

After the fix, test both ways: signed out (a private window) you should only see public data, and signed in as two different users each one should only see and change their own rows.

What NOT to do

  • Don't disable RLS to make the error go away. Supabase warns that a table in an exposed schema without RLS is readable and writable by any role with a grant on it — and the anon key is in your page, so that means anyone.
  • Don't use the service_role key (or an sb_secret_ key) in the browser. It bypasses RLS; Supabase says never to use a secret key in the browser or expose it to customers.
  • Don't blindly grant everything to anon (grant all, or a bulk grant on all tables in schema public). The error disappears, but signed-out visitors get a write path; Supabase points out that adding policies doesn't take grants back.
  • Don't add a policy like to anon using (true) on personal data: it gives every signed-out visitor read access to every row.
  • Don't roll back the October 30 change for the whole project because one table is missing a grant. Grant that table.

A prompt for Lovable, Bolt or Cursor

My app gets Supabase error 42501 "permission denied for table <table>". Fix it safely: (1) grant only the privileges each role needs on that table — select/insert/update/delete for authenticated, select for anon only if the data is meant to be public, never write access for anon unless I ask; (2) keep Row Level Security enabled; (3) add one policy per operation so users can only read and change their own rows (auth.uid() = user_id), with WITH CHECK on insert and update; (4) if the error is about a function or sequence, grant only EXECUTE or USAGE on that object. Do not disable RLS and never use the service_role or secret key in frontend code. Put it all in one migration, show me the SQL and list every table you changed.

Short checklist

  • Read error.hint — it usually names the missing GRANT.
  • Grants match what each role really needs (anon read-only, and only for public data).
  • RLS enabled on the table, with one policy per operation scoped to auth.uid().
  • WITH CHECK on insert and update policies.
  • No service_role or secret key in frontend code.
  • Tested signed out and as two different users.

Not sure whether your existing tables are readable by anyone? The free LeakyVibe scan above checks from the outside, read-only, in about 15 seconds whether your Supabase tables answer to your public anon key, plus leaked keys and missing headers. It's a quick first check, not a full security audit.

Official sources: PostgreSQL error codes (https://www.postgresql.org/docs/current/errcodes-appendix.html), GRANT (https://www.postgresql.org/docs/current/sql-grant.html) and privileges (https://www.postgresql.org/docs/current/ddl-priv.html); Supabase "Database API 42501 errors" (https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors), "Handling errors in supabase-js" (https://supabase.com/docs/guides/api/handling-errors-in-supabase-js), "Securing your API" (https://supabase.com/docs/guides/api/securing-your-api), "Row Level Security" (https://supabase.com/docs/guides/database/postgres/row-level-security), "Using custom schemas" (https://supabase.com/docs/guides/api/using-custom-schemas) and the changelog "Breaking Change: Tables not exposed to Data and GraphQL API automatically" (https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically).

Free scan · about 15 seconds · no sign-up

Passive, read-only checks. Only scan apps you own or have permission to test.