Security basics
New row violates row-level security policy in Supabase. Now what?
"New row violates row-level security policy" means Supabase refused a write. The fix that clears it in ten seconds also reopens the table to everyone.

In short
- New row violates row-level security policy means your database checked the rules on that table and found none of them allowing the row you were saving. It refused the write and kept the table as it was.
- One message covers three different situations: no rules at all, a rule that is only about reading, or a rule about writing that your row does not satisfy.
- The answer at the top of every search result makes the error stop by allowing every write from anyone. The rule you actually want takes about two more minutes.
Something told you to turn Row Level Security on. The advisor inside your Supabase dashboard, a scan result, a checklist, somebody in a Discord. You turned it on. Now your app cannot save anything at all. Every attempt comes back saying that a new row violates row-level security policy:
new row violates row-level security policy for table "profiles"
Here is the part that guide after guide gets wrong: the answer that clears this error in ten seconds also undoes the thing you just switched on. It is the first result you will find, it works immediately, and it leaves the table exactly where it was before anybody told you to fix it.
What "new row violates row-level security policy" means
Your database was asked to store a row, checked the rules on that table, found none of them allowing that particular row in, and refused.
That is the entire message. Nothing is corrupt and nothing was lost: the row was
never saved, and the rest of the table sits exactly as it did a minute ago. The
wording comes from Postgres, the database engine Supabase runs on, which is why
it reads like machinery rather than like anything your builder wrote. Supabase
passes it through with its number attached, 42501.
What the message will not tell you is which rule was missing. One sentence covers three different situations, and telling you which one you hit would describe your rules to whoever triggered the error:
- The table has Row Level Security switched on and no rules written at all.
- It has a rule, and that rule is only about reading.
- It has a rule about writing, and the row you sent does not satisfy it.
All three print that identical line. The second is the common one in a newly launched app, where a rule was added to get the screens working again and saving never came up.
Why it appeared the moment you turned Row Level Security on
Because switching it on refuses everything until a rule says otherwise, and your own app is part of everything.
This is the sequence almost everybody goes through. You turn the setting on. The screens go blank, because with no rules written the database now refuses to hand over rows to anyone, your app included. You add a rule so the lists come back, usually the first one that a search result or your builder suggests. The screens fill up. And the first time somebody presses save, this error appears, from a table you thought you had already fixed.
Nothing went wrong in that sequence. The rule you added was about reading, and saving is a different question that nobody had asked yet.
A rule has two halves, and only one of them is about reading
A policy is a condition, and where the database applies that condition depends on what you asked it to do.
USING is applied to rows already sitting in the table. It decides which ones
you are allowed to see, change or remove. WITH CHECK is applied to the row you
are trying to create, before it exists anywhere. It decides whether that row is
allowed to come into being at all.
That second one is the part that surprises people, because it is a rule about something that is not there yet.
| Your app asks to | Checked against rows already in the table | Checked against the row being written |
|---|---|---|
read (select) | USING | nothing to check |
save a new row (insert) | nothing to check | WITH CHECK |
change a row (update) | USING | WITH CHECK |
remove a row (delete) | USING | nothing to check |
A policy written FOR SELECT only ever carries the first half, because there is
no new row to check when somebody is reading. So it can never permit a save,
however permissive it looks. That is the whole of the mismatch, and it explains
why your reads recovered and your writes did not.
If you would rather see this from the outside, our free scan asks your live database what a stranger can already read from it, without an account and in about 20 seconds: scan your app.
The fix that makes the error stop, and what it costs
The fastest way to clear this is a rule that allows every write from anyone, which is why it is the top answer nearly everywhere you look.
It usually arrives looking like this:
CREATE POLICY "Enable insert for all users"
ON public.profiles
FOR INSERT
WITH CHECK (true);
Every row satisfies true, so every save is allowed, from your app and from
anybody else holding the key that ships inside it. Turning Row Level Security
back off does the same job more thoroughly.
Both of them work. Both of them leave you where you were before the advisor flagged the table, and neither shows any sign afterwards that something is open, because your app behaves identically in all four combinations of setting and rule. A table can be switched on, carry a valid policy, show a green badge in your dashboard and still hand its rows to a stranger. That is the other half of this problem, and it is worth reading before you paste anything.
The rule that lets your app save, and only your app
Name the person the row belongs to, and compare it to whoever is asking:
CREATE POLICY "Users insert their own rows"
ON public.profiles
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
auth.uid() is whoever is signed in and making the request. user_id is the
column on the row saying who it belongs to. When those two match, the row is
saved. When they do not, or when nobody is signed in, it is refused, and the
person refused sees the same message you have been looking at.
Two details in that rule earn their place. TO authenticated means the rule
applies to signed-in visitors; leave it out and the policy applies to everybody,
strangers included. And your app has to actually send the user_id column,
because a rule comparing auth.uid() to a value that arrived empty can never
match.
When the rule is right and the error still appears
Look at who was signed in before you look at the policy again.
Three explanations cover most of what is left:
Nobody is signed in. auth.uid() comes back empty for a visitor who has not
logged in, so a rule comparing it to an owner column cannot match. This is
normal on a signup form, a waiting list or a contact form, and those need their
own rule describing what a stranger is allowed to add.
The owner column never arrives. Your rule compares auth.uid() to
user_id, and your app sends everything except user_id. The comparison runs
against a blank and fails every time.
The write is going to Storage. File uploads land in Supabase Storage, which
keeps its own policies on storage.objects rather than on your table. A rule
written on profiles has nothing to say about a file.
There is a fourth explanation, and it is the one worth ruling out early: if saving works from your builder's preview and fails from your live site, the two are not using the same key. A secret key ignores every rule you have written, which is what it is for, so an app that only saves while a secret key is in play is an app whose rules have never actually been tested. Which API keys are safe in your frontend covers how to tell one from the other.
What to do today
What to do
- Read the error as a refusal rather than a fault. The write did not happen, the table is unchanged, and nothing needs recovering.
- Check whether the table has any policy about writing at all. A rule written
FOR SELECTfixed your screens and said nothing about saving. - Add a policy
FOR INSERTwhoseWITH CHECKcondition names the row's owner, and add theTOclause you meant. - Confirm your app actually sends the owner column, then try the save again while signed in.
- Leave
WITH CHECK (true)for tables you would be comfortable publishing on a public page. For anything with a person in it, spend the extra two minutes.
Start with the table the error came from, and check every other table you turned the setting on for the same afternoon. The 10-minute security checklist covers what else tends to be left open in a newly launched app, and the Supabase safety guide goes through the rest of what a stranger can reach.
FAQ
What does "new row violates row-level security policy" mean?
It means your database was asked to store a row, checked the rules on that table, and found that none of them allowed that row in. So it refused the write. Nothing was lost and nothing is broken, because the row was never saved and the rest of the table is untouched. The message comes from Postgres, the database engine Supabase runs on, and it carries the code 42501. What it deliberately does not tell you is which rule was missing, because saying so would describe your rules to whoever triggered the error.
I added a policy and reads work, so why does saving still fail?
Because a policy about reading has nothing to say about writing. A rule carries a USING half, which the database applies to rows already in the table, and a WITH CHECK half, which it applies to the row you are trying to create. A policy written FOR SELECT only ever has the first one, since there is no new row to check when you are reading. Your screens fill up again and the first save still fails. Add a second policy FOR INSERT with a WITH CHECK condition.
Should I just turn Row Level Security off to make this go away?
That does clear the error, and it leaves every row in that table readable by anyone who has the key that ships inside your app. Your app works either way, so nothing afterwards tells you which of the two you chose. If the table holds people, orders or messages, the two minutes it takes to write a real rule is the difference between a private table and a public one.
My policy looks right and inserts still fail. What else could it be?
Three things account for most of these. Nobody is signed in, so auth.uid() is empty and a rule comparing it to an owner column can never match, which is normal on a signup or contact form. Or your app never sends the owner column at all, so the rule compares against a value that arrived blank. Or the write is going into Supabase Storage rather than a table, and Storage keeps its own policies on storage.objects.
Does this error mean somebody tried to attack my app?
Almost never. In a newly launched app it is nearly always your own app being refused, because Row Level Security was switched on before a rule existed that covered your own writes. It is worth reading rather than dismissing, though: the same message appears when a request that should not be writing to that table is refused, and you cannot tell the two apart from the message alone.