All guidesSupabase RLS and keys
On this page

Supabase error

Supabase

new row violates row-level security policy

By Ohad Krispin, founder of VibeRaven · LinkedIn

"new row violates row-level security policy for table" (code 42501) means Postgres refused an insert or update because no policy for that role passed the row: row level security is on, and either the table has no INSERT or UPDATE policy for the role making the request, or the policy's WITH CHECK returned false or null for the row the app sent. Fix it with a policy for that command and role whose WITH CHECK matches the row, such as with check ((select auth.uid()) = user_id), and make sure the request carries the user's session. If you chain .select() after .insert(), the user also needs a SELECT policy that lets them read the new row, or the same error comes back. Checked against the PostgreSQL source and a PostgreSQL 16 run on 2026-10-25.

psql on PostgreSQL 16.15 as the authenticated role: an insert with no policy, an insert naming another user, and an insert with returning but no select policy all fail with new row violates row-level security policy.
psql on PostgreSQL 16.15 as the authenticated role: an insert with no policy, an insert naming another user, and an insert with returning but no select policy all fail with new row violates row-level security policy.

Repository check

Check this in your repo

Find tables your client writes to with no policy

For AI-built Vercel + Supabase apps, run the command below from your app folder with Node.js 20 or newer. It reads the SQL migrations in your repo and lists each public table that has row level security on and no policies at all as rls_no_policies: the browser client cannot write to such a table, so every insert from the app fails with this error. A public table that no migration enables row level security on comes out as rls_disabled, which is also what the wrong fix looks like: turning row level security off to make the error go away.

It is a repository check, not a live database test: it does not run your policies, so a WITH CHECK that compares the wrong column, a request sent without the user's session, or a policy created only in the dashboard is not reported. It is advice, not a gate, and it exits with code 1 when it finds a blocker.

npx -y viberaven@1.6.12 check

What it printed on a fixture repo

On 2026-10-25 we ran it on a small Next.js fixture repo made for this page, not a customer repo. One migration creates public.messages with row level security on and no policy, which a client component inserts into, and public.reactions with no row level security at all. The output is below, with the temp folder shortened to ~/chat-app. The table without row level security is a blocker, so the exit code was 1.

$ npx -y viberaven@1.6.12 check

viberaven check · ~/chat-app

🔴 No RLS policy proof in migrations  (rls_disabled)
   1 public table without row level security: public.reactions (supabase/migrations/20261101000000_messages.sql:9). The Data API does not enforce row restrictions for roles and operations allowed by the table's grants. Verify those grants to determine who can read or change rows.
⚪ Table with RLS on and no policies (service role only)  (rls_no_policies)
   supabase/migrations/20261101000000_messages.sql:7: public.messages has row level security on and no policies, so it is locked to the service role: the browser client gets no rows back and cannot write. Fine for server-only tables; add policies if the app reads it from the frontend.
⚪ No error monitoring detected  (missing_monitoring)
   No Sentry/PostHog (or similar) instrumentation was found. Production errors will only surface when users complain.

Verdict: ❌ 1 blocker, 0 warnings · score 70
Fix: viberaven fix · Details: ~/chat-app/.viberaven/agent-tasklist.md

Where the message comes from, and code 42501

Postgres raises it in ExecWithCheckOptions (src/backend/executor/execMain.c) with SQLSTATE 42501, insufficient_privilege, and the message always names the table. When no policy applies to the command at all, Postgres adds an always-false check, a default-deny policy, so the error is the same as for a policy that rejected the row. Only a restrictive policy gets its own name in the message, as new row violates row-level security policy "name" for table "messages". supabase-js does not throw: the call returns this in error, with code 42501.

Insert works, but insert with .select() fails

supabase-js returns the inserted row only when you chain .select(), and Postgres then applies the table's SELECT policies to it: the PostgreSQL CREATE POLICY reference says a returned row that fails them raises an error and is never silently dropped. In our PostgreSQL 16.15 run, a row that inserted fine failed with this error once the insert had returning id and no SELECT policy let the user read it, and went through after a select policy for the owner. Add that SELECT policy, or drop .select() if the app does not need the row back.

Upsert needs UPDATE and SELECT policies too

An upsert is INSERT ... ON CONFLICT DO UPDATE. The CREATE POLICY reference says the rows proposed for insertion are checked against the SELECT policies, and when the update path is taken, the existing row is checked against the UPDATE policy's USING and the new row against its WITH CHECK. In our run, an upsert onto an existing row with only an insert policy failed with this error. Write INSERT, SELECT and UPDATE policies for a table the app upserts into.

It works for you and fails for a signed-out visitor

Without a session, supabase-js runs as anon. A policy written to authenticated does not apply to anon, so no policy applies and the insert fails; auth.uid() is null too, and a check like (select auth.uid()) = user_id is then null, which counts as a failed check. In our run the same insert as anon failed with this error.

If the client reports no session even though the user signed in, see why supabase-js says Auth session missing.

permission denied for table is a different error

Both carry code 42501. permission denied for table means the role has no grant for the command, checked before any policy runs; this error means the grant is there and a policy rejected the row. For the grant error, see the missing grant fix. An upload to Supabase Storage that fails with this text needs a policy on storage.objects instead: the Storage upload policy fix.

Checklist

How to fix it, then check the fix

  1. 01Note the table the message names and the command that failed: insert, update or upsert, with or without .select().
  2. 02
    List that table's policies with their commands, roles and clauses:
    select policyname, cmd, roles, qual, with_check
    from pg_policies
    where schemaname = 'public' and tablename = 'messages';
  3. 03Find the role the request ran as. Signed out, supabase-js runs as anon; with a session, as authenticated.
  4. 04
    Add the policy for that command and role in a migration. For rows that belong to the user who writes them:
    create policy "Users can insert their own messages"
    on public.messages for insert
    to authenticated
    with check ((select auth.uid()) = user_id);
  5. 05Send the column the check compares, here user_id set to the signed-in user's id. If the app reads the row back or upserts, add the SELECT and UPDATE policies those need.
  6. 06Apply the migration to the project that fails, for example with supabase db push, then repeat the request as the same user. Do not silence the error by turning row level security off, writing with check (true), or putting the secret key in the browser.

Related

Sources

Official docs behind this checklist

FAQ

FAQ for builders before launch

Why does the same insert work in the SQL editor?

The SQL editor runs as postgres, which owns the tables, and PostgreSQL says a table's owner normally bypasses its row security. The app runs as anon or authenticated, so test as that role or from the app itself.

Does the service role key make the error go away?

It bypasses row level security, so it must stay on the server, and Supabase says it bypasses RLS only when the request carries no user access token: an SSR client created with it that picks up the user session from cookies still gets this error.