FREE1 free audit every month. No card, every finding included.

GUIDESEPISODES

EP 19 · TRUEAster × Claude

Supabase policy says true? Anyone can read every row

Your sign-up threw a red error: “new row violates row-level security policy”. Your AI made it go away with one tiny policy that says true. The error was the lock working, and true means yes, to everyone. Here is how to find that policy and put a real lock back, in four steps checked against the Supabase and PostgreSQL docs.

  • 11 min read
  • Beginner
  • Oct 22, 2026
Voxel episode cover tagged EP. 19 on a dusk-indigo background with faint blurred code: the headline reads ONE KEY. in cream and EVERY BOX. in lime; in a post office hall at sunset, Aster, in a black t-shirt with the lime lock logo and his closed silver laptop at his side, holds his head in shock, while Claude, a terracotta-orange clay block with two black eye slits, two small white fangs, a red medallion and a black vampire cape, sits on a green marble counter and holds up a big brass key with a paper tag that reads TRUE; behind them a wall of brass P.O. boxes hangs open and envelopes fly out.

01WHAT HAPPENED

The red error, the one-line “fix”, the open boxes

Why it mattersYour AI is rewarded for making the error go away. true does that, and opens every row.

Here’s the story. Your app lets people sign up and write private notes. You built it on Supabase with an AI agent. One night, sign-up breaks with a red error:

Illustration: in an after-hours post office hall at sunset, Aster, in a black t-shirt with the lime lock logo, leans on a bottle-green marble counter and stares at his open silver laptop, whose screen shows a red banner that reads ERROR, while Claude, a terracotta-orange clay block with two black eye slits, two small white fangs, a red medallion and a black vampire cape, scratches his head behind the counter with a ring of brass keys; a wall of small brass P.O. boxes fills the background.
Sunset at the post office. The sign-up just threw a red error.
Open the full chapterIncludes code, a note
error
new row violates row-level security policy for table "notes"

PostgreSQL’s own message (error code 42501). Supabase passes it straight to your app.

You paste it to your AI. It writes one tiny policy, the error goes away, and sign-up works again.

supabase/migrations/fix_signup.sql
-- The one-line "fix": the error is gone, and so is the lock
create policy "Allow all" on notes
for all
using (true)
with check (true);

This is the bug, not the fix. true is a condition every row passes, for every person.

Then a stranger reads every user’s private notes. Nobody broke in. The policy let them.

Why the error appeared in the first place

Row Level Security (RLS) is on for notes, and there was no policy that allowed the insert yet. PostgreSQL’s rule: “If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified.” So the database said no. That is RLS doing its job.

02WHAT TRUE MEANS

One master key that opens every box

Why it mattersRLS is only as strong as its policies. A policy that says true is no lock at all.

Picture your table as a wall of P.O. boxes. Each row is one user’s box. A policy is the little lock on each box: it decides who may open it.

Illustration: a big brass master key with a paper tag that reads TRUE hangs on a hook on a wall of brass P.O. boxes, and every small box door has swung open at once, with white envelopes flying out; Aster raises one hand in shock, his closed silver laptop under his arm, while Claude, in his vampire cape, sits on the green marble counter holding his ring of keys.
True means yes. To everyone. Every box at once.
Open the full chapterIncludes a table, a note

A policy’s using part is a question asked about every row: “may this person see this row?” true answers yes for every row, for every person. Supabase’s linter puts it plainly: always-true expressions “effectively bypass the security that RLS is meant to provide.”

Who each policy lets in

The policy on your user dataWho it lets in
RLS on, no policy for that actionNobody. That’s the red error on insert
using (true) with no to clauseEveryone, even people who never signed in. The policy applies to every role by default
to authenticated using (true)Every signed-in user, and anyone can sign up
to authenticated using ( (select auth.uid()) = user_id )Only the owner of each row
RLS turned offSupabase: “readable and writable by any role with a grant on it”

This guide is about RLS that is on, with a policy that says yes to everyone. If your tables have RLS off, start with the Supabase RLS guide.

