Home / RLS / permission denied for sequence

Supabase RLS · Error fix

Fix "permission denied for sequence" in Supabase

Tested on Postgres 18 · Supabase✓ code tested against a real database
TL;DR

A serial / bigserial column takes its value from a separate sequence, and the inserting role needs USAGE on it. Use an identity column (generated always as identity for new tables) and the problem goes away, or grant the sequence. From Supabase's Oct 30 change, service_role also needs an explicit table grant on new tables: bypassing RLS doesn't bypass grants.

01 What the error means

response

{
  "code": "42501",
  "message": "permission denied for sequence invoices_id_seq"
}

You granted insert on the table and the insert still fails. A serial or bigserial column is a default of nextval('invoices_id_seq'), and calling nextval needs USAGE on that sequence. The table grant doesn't cover it.

Until recently Supabase granted every new table and sequence to the API roles by default, so most projects never hit it. New projects have had that off since a gradual rollout that started May 30, 2026, and existing projects follow on October 30, 2026 (what changes on Oct 30). From then on, the first insert into a new table with a serial id hits this error.

02 Best fix: use an identity column

An identity column also uses a sequence internally, but Postgres treats it as part of the column: the insert grant on the table is enough.

migration.sql

create table public.invoices (
  id      bigint generated always as identity primary key,
  user_id uuid not null references auth.users(id),
  amount  numeric not null
);

alter table public.invoices enable row level security;
create policy "owner inserts" on public.invoices for insert to authenticated
  with check (user_id = (select auth.uid()));
grant insert on public.invoices to authenticated;

No sequence grant anywhere, and inserts work. The Postgres wiki's "Don't Do This" page recommends identity over serial too.

03 Keeping serial: grant the sequence

If the table already exists with a serial column, grant USAGE on its sequence to the role that inserts, in the same migration as the table grant:

migration.sql

grant insert on public.invoices to authenticated;
grant usage on sequence public.invoices_id_seq to authenticated;

The sequence is named <table>_<column>_seq by default. pg_get_serial_sequence('public.invoices', 'id') returns the exact name if you're not sure. Grant it per sequence, to the role that needs it, rather than on all sequences in schema public.

04 service_role gets 42501 too

A backend using the service_role key bypasses RLS, so it's easy to assume it can touch any table. It can't: RLS and grants are separate checks, and after the change new tables aren't granted to service_role either. The first new table your backend reads fails with permission denied for table.

migration.sql

-- only what the backend actually does with this table
grant select, insert, update on public.invoices to service_role;

If that table uses serial, the backend's inserts also need grant usage on sequence public.invoices_id_seq to service_role;. Once granted, service_role still bypasses RLS, as before.

05 Find the sequences that will break

Run this in the SQL editor. It lists the sequences behind serial columns in public, the table each one feeds, and whether authenticated can use it. Identity sequences are left out, since they don't need a grant. Swap the role name to check anon or service_role:

check.sql

select t.relname as table_name,
       c.relname as sequence_name,
       has_sequence_privilege('authenticated', c.oid, 'usage') as auth_can_use
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
left join pg_depend d on d.objid = c.oid and d.deptype = 'a'
left join pg_class t on t.oid = d.refobjid
where n.nspname = 'public' and c.relkind = 'S'
  and not exists (select 1 from pg_depend i
                  where i.objid = c.oid and i.deptype = 'i')
order by 1, 2;

A row with auth_can_use = false on a table your app inserts into will fail on its next insert.

◆ FREE · 30 SECONDS · NO SIGNUP

Is your app leaking right now?

Paste your live URL. The scanner reads your page and its JavaScript for a Supabase service_role key, the one leak that skips every policy in this guide.

◆ FREE · MIT · npm + GitHub Action

Catch this before it ships

airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive: the same class of bug, caught in CI against a database with your migrations applied, before it reaches prod.

npx airlock-rls→

Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.

FREE PDF

Grab the Supabase RLS cheat sheet

The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.

FAQ

Do I need SELECT on the sequence too?

Not for inserts. USAGE allows nextval and currval, which is what a serial default calls. SELECT on a sequence only lets a role read its last value, which an API role rarely needs.

How do I switch an existing serial column to identity?

In one migration, four statements. First: alter table public.invoices alter column id drop default. Second: drop sequence public.invoices_id_seq. Third: alter table public.invoices alter column id add generated by default as identity. Fourth: select setval(pg_get_serial_sequence('public.invoices', 'id'), (select max(id) from public.invoices)), so new ids continue after the existing ones (on an empty table it does nothing, which is fine). by default keeps inserts that pass an explicit id working; use always if none do. Run it on a branch first.

My insert works but .insert().select() fails with permission denied for table. Why?

Returning the new row is a read. supabase-js .insert().select() sends INSERT ... RETURNING, which needs the SELECT grant on the table (and a SELECT policy that lets the caller see the row), not only INSERT.

Why doesn't an identity column need the grant?

Postgres treats the identity sequence as part of the column, so the INSERT privilege on the table covers generating the value. A serial column is just a default that calls nextval on an independent sequence, which has its own privileges.