← All guides

"new row violates row-level security policy": How to Fix the Supabase Error Safely

Updated 2026-10-29

Short answer: Supabase shows new row violates row-level security policy for table "x" (Postgres error 42501) when row level security is enabled on a table and no policy allows the row you are trying to write. Fix it by adding an INSERT policy (and an UPDATE policy for upserts) whose WITH CHECK condition matches the row, usually auth.uid() = user_id, and by making sure the request is actually sent as a logged-in user. Don't fix it by disabling RLS or by using the service_role key in the browser.

This is one of the most common errors in apps built with Lovable, Bolt, v0, Cursor and Replit on top of Supabase. It usually shows up in the console like this:

{
  "code": "42501",
  "message": "new row violates row-level security policy for table \"todos\""
}

The good news: the error means RLS is doing its job. The goal is to allow exactly the writes you want and nothing more.

Why the error happens

When RLS is enabled on a table, Postgres denies every row by default. Each operation needs a policy that allows it:

"New row violates" means a WITH CHECK condition failed, or there's no policy at all for that operation. The usual causes, roughly in order of how often we see them:

  1. No INSERT policy exists. The table has a SELECT policy, so reads work, but writes fail.
  2. The user isn't logged in when the request is sent. The request runs as the anon role, auth.uid() is null, and null = user_id is never true.
  3. The user_id column isn't set, or it's set to something other than the current user's id.
  4. An upsert needs an UPDATE policy too. upsert() can turn into an update, so Postgres checks both.
  5. insert().select() needs a SELECT policy. Returning the inserted row requires that the user can read it.
  6. The policy targets the wrong role, for example TO anon when the user is authenticated.

The correct fix

For a typical "users own their rows" table, add these policies in the SQL editor:

alter table public.todos enable row level security;

create policy "Users can read their own todos"
  on public.todos for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "Users can create their own todos"
  on public.todos for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

create policy "Users can update their own todos"
  on public.todos for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "Users can delete their own todos"
  on public.todos for delete
  to authenticated
  using ((select auth.uid()) = user_id);

Wrapping auth.uid() in (select ...) lets Postgres evaluate it once per query instead of once per row, which matters on bigger tables. Add an index on user_id too.

Then make the column default to the current user so the client can't forget it, and can't set it to someone else:

alter table public.todos
  alter column user_id set default auth.uid();

Now the insert from your frontend can leave user_id out entirely:

const { data, error } = await supabase
  .from("todos")
  .insert({ title: "Ship it" })
  .select()
  .single();

The WITH CHECK policy still rejects a request that tries to pass someone else's user_id, so the default is a convenience, not the security boundary.

Check that the request is authenticated

If your policies look right and the error persists, the request is probably anonymous. Log the session right before the insert:

const { data: { user } } = await supabase.auth.getUser();
console.log("inserting as", user?.id ?? "ANON");

Common reasons it's anonymous:

// Supabase Edge Function: forward the caller's JWT so RLS sees the user
const supabase = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_ANON_KEY")!,
  { global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
);

Fixes to avoid

AI assistants and forum answers often suggest one of these. Each makes the error go away by removing the protection.

Disabling RLS

-- don't do this on any table the public API can reach
alter table public.todos disable row level security;

With RLS off, anyone holding your anon key (which is in your frontend bundle, by design) can read, change and delete every row through the REST API. This is the root cause of most Supabase data leaks we find in scans. See the Supabase RLS checklist for how to audit every table.

A policy that allows everything

-- "fixes" the error, but any visitor can insert any row
create policy "allow all" on public.todos
  for all using (true) with check (true);

using (true) on a table with private data is the same as having no RLS. It's only reasonable for genuinely public, append-only data, and even then you want a rate limit.

Using the service_role key in the browser

The service_role key bypasses RLS completely. If it's in a NEXT_PUBLIC_ or VITE_ variable, anyone can copy it from your JavaScript and get full database access. If that has already happened, rotate the key now and read about exposed API keys in NEXT_PUBLIC variables.

The service role key is fine on the server for trusted jobs (webhooks, admin tasks), as long as that code checks who the caller is first.

Special cases

Profiles created on signup. If the client inserts into profiles right after signUp(), the user may not have a session yet when email confirmation is on. Create the profile with a database trigger on auth.users instead, using a security definer function owned by a trusted role.

Team or organization data. Replace auth.uid() = user_id with a membership check:

create policy "Members can add projects"
  on public.projects for insert
  to authenticated
  with check (
    exists (
      select 1 from public.memberships m
      where m.org_id = projects.org_id
        and m.user_id = (select auth.uid())
    )
  );

Make sure users can't insert their own rows into memberships, or they can join any org.

Storage uploads. The same error appears for Supabase Storage as new row violates row-level security policy because Storage stores file metadata in the storage.objects table. You need an INSERT policy on storage.objects for that bucket. See Supabase storage bucket security.

How to test your policies

After fixing the error, confirm the policies block what they should:

  1. Log in as user A, insert a row, and confirm it works.
  2. Try to insert a row with user B's user_id and confirm it fails with 42501.
  3. Log out and try to read the table with just the anon key:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/todos?select=*" \
  -H "apikey: YOUR_ANON_KEY"

You should get [] back, not other people's rows. Supabase's Security Advisor in the dashboard also flags tables with RLS disabled and policies that look too permissive.

Get an automated check

PreflightPR reviews AI-built apps for tables without RLS, permissive policies, service role keys in frontend code, API routes without auth checks and other launch blockers. Run a free scan on a public repo for a launch score and a prioritized fix list, or install the PreflightPR GitHub App to check every pull request before it merges. For the full list of things to check before launch, see the pre-launch security checklist for AI-built apps.

Check your repo in about a minute

Paste a public GitHub URL and get a free launch score.

Run a free scan

More guides