03STEP 1 · FIND IT

Let the Security Advisor point at the bad policy

Why it mattersYou can’t fix a policy you haven’t found. The Advisor finds it in seconds.

In your Supabase dashboard, open Advisors → Security Advisor. It runs Supabase’s security checks on your database.

Illustration: at the green marble counter, Aster, in a black t-shirt with the lime lock logo, points at his open silver laptop, whose screen highlights one red row that reads TRUE, while Claude, in his vampire cape and holding a ring of keys, looks through a big brass magnifying glass at the wall of P.O. boxes, where one tiny lock glows red.
Suspect is… a policy that says yes.
Open the full chapterIncludes code, a note

The one you are looking for is lint 0024, “Security policy allows unrestricted access”. It warns when “An RLS policy uses an always-true condition like USING (true)”. It flags using (true), with check (true), and the same trick written as 1=1.

Or list every policy yourself

Open the SQL Editor and run this. It shows each policy’s table, action, role and both conditions:

SQL Editor
-- Every policy in the public schema: who it's for and what it checks
select tablename, policyname, cmd, roles, qual as using_expr, with_check
from pg_policies
where schemaname = 'public'
order by tablename, cmd;

Read the using_expr and with_check columns. true on a table of user data is the bug. {public} in roles means the policy has no to clause.

04STEP 2 · LOCK READS

Every box checks the name on the key

Why it mattersA real policy asks one question about every row: is this yours?

Replace the open policy with one that compares the signed-in user with the row’s owner. This is Supabase’s own pattern:

Illustration: Aster, in a black t-shirt with the lime lock logo and his closed silver laptop at his side, turns a small silver key with a blank name tag in one brass P.O. box, which opens to show his own envelope, while every other box on the wall keeps its small lime padlock shut; Claude, in his vampire cape, stands beside him holding a ring of keys with a tag.
Your key opens your box. Only yours.
Open the full chapterIncludes code, a note
supabase/migrations/notes_owner_policies.sql
drop policy "Allow all" on notes;

create policy "Owners read their notes" on notes
for select to authenticated
using ( (select auth.uid()) = user_id );

auth.uid() is the id of the signed-in user. user_id is the column that says who owns the note.

Why “to authenticated”

Supabase’s docs: “Always name the role a policy applies to, using the to clause.” It “prevents the policy … from running for any anon users.” Visitors who never signed in don’t even get to the question.

Why the extra “select” around auth.uid()

It’s for speed, not safety. Supabase’s docs say wrapping the function lets Postgres “‘cache’ the results per-statement.” The check runs once per query instead of once per row.

05STEP 3 · LOCK NEW ROWS

Inserts and edits need the same check

Why it mattersThe error goes away because the row is yours, not because the lock is gone.

Reading is one action. Adding, editing and deleting are three more, and each needs its own policy. For new rows, the condition goes in with check:

Illustration: at the green marble counter, Aster, in a black t-shirt with the lime lock logo and his silver laptop open beside him, hands a white envelope with a name tag to Claude, in his vampire cape, who inspects it above a brass mail slot glowing lime, with a small brass plate above the slot that reads CHECK.
New mail gets checked before it goes in.
Open the full chapterIncludes code
supabase/migrations/notes_owner_policies.sql
create policy "Owners add their notes" on notes
for insert to authenticated
with check ( (select auth.uid()) = user_id );

create policy "Owners edit their notes" on notes
for update to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

create policy "Owners delete their notes" on notes
for delete to authenticated
using ( (select auth.uid()) = user_id );

Insert checks the new row. Update checks the row before (using) and after (with check), so nobody can hand their note to someone else. Delete checks the row it removes.

PostgreSQL is strict here. Its docs: “An error will be thrown if the expression evaluates to false or null for any of the records inserted.” So the red error comes back if your app sends the wrong owner. That’s good.

Make your app send the right owner

The insert passes when user_id is the signed-in user. Set it from the session, never from a form field:

app/notes/new.js
const { data: { user } } = await supabase.auth.getUser()

