Skip to content
RowShield

Help / Troubleshooting

Fixing "new row violates row-level security policy for table X"

All plansLast reviewed 2026-09-07

This error means row level security is enabled on the table named in the message, and no policy passed for the row you tried to write, under the role the request actually ran as. For an insert, no policy's WITH CHECK expression returned true for the new row; for an update, either the existing row failed a policy's USING clause or the changed row failed its WITH CHECK. Postgres does not say which policy came closest or why — only that none did. The one-line first check: repeat the same insert or update as the exact role the request used — anon, authenticated, or a specific authenticated user — and compare the values you are writing against what the table's policies actually require.

The rest of this page covers reading the rest of the message, the four causes that account for almost every case, how to list the policies that actually exist on the table, the shape of a policy that fixes this correctly, two fixes that look right but are not, and how to test as the same role your app uses.

Reading the error

The message is deliberately terse: new row violates row-level security policy for table "your_table", raised with SQLSTATE 42501. It names the table but not the operation, the role, or which column tripped the check, so you have to supply that context yourself from what your code just did.

Operation: the same wording covers both INSERT and UPDATE, because both are enforced by a WITH CHECK evaluation against the proposed row. You already know which one you ran; Postgres is not distinguishing them for you here.

Role: check which credential the request used. With the anon key and no session, the request ran as anon. With a signed-in user's access token attached, it ran as authenticated, evaluated against that user's auth.uid(). If the request used the service_role key, this error cannot happen at all — that role bypasses row level security entirely, so seeing this error is itself proof the request did not use it.

The four usual causes

Four situations account for almost every case:

No insert policy exists at all. A table can have a working select policy and still reject every insert, because each command is governed by its own policies — a policy written for select says nothing about insert, and the default with no matching policy is deny.

The WITH CHECK references auth.uid() but the request is anonymous. auth.uid() returns null for an unauthenticated request, so a check such as user_id = auth.uid() compares a real column value to null and is never true — every anonymous insert fails by construction, whether or not that was the intent.

The policy exists but targets the wrong role or command. A policy created to authenticated does nothing for anon, and the reverse holds too; a policy scoped for update does not cover insert even if its predicate would otherwise allow the row. It is easy to write a policy that looks right and simply does not apply to the request being made.

A column the client sets does not match auth.uid(). If the app lets the client populate user_id, or a similar column, directly rather than defaulting it server-side, a mismatched value — stale local state, a different user's id, a value typed by hand while testing — fails the check even though the policy itself is correct.

See the policies that actually exist

Do not reconstruct the policy set from memory or from migration files — what is live on the table is what matters. Query the catalogue directly:

cmd shows which command a policy governs (insert, update, select, delete, or all); roles shows which roles it applies to; with_check is the expression this error is actually about. An empty result for your operation and role means no policy is filling that gap at all — you are looking at cause one.

SELECT
  policyname,
  permissive,
  roles,
  cmd,
  qual AS using_expression,
  with_check
FROM pg_policies
WHERE schemaname = 'public'
  AND tablename = 'your_table';

The correct fix, and two that make it worse

A working policy ties the row explicitly to the caller:

This admits a row only where user_id in the insert matches the caller's own id, and only for the authenticated role — it does not open the table to anon just by existing.

Two fixes reach for a hammer instead, and both are wrong. The first is with check (true). This narrows nothing: it admits any row from any caller, and because permissive policies combine with OR, one tautological policy overrides every narrower policy sitting next to it — the table becomes writable by anyone who can reach it. The second is switching the client to the service_role key so the error goes away. It does, because that role bypasses row level security entirely — which is exactly why it must never ship to a browser or any code a user can inspect. A service_role key in client-side code hands out full read and write access to every table it can reach, policies included, to anyone who opens devtools.

CREATE POLICY "authenticated users insert their own rows"
ON public.your_table
FOR INSERT
TO authenticated
WITH CHECK (user_id = (SELECT auth.uid()));

Test as the same role your app uses

A fix tested as the table owner, or with the service_role key, proves nothing, because neither is subject to row level security. Test as the role and identity the app actually sends.

In SQL, assume the role and set the claim the policy reads, run the insert inside a transaction, then roll it back rather than committing test data: set local role authenticated; then set the JWT claim that auth.uid() reads — the Supabase documentation shows the current set_config call for request.jwt.claims — then the insert statement itself.

In application tests, sign in as a real test user through the normal auth flow and run the write through the same client the app uses, so the access token — not an assumed role — is what actually gets checked.

Cover both directions: confirm the write that should succeed does, and confirm a row belonging to a different user's id still gets rejected. A policy that only works in one direction is easy to miss until it is the direction that mattered.

What RowShield checks here

The free probe finds tables that answer to the anon key and any service_role key sitting in a public bundle — both relevant to this error, since seeing it at all means the request was not using service_role. The connected-project audit's nine rules include checks for row level security disabled entirely and for policies whose check evaluates to true regardless of the row. Neither one tells you why one specific insert failed; that diagnosis is what this page is for.

Related questions

Does this error mean my data is exposed?
No — the opposite. It means a policy blocked a write, which is row level security doing its job. Exposure is when a read or write goes through that should not, not when a legitimate one is rejected. If the write should have been allowed, the policy is too strict rather than too loose.
Why does it work with the service_role key?
Because the service_role key bypasses row level security entirely — it is not subject to the policy that just rejected your insert, or to any other policy on any table. That is expected in trusted server-side code, and exactly why the key must never reach a browser: anyone holding it can write past every policy on every table.
Why does it fail only for anonymous users?
Usually because the WITH CHECK references auth.uid(), which returns null for an anonymous request. A check comparing a column to null is never true, so every anonymous insert fails regardless of the values it sends. If anonymous inserts should be allowed at all, the policy needs a predicate that accounts for that case explicitly, rather than assuming a signed-in caller.

Did this answer your question? If not, tell us what is missing — article corrections go straight to the person who maintains it.

RowShield checks 9 rule classes continuously. This article describes shipped behaviour only.