← All posts
·7 min read·GuardLayer

Supabase: permission denied for table (42501)

SupabasePostgresRLSPermissionsSecurity

TL;DRpermission denied for table X (SQLSTATE 42501) means the role calling your API (anon or authenticated) has no table-level GRANT. It is not an RLS problem — the grant check runs before RLS, so no policy ever executes. Fix it by granting the privilege:

grant usage on schema public to anon, authenticated;
grant select, insert, update, delete on public.your_table to authenticated;

Adding RLS policies will not help. Keep reading for the sequence gotcha, the role trap, and how to tell this apart from the other 42501.

Why does Supabase say "permission denied for table"?

Postgres checks access in layers, in a fixed order:

  1. Database CONNECT
  2. Schema USAGE
  3. Table privilege (SELECT / INSERT / UPDATE / DELETE)
  4. Column and sequence privileges
  5. Row-Level Security policies

RLS is dead last. That ordering is the whole story. When the calling role has no GRANT on the table (layer 3), Postgres stops there and raises 42501: permission denied for table X. It never reaches layer 5, so your RLS policies are irrelevant to this error. Writing more of them, or "fixing" them, changes nothing.

Supabase's Data API (PostgREST) forwards the raw Postgres error straight through, often with a hint naming the exact privilege you're missing. Trust that hint — it's pointing at the right layer.

The most common way to hit this: you created the table with raw SQL, a psql session, or an ORM migration (Prisma, Drizzle). Supabase configures default privileges so that objects created by its own dashboard roles get anon/authenticated grants automatically. Tables created by external tooling running as a different owner don't always inherit those defaults — so the table exists, RLS may even be enabled, but the first API call throws permission denied because nobody was ever granted access.

Don't rely on that default-privilege behavior. The durable habit is to grant explicitly in the same migration that creates the table, so schema, grants, and policies never drift.

The fix: grant the right privileges to the right role

Here is the complete fix. Run it in the SQL Editor or bundle it into your migration:

-- 1. schema access (checked before any table privilege)
grant usage on schema public to anon, authenticated;

-- 2. table privileges — list only the verbs you actually need
grant select, insert, update, delete on public.your_table to authenticated;

-- add anon ONLY if you truly want unauthenticated access:
-- grant select on public.your_table to anon;

-- 3. sequences — REQUIRED for INSERT into a serial/bigserial PK,
--    or the insert throws its own separate 42501 on the sequence.
--    (GENERATED ... AS IDENTITY columns do NOT need this.)
grant usage, select on all sequences in schema public to authenticated;

Three things people forget, in order of how often they bite:

  • The sequence grant. You grant the table, SELECT works, then your first INSERT fails with permission denied for sequence your_table_id_seq. That's a second 42501, on the sequence behind a serial/bigserial PK. Grant usage, select on it. Identity columns are exempt — INSERT on the table covers them.
  • Schema USAGE. Checked before table privileges. Miss it and everything underneath is denied no matter how many table grants you add.
  • The role. anon is the unauthenticated key; authenticated is a request carrying a user JWT. Grant to authenticated but call with the anon key (or the reverse) and you'll still get denied. Don't hand anon write access unless you genuinely want anonymous writes.

Verify what's actually granted right now:

select grantee, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and table_name = 'your_table';

Grants decide whether a role can touch a table. RLS decides which rows it sees. Independent layers, and you almost always want both. Put the grant statements, enable row level security, and your policies in the same migration so nothing drifts apart.

Is this the same as "new row violates row-level security policy"?

No — and this is the most expensive mix-up, because both errors share SQLSTATE 42501. Different message, different layer, different fix:

MessageLayerRoot causeFix
permission denied for table XTable GRANT (pre-RLS)Role has no table privilegeGRANT to the role
new row violates row-level security policy for table "X"RLS WITH CHECKRole has the grant, but the row fails a policyFix the RLS policy

If your message is permission denied for table, you have a grant problem — stay in this post. If it's new row violates row-level security policy, you're past the grant check and your INSERT/UPDATE row is failing a policy's WITH CHECK expression; that's covered in our companion guide on why new rows violate your RLS policy — the 42501 you get after the grants are correct. Auditing grants when the culprit is a policy (or vice versa) is how people lose an afternoon. The message text is the tell — read it first.

One more thing: a SELECT-blocking RLS policy never raises an error. It silently returns zero rows. So if you're seeing any 42501 at all, RLS SELECT filtering is not your problem — you're either missing a grant or failing a WITH CHECK on a write.

Common wrong turns

  • Adding more RLS policies to fix permission denied for table. Wrong layer. RLS runs after the grant check; with no grant, policies are unreachable.
  • Disabling RLS to make it go away. It won't — this error is pre-RLS — and you'd strip row protection for nothing. If you're tempted to toggle RLS at all, read what actually happens when RLS is disabled first.
  • Reaching for service_role from the browser. It "works" because it bypasses RLS entirely, which is exactly why it's a critical hole, not a fix. Never ship the service role key to a client.
  • Granting to the wrong role, or forgetting the sequence — the two failure modes from the fix above.

If you're still shaky on how grants, roles, and policies fit together, the complete guide to Supabase Row Level Security walks the full model end to end.

FAQ

Is permission denied for table an RLS error? No. It's a missing table-level GRANT, checked before RLS. No policy runs when this fires. Fix it with grant, not with policies.

Why did my table work in the SQL Editor but fail over the API? The API calls as anon or authenticated. Tables created by raw SQL or ORM migrations may never have received grants for those roles, so the API is denied even though your own SQL session works fine.

I granted the table but INSERT still fails with 42501. Why? You're hitting the sequence behind a serial/bigserial primary key. Run grant usage, select on all sequences in schema public to authenticated;.

Should I grant to anon or authenticated? authenticated for logged-in users, anon only for genuinely public access. Match the grant to the key your client uses. Never give anon write access unless anonymous writes are intended.

Will disabling RLS fix permission denied for table? No. The error occurs before RLS is evaluated, so disabling RLS has no effect — and it removes your row-level protection.

Where GuardLayer fits (and where it doesn't)

Straight up: GuardLayer is a static scanner, and it does not inspect GRANT statements or your database's live privilege state. Grants live in Postgres catalog tables and in migration SQL that runs against your database — runtime and infrastructure state a source-tree scan can't reason about. So GuardLayer will not tell you that authenticated is missing SELECT on public.orders. Use the information_schema.role_table_grants query above for that; it's the source of truth.

What GuardLayer does catch is the adjacent class of mistakes that live in code — a service_role key wired into a client-side file, or a table shipped with RLS never enabled. Those are the patterns that turn a grant fix into a breach. For the permission-denied error itself, the fix is the GRANT — nothing to scan.

Catch this before it ships — free

GuardLayer scans every push for this and 23 other Next.js + Supabase issues, with the exact fix inline.

No signup, no card — your code is scanned in memory and never stored.

Keep reading