const { error } = await supabase
  .from('notes')
  .insert({ body: noteText, user_id: user.id })   // the name on the key, not a value from a form

If user_id is missing or belongs to someone else, with check rejects the row.

Or let the database fill it in, so the app can leave it out:

supabase/migrations/notes_default_owner.sql
-- Optional: fill user_id with the signed-in user when the app leaves it out
alter table notes alter column user_id set default auth.uid();

The insert policy still checks it. The default only saves your app from sending it.

Now sign-up works again, for the right reason: the database checked the name on the key and said yes.

06STEP 4 · TEST AS A STRANGER

An empty list means the lock works

Why it mattersYou only know a lock works when a stranger’s key fails.

Make two test users, A and B. Sign in as A and write a note. Then sign in as B and try to read A’s notes:

Illustration: at night in the post office hall, Aster, in a black t-shirt with the lime lock logo, opens one brass P.O. box with a plain silver key, finds it completely empty and gives a thumbs up, while Claude, in his vampire cape, stands beside him with his ring of keys; every other box on the wall has a small lime padlock, and Aster’s silver laptop sits open on the counter behind them.
A stranger’s key opens nothing. Empty means locked.
Open the full chapterIncludes code, a note
test in the browser console or a script
// Signed in as test user B. USER_A_ID = the id of test user A.
const { data, error } = await supabase
  .from('notes')
  .select('*')
  .eq('user_id', USER_A_ID)

console.log(data)   // [] = the lock works. Any of A's notes here = a policy still says yes.

Use your normal app client with the publishable key, signed in as B.

Why you get an empty list, not an error

This surprises everyone. For reads, PostgreSQL doesn’t complain about rows you may not see: “Typically, such rows are silently suppressed; no error is reported.” So [] is the answer you want. Any note from A in that list means a policy still says yes.

Test it the way a stranger would

  • Use the publishable (anon) key and a real signed-in test user, like your app does.
  • Never test with the secret (service_role) key. Supabase: it “bypasses every Row Level Security policy you have”, so every test passes.
  • Don’t test in the SQL Editor. Supabase: “Queries you run in the Dashboard execute as postgres”, and postgres “has bypassrls.” A query there sees every row whatever your policies say.
  • Also try it signed out. With to authenticated, a visitor who never signed in gets an empty list too.

07LOCKED

The whole fix on one page

Why it mattersFind it, lock reads, lock new rows, test as a stranger.

Keep this next to your Supabase dashboard.

Illustration: at night, every brass P.O. box on the wall is shut with a small glowing lime padlock; behind the green marble counter, Claude, in his vampire cape, drops the big brass master key into a wooden drawer, while Aster sits on a stool with his silver laptop open and a paper coffee cup beside it, smiling and giving a thumbs up.
The master key goes in the drawer. Every box has its own lock again.
Open the full chapterIncludes a table, a note
What you seeWhat it meansWhat to do
“new row violates row-level security policy”RLS is on, no policy allows this insertAdd an owner-only insert policy. Never true
A policy with using (true) on user dataEveryone can read every rowReplace it with (select auth.uid()) = user_id
with check (true)Anyone can write any row, for anyoneOwner-only with check
{public} in a policy’s rolesNo to clause, so it runs for visitors tooAdd to authenticated
Inserts fail after the fixThe app sends no user_id, or the wrong oneSend the signed-in user’s id, or default it to auth.uid()
User B can see user A’s rowsA policy still says yesBack to step 1
User B gets []The lock worksShip it

Then paste the prompt from the top of this page into Claude Code. It lists your policies, flags any that just say true, rewrites them owner-only, and shows you the SQL before it runs anything.

THE QUICK READ

