Home / RLS / Fix: new row violates row-level security policy

Supabase RLS · Error fix

Fix: new row violates row-level security policy

Postgres 15 · Supabase✓ code tested against a real database
TL;DR

Your INSERT is blocked because the new row fails the table's WITH CHECK — usually because there's no INSERT policy, or the row's user_id doesn't equal auth.uid(). Add an INSERT policy with with check (user_id = (select auth.uid())) and make sure the row carries the signed-in user's id.

01 Why you're seeing this

Postgres throws new row violates row-level security policy for table "…" when Row-Level Security is on and the row you're writing isn't allowed by any policy's WITH CHECK expression. RLS on with no matching policy means deny — the safe default doing its job, not a bug. It fires on INSERT and on UPDATE (when the updated row would move outside what you're allowed to write).

CAUSE 1 · most common

There's no INSERT policy at all

You enabled RLS and added a SELECT policy, but never one for INSERT. Reads work, writes are denied — RLS is default-deny per operation.

CAUSE 2

The row's user_id doesn't match auth.uid()

You have with check (user_id = auth.uid()), but the client inserted the row without setting user_id (so it's null), or set a different id. The check evaluates false → violation.

CAUSE 3

The request isn't authenticated

If the call runs with the anon key and no signed-in session, auth.uid() is null, so user_id = auth.uid() can never be true.

02 The fix

Give the table an INSERT policy that lets a signed-in user write their own rows, and make sure the row carries their id.

migration.sql

-- 1. RLS on (safe to run if it already is)
alter table notes enable row level security;

-- 2. a signed-in user may insert rows that belong to them
create policy "owner inserts own notes" on notes for insert
  with check (user_id = (select auth.uid()));

Let the database fill the id so the client can't get it wrong:

migration.sql

alter table notes
  alter column user_id set default (select auth.uid());

insert.ts

// with the default above, just don't send user_id
await supabase.from('notes').insert({ title });

// or set it explicitly from the signed-in user
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('notes').insert({ title, user_id: user.id });

If cause 3 is yours, the fix is upstream: create the Supabase client with the user's session (their JWT), not the bare anon key — on the server, forward the request cookies so auth.uid() resolves.

03 The gotcha — USING vs WITH CHECK

An INSERT policy with only USING and no WITH CHECK doesn't permit the write — that throws this exact violation. For UPDATE you usually need both: USING to pick the row, WITH CHECK so the user can't edit it into someone else's.

04 Prove it — don't hope

A policy you didn't test is a policy you don't have. Sign in as tenant A, confirm you can write your own row, and confirm you can't write one for tenant B.

signed-in A inserts a row for A  → succeeds
A inserts a row with user_id = B  → violates RLS (blocked, as it should)
anon (no session) inserts anything  → violates RLS

Run that against a real Postgres in CI on every migration, so a table that ships without a correct write policy turns the build red before production.

◆ 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 on the pull request instead of in prod.

npx airlock-rls

Or start from nextjs-supabase-starter — auth + a table with RLS + an isolation test, so a fresh table is safe by default.

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

Does this error mean my table is exposed?

No — the opposite. RLS is on and doing its job (deny by default). The risk is when RLS is off: then there's no error and the table is public. This error means the guardrail is working; you just need a policy that permits the legitimate write.

Why does the insert work in the SQL editor but fail from my app?

The SQL editor runs as a privileged role that bypasses RLS. Your app runs as authenticated or anon, which RLS applies to. Always test writes the way your app makes them — signed in, through the client.

Can I just disable RLS to make it go away?

You can, and you'll have made the table public to anyone with your anon key — the #1 Supabase leak. Keep RLS on and add the correct policy instead.