GUIDESEPISODES
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.

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:

Open the full chapterIncludes code, a note
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.
-- 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.

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 data | Who it lets in |
|---|---|
| RLS on, no policy for that action | Nobody. That’s the red error on insert |
using (true) with no to clause | Everyone, 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 off | Supabase: “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.

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:
-- 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:

Open the full chapterIncludes code, a note
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:

Open the full chapterIncludes code
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:
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 formIf 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:
-- 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:

Open the full chapterIncludes code, a note
// 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”, andpostgres“hasbypassrls.” 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.

Open the full chapterIncludes a table, a note
| What you see | What it means | What to do |
|---|---|---|
| “new row violates row-level security policy” | RLS is on, no policy allows this insert | Add an owner-only insert policy. Never true |
A policy with using (true) on user data | Everyone can read every row | Replace it with (select auth.uid()) = user_id |
with check (true) | Anyone can write any row, for anyone | Owner-only with check |
{public} in a policy’s roles | No to clause, so it runs for visitors too | Add to authenticated |
| Inserts fail after the fix | The app sends no user_id, or the wrong one | Send the signed-in user’s id, or default it to auth.uid() |
| User B can see user A’s rows | A policy still says yes | Back to step 1 |
User B gets [] | The lock works | Ship 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.
- 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.
- 02
A policy with
using (true)orwith check (true)says yes to everyone. Supabase’s own lint calls it a policy that “defeats the purpose of having Row-Level Security enabled.” - 03
Find it in Supabase → Advisors → Security Advisor. It warns about policies with always-true conditions.
- 04
The real lock checks the name on the key:
(select auth.uid()) = user_id, withto authenticated, on select, insert, update and delete. - 05
Your app must send
user_idas the signed-in user (or the column defaults toauth.uid()). Then the error goes away for the right reason. - 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.
- 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.
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.
Find it, lock reads, lock new rows, test as a stranger. Before you ship, check that no policy just says true: your first repository audit is free, and Aster reads your Supabase migrations for tables with RLS off and policies that let everyone in.
Source notebook7 links
- PostgreSQL Docs: Row Security Policies (default deny when no policy exists)
- PostgreSQL Docs: CREATE POLICY (USING rows are silently suppressed, WITH CHECK throws an error)
- Supabase Docs: Database Advisors, lint 0024 permissive RLS policy (“Security policy allows unrestricted access”)
- Supabase Docs: Row Level Security (policy examples, the to clause, bypassrls)
- Supabase Docs: Understanding API keys (publishable vs secret keys)
- Supabase Docs: Database Advisors (Advisors → Security Advisor)
- Supabase Docs: Postgres Roles (dashboard queries run as postgres)