What to take away.
In one minute.

  1. 01

    “new row violates row-level security policy” is not a bug. RLS is on and no policy allows that insert yet. The error is the lock working.

  2. 02

    A policy with using (true) or with check (true) says yes to everyone. Supabase’s own lint calls it a policy that “defeats the purpose of having Row-Level Security enabled.”

  3. 03

    Find it in Supabase → Advisors → Security Advisor. It warns about policies with always-true conditions.

  4. 04

    The real lock checks the name on the key: (select auth.uid()) = user_id, with to authenticated, on select, insert, update and delete.

  5. 05

    Your app must send user_id as the signed-in user (or the column defaults to auth.uid()). Then the error goes away for the right reason.

  6. 06

    Test as a stranger. With RLS, rows you may not see are filtered out without an error, so an empty list means the lock works.

  7. 07

    The publishable (anon) key in your browser is fine when real policies are on. The secret (service_role) key skips every policy: server only.

THE FIX, IN ONE PASTE

Paste this. Find every policy that says yes to everyone.

Paste this into Claude Code, Cursor or any coding agent. It lists your policies, flags the ones that let everyone in, and shows you the SQL before it runs anything. It never turns RLS off.

agent-prompt.md
Check my Supabase row-level security before I ship. For every table in the public schema that holds user data:
1. List each policy: its command (select/insert/update/delete), its role, and its USING and WITH CHECK expressions.
2. Flag any policy whose condition is just true, any policy without "to authenticated" on user data, and any table with RLS turned off.
3. Rewrite those as owner-only policies: select and delete using ((select auth.uid()) = user_id); insert with check ((select auth.uid()) = user_id); update with both. Keep "to authenticated".
4. Check that my app's inserts send user_id as the signed-in user (or set its default to auth.uid()).
5. Give me a test: sign in as a second test user and show that reading the first user's rows returns an empty list.
Do not turn RLS off, do not use the service_role key in the browser, and show me the SQL before you run it.

Paste it into the agent you already build with

  • Claude
  • ChatGPT
  • Gemini
  • Cursor
  • Copilot

QUESTIONS PEOPLE ASK

Good questions.
Short answers.

What does “new row violates row-level security policy for table” mean?

Row Level Security is on for that table and no policy allows this insert or update. PostgreSQL denies by default when no policy exists. The fix is an owner-only policy with with check ( (select auth.uid()) = user_id ), not a weaker one.

Is a Supabase policy with using (true) ever OK?

Only for data that is meant to be public to everyone, for reading only, like published blog posts. Never on user data and never for insert, update or delete. Supabase’s Security Advisor still warns about it, so you know where they are.

Is it safe to have the Supabase anon or publishable key in my frontend?

Yes, when RLS is on and your policies are real. Supabase calls the publishable key “Safe to expose online” and says RLS decides what it can reach. The secret (service_role) key is different: it skips every policy, so it stays on the server.

Why does my query return an empty list instead of an error?

That is how RLS works for reads. Rows a policy doesn’t allow are filtered out without an error. In PostgreSQL’s words, they are “silently suppressed”. For a stranger, an empty list means the lock works.

Can I turn RLS off to get rid of the error?

No. The error goes away because the lock is gone. Supabase: a table in an exposed schema without RLS “is readable and writable by any role with a grant on it.” Keep RLS on. Write owner-only policies.

Why add “to authenticated” to a policy?

Without a to clause, a policy applies to every role, including visitors who never signed in. With to authenticated, Supabase says the policy stops “at the to authenticated step” for anon users.

Why wrap auth.uid() in (select …)?

For speed. Supabase’s docs say it lets Postgres run the function once per query and cache the result, instead of once per row. The rule it checks is the same.

Does Aster check my Supabase policies?

Aster reads your GitHub repository, including your Supabase migrations, and flags tables with RLS off and policies that let everyone in or any signed-in user. It doesn’t change your database, and it can’t see policies you made only in the dashboard, so run the Security Advisor and the stranger test too.

Done with the guide?
Let Aster check the rest.

1 free audit every month. Aster reads your code, env files, database rules and packages. You get each finding in plain English, with the file and line.

Aster

CHECKING

YOUR GUIDE TO ASTER

Hi, I’m Aster.
Ask me what Aster checks.

I can’t see your code from here. Please don’t paste secrets or private code.

Prepared answers are always available.