"New row violates row-level security policy" when your app saves to Supabase
You built an app with Lovable, Bolt, v0, or Cursor on top of Supabase. Reading data works, but the moment the app tries to save something, a form submit, a new record, an edit, the request fails with a 403 (or 401) and the message new row violates row-level security policy for table "something". This is not a bug in your code. It is a missing or mismatched security rule, and it is fixable in a few minutes without weakening anything. (If your problem is the opposite, saves work but reads come back empty, see the data not showing guide.)
What Row Level Security actually is
Row Level Security (RLS) is a per-table firewall for rows. When it is on, every insert, update, delete, and select must be explicitly allowed by a policy, a small rule that says who may do what. No matching policy means denied, that is the default. So a table with RLS enabled and zero policies blocks everything, and that is by design: your app talks to Supabase using the anon key, which ships inside your frontend and is public by definition, so RLS is the only thing standing between the open internet and your data. The fix is never to remove RLS. It is to write the one policy that is missing.
Step 1: read the error, then look at the policies
The error names the table, for example for table "messages". In the Supabase dashboard, open Authentication, then Policies (or Table Editor, select the table, then view its policies). Look at the list for that table, organized by operation: SELECT, INSERT, UPDATE, DELETE. If the operation your app is attempting has no policy listed, you have found the bug. Zero INSERT policies means every insert from the app is rejected, no matter what the code does.
The number one cause in AI built apps
The builder tool enabled RLS (good) and wrote a SELECT policy so the app can read (good), then stopped. No INSERT policy at all, or an INSERT policy granted to authenticated while your app runs with no login, so auth.uid(), the current user's id, is null and matches nothing. Either way, writes die with exactly this error.
The fix is one INSERT policy whose with check condition matches how your app actually authenticates. In the Supabase SQL editor, for the common case where signed in users may create rows that belong to them:
create policy "Users insert own rows"
on public.your_table for insert
to authenticated
with check (auth.uid() = user_id);
For a truly public write, like a contact form or waitlist with no login:
create policy "Anyone can submit"
on public.your_table for insert
to anon
with check (true);
Understand the tradeoff on that second one: anyone on the internet can now insert into this table, including bots and spam. That is fine for a contact form (add rate limiting or a captcha if it grows), and wrong for anything like orders, votes, or user content tied to accounts. If your app has login, prefer the first shape. Supabase's dashboard also offers policy templates and an AI assistant in the policy editor, and both are legitimate ways to generate these, just read what they produce before running it.
WITH CHECK vs USING, in plain English
Policies have two possible conditions. USING filters rows you are allowed to touch that already exist, so it governs SELECT, and which rows UPDATE and DELETE may reach. WITH CHECK validates rows you are trying to write, so it governs INSERT, and the new values in an UPDATE. That means INSERT policies need with check, UPDATE policies need both, and a policy that only has using will still fail inserts with this exact error. If you see this error on an update, check that the UPDATE policy has a with check clause too.
The mismatched user_id trap
Sometimes the INSERT policy exists and looks right, with check (auth.uid() = user_id), and inserts still fail. The policy compares the logged in user's id to the user_id value on the row being inserted. If the app never sets user_id in the insert (AI builders forget this constantly), the column is null, null never equals anything, and the row is rejected. Same result if the code sets it to some other value. Two fixes, pick one:
- In code: include the current user's id in every insert, for example
{ ...form, user_id: user.id }. - In the database (sturdier): give the column a default so it fills itself in:
Then the app can omit it entirely and the policy passes.alter table public.your_table alter column user_id set default auth.uid();
Do not do this
Two "fixes" that make the error vanish and create a breach. Do not disable RLS on a public app: your table becomes readable and writable by anyone holding the anon key, which is everyone who has loaded your site. And do not put the service_role key in the browser to push writes through: that key bypasses RLS entirely, and anyone who opens devtools can copy it and own your whole database. If an AI chat suggests either when you paste this error in, refuse. The correct fix is always a policy, and it is usually four lines of SQL.
Still stuck?
Run the instant diagnosis. Paste your public repo URL into the form and an automated clean-room check reports what is broken: install, build, missing env config, and render, with the exact blocker named, in minutes, free. RLS policies live in your Supabase project rather than your code, so for this one also paste the exact error message and the failing request from your Network tab into the issue and I will look at it the same day. Private repo or zip? Email works too.
Get an instant free diagnosisOr email me instead. Fixes with 24 hour turnaround start at $95. Prefer self-serve? The $5 instant diagnosis on Apify checks your repo privately, no public issue needed.