"New row violates row-level security policy" (Supabase error 42501) means a write hit a table with row level security on, and no insert or update policy accepted that row for the role making the request. The usual causes: no insert policy, the user isn't logged in so auth.uid() is null, or the row's user_id doesn't match. The fix is a policy, not turning RLS off.
In a vibe-coded app built with Lovable, Bolt, or Replit, this error usually shows up the day after someone turned RLS on, ran the security scan, or asked the AI to "lock down the database". Reads still work, so the app looks fine until a user tries to save something. Below is how to read the error, the six causes I see in these apps with a fix for each, the policy to add, the storage version of the error, and how to test the fix as a real user. The SQL runs in the Supabase SQL editor. In Lovable Cloud, the docs say policies are under More, then Database, then Manage, then Policies, and that view is read-only: you ask Lovable in chat to change one, then check what it wrote.
What “new row violates row-level security policy” means
Row level security (RLS) is a Postgres feature: rules called policies decide which rows each user can read or change on a table. A policy has two parts. using filters rows that already exist, for reads, updates, and deletes. with check inspects the row that would exist after an insert or update. The Postgres docs say an insert policy can only have a with check expression, and if it returns false or null for any row, the whole statement fails with an error.
That's the difference from most RLS problems. A read that is denied comes back empty, with no error, because usingjust hides rows. A write that is denied is loud: Postgres raises "new row violates row-level security policy", Supabase passes it through as code 42501, and the app shows it or logs it. So the error is good news in one way. The table is protected. The job is to let the right write through.
Supabase's RLS guide adds the detail that makes this error so common in AI-built apps: once RLS is enabled, no data is accessible through the API with the public key until you create policies. The AI tool usually writes a select policy so the screens fill, and forgets inserts and updates until someone presses Save.
Read the error: table, policy, 401 or 403
Before changing anything, get the full error. In Chrome, open developer tools, go to the Network tab, filter for rest/v1, press Save in the app, and click the failed request. The response is a small JSON object with four fields:
{
"code": "42501",
"message": "new row violates row-level security policy for table \"projects\"",
"details": null,
"hint": null
}- The table is in the message, without its schema. A table called
objectsisstorage.objects: a file upload, covered below. - The policy nameappears in some versions of the message, in quotes before "for table". Postgres has both forms in its source. If yours names the policy, that policy's
with checkis the one that said no. - "(USING expression)"in the message means the write was an upsert that hit an existing row, and the update policy's
usingclause rejected it, or there is no update policy at all. - The HTTP statustells you who made the request. PostgREST, the layer Supabase uses for its API, maps 42501 to 403 if the request is authenticated and 401 if it isn't. A 401 means the app sent the request logged out, and that's the bug, whatever the policy says.
Then list the policies on that table, so you're fixing what exists rather than what the AI tool told you it wrote:
select policyname, cmd, roles, qual as using_expr, with_check
from pg_policies
where schemaname = 'public' and tablename = 'projects';cmd is the action each policy covers. If there is no row with INSERT or ALL, that's cause one. If roles says {public}, the policy applies to every role, including logged-out visitors, which is rarely intended.
Six causes of the error in a Lovable or Bolt app
In the order I check them. The first three cover most cases; the last three are the ones people spend a day on because the policy looks right.
| Cause | How to tell | Fix |
|---|---|---|
| 1. No insert policy on the table | pg_policies shows select only | Add the insert policy below |
| 2. Request isn't logged in | Status 401; getUser() returns null | Fix the session, not the policy |
| 3. user_id missing or someone else's | Request payload has no user_id, or a different one | Set it from the session, or a column default |
| 4. insert().select() with no select policy | Insert policy exists; app asks for the row back | Add a matching select policy |
| 5. Upsert or update with no update policy | Message says "(USING expression)", or code uses upsert | Add update policy with using and with check |
| 6. A signup trigger blocked by RLS | Fails on signup; table is users or profiles | Make the trigger function security definer |
1. No insert policy on the table
Policies are per action. A select policy does nothing for inserts. This is the case when the app read data fine for weeks and the first save failed. Add the insert policy from the next section and test again.
2. The request isn't logged in
Supabase's guide says auth.uid() returns null when a request has no authenticated user, so a check like auth.uid() = user_id can never be true. The policy is right; the request is wrong. Three ways this happens in AI-built apps:
- Right after signup.Supabase's reference says that with email confirmations on, which is the default on hosted projects,
signUp()returns a user but the session is null until the email is confirmed. Code that creates a profile row straight after signup runs logged out and fails. Create the profile from a database trigger instead (cause 6), or after the first real login. - A second client. Server code, an Edge Function, or a helper file that creates its own Supabase client with the public key and no user token runs as
anon. Pass the user's token through, or use the secret key only on the server. - The session expired or never loaded. If the app fires the save before the auth state has loaded, the request goes out without a token. Check
supabase.auth.getUser()right before the insert: if it's null, the database sees null too. An app that never finishes loading the session has its own set of causes, in Lovable app hangs after login.
3. The row's user_id is missing or someone else's
The policy compares the row's user_idwith the logged-in user. If the app doesn't send user_idat all, the comparison is against null and fails. If it sends an ID it took from the page, a URL, or a form field, it can be someone else's, and the policy correctly refuses. Look at the request payload in the Network tab. The fix is to set the column from the session on the server side of the line, with a column default (in the next section), so the app can't get it wrong. In the Supabase GitHub discussion on this error, the maintainer's summary was that the message just means you aren't meeting RLS: either not signed in, or the policies aren't met.
4. The app asks for the new row back
Supabase's reference says inserted rows aren't returned unless you chain .select(). AI-written code almost always chains it. The Postgres docs explain what happens next: with a RETURNINGclause, the new row must satisfy the table's select policies, and if it doesn't, an error is thrown rather than the row being silently dropped. So an insert policy alone isn't enough. Add a select policy that covers the rows the user just created, or drop .select()where the app doesn't use the result.
5. It's an upsert or an update
.upsert() inserts if the key is new and updates if it exists, so it has to pass the insert policy, the update policy, and the select policy. The Postgres docs say the rows proposed by an ON CONFLICT DO UPDATE are checked against the select policies, and the update path is never silently skipped. The same GitHub discussion found a second trap: the upsert has to include the primary key, or the row the policy checks has no ID to compare. A plain .update()with no update policy is different: it matches no rows and reports success, which is why some saves "work" and change nothing. The Supabase RLS checklist covers that silent case and the rest of the table's policies.
6. A signup trigger is blocked by RLS
Many apps create a row in public.profiles or public.users with a trigger on auth.users. When the error appears for table usersduring signup, a Supabase maintainer's accepted answer in a 2025 discussion was that the trigger function itself needs to be security definer, so it runs with its creator's rights instead of the new user's. The user has no session yet, so no insert policy on profiles will let them through. It isn't RLS on auth.users: its owner bypasses RLS there.
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
insert into public.profiles (id, email)
values (new.id, new.email);
return new;
end;
$$;
drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();The Supabase insert policy with WITH CHECK
This is the policy set for a table where each row belongs to one user, following Supabase's own examples: name the role with to authenticated, so the expression never runs for visitors, and wrap auth.uid() in select, which Supabase's docs say lets Postgres evaluate it once per statement instead of once per row. The column default fills in user_id when the app leaves it out, which removes cause 3 for good.
alter table public.projects enable row level security;
-- Fill user_id from the session when the app doesn't send it
alter table public.projects
alter column user_id set default (select auth.uid());
create policy "Users add own projects"
on public.projects for insert
to authenticated
with check ((select auth.uid()) = user_id);
-- Needed for insert().select(), upsert, and reading the list
create policy "Users read own projects"
on public.projects for select
to authenticated
using ((select auth.uid()) = user_id);
-- using: which rows they may change. with check: what the row may become.
create policy "Users change own projects"
on public.projects for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create index if not exists projects_user_id_idx on public.projects (user_id);The with check on the update policy matters. The Postgres docs say that when an update policy has only using, the same expression is used to check the new row, which is usually what you want. But an AI-written policy with using (true) and no with check lets a user change user_id and hand a row to someone else, or, on a subscriptions table, set their own plan to paid.
If the table has a team or workspace column instead of user_id, the check becomes "is this user a member of that team". Writing that as a sub-query on the members table is what produces the other famous RLS error, and the fix with a helper function is in infinite recursion detected in policy for relation.
Fix the storage variant: for table objects
"New row violates row-level security policy for table objects", usually as a 403 from an upload, is the same error on the table where Supabase Storage keeps each file's metadata, storage.objects. Supabase's storage guide says that by default Storage allows no uploads to a bucket without RLS policies, and that uploading needs only an insert policy on storage.objects. Making the bucket public doesn't help: the buckets guide says a public bucket bypasses access control for reading files, while access control is still enforced for uploads, deletes, moves, and copies.
Two more things from Supabase's storage troubleshooting page. With upsert: true, the upload runs as an insert with RETURNING *, so a select policy is needed too, and the page says the 403 in that case means the database can't return the new object's metadata. And overwriting an existing path needs an update policy as well. Leave upsert off unless the app really replaces files.
create policy "Users upload own files"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "Users read own files"
on storage.objects for select
to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);Supabase's helper docs say storage.foldername(name) returns the file's folders as an array, so for public/subfolder/avatar.png it returns ['public', 'subfolder']. Postgres arrays start at 1, not 0. Two upload bugs follow from that: the app uploads to the bucket root, so there is no first folder and the check compares against null, or the AI wrote [0]. The upload path in the app must be ${user.id}/filename for this policy to pass.
Test the insert as the user, not as postgres
The SQL editor runs as postgres, which skips RLS, so an insert that works there proves nothing. Test as the role the app uses. The Supabase dashboard has a role dropdown in the SQL editor and the Table editor, which its December 2023 Studio announcement describes as running queries as anon or as authenticatedwith a chosen user's ID. An RLS Tester, announced as a feature preview on 24 April 2026, shows which policies a query evaluated, but only for selects so far. For writes, do it in SQL, inside a transaction you throw away:
begin;
set local role authenticated;
select set_config(
'request.jwt.claims',
'{"sub": "PASTE-A-USER-ID", "role": "authenticated"}',
true
);
insert into public.projects (name) values ('rls test')
returning id, user_id; -- should succeed, user_id = that user
insert into public.projects (name, user_id)
values ('rls test', 'SOME-OTHER-USER-ID'); -- should fail with 42501
rollback;The second statement is the one people skip. A policy that lets the right insert through but also lets a user write rows under someone else's ID has the same symptom as no policy at all: the app works. Then repeat the first insert with set local role anon and no claims; it should fail. Finally, test in the app itself with two accounts, as described in the checklist post, because the app may create its client differently from how you imagine.
Why not disable RLS or use the secret key
Two fixes make the error disappear in under a minute, and both appear in forum answers and sometimes in the AI tool's own suggestion.
- Turning RLS off on the table.Supabase's API key docs say the publishable key is safe to expose online and only reaches what RLS allows. With RLS off, that key reaches everything, and it's in every visitor's browser. The same goes for a policy of
with check (true)on a private table: any logged-in user can write rows under anyuser_id. - Switching the app to the secret key. The same docs say a secret key bypasses every RLS policy and must never be put in a browser, a shipped app, or source control. It belongs in server code only: an Edge Function, a webhook handler, a cron job. If the AI moved an insert to the secret key to stop this error, move it back and write the policy.
There is one place the secret key is the right answer: a write that no user should be able to make from the browser, such as marking a subscription paid. That belongs in server code with the secret key, and the table gets no insert or update policy for users at all. If the app writes it from the browser today, the error is telling you the design is wrong, not the policy.
When to get help with Supabase RLS errors
Fix it yourself if it's one table, the app has one kind of user, and the test above passes both ways afterwards. Get help if the error appears on several tables at once, which usually means policies were written one prompt at a time and the rest are untested too, or if the fix the AI proposed was to turn RLS off, or if the table holds payments or personal data.
Checking every policy as an outsider and as a second user, and writing down what to fix, is my Launch-Ready Review, a fixed $900–1,500 in 3–5 days; fixing the list is a Production Sprint. The pattern is the same one I dealt with on the social challenge app, which an agency left half-built with AI-generated code: security, login, and payments all had problems, and the product moves real money. I reviewed the code, kept what was solid, rewrote the rest, and launched in eight weeks. It now has 10,000 users and $40K staked. What each step costs is in the cost to fix a vibe-coded app.
If you'd rather hand it over, send me the error and your app's link.
Sources
- Row Level Security: with check, roles, auth.uid() null, bypassrls, Supabase Docs
- CREATE POLICY: USING vs WITH CHECK, RETURNING and ON CONFLICT, PostgreSQL Docs
- execMain.c: the row-level security error messages, PostgreSQL on GitHub
- HTTP status codes: 42501 is 403 if authenticated, else 401, PostgREST Docs
- new row violates row-level security policy for table users (42501), Supabase on GitHub
- RLS blocking user signups: a trigger writing to public.users, Supabase on GitHub
- insert(): rows are not returned unless .select() is chained, Supabase Docs
- signUp(): with email confirmations on, session is null, Supabase Docs
- Storage error 403: new row violates row-level security policy on upload, Supabase Docs
- Storage access control: upload policies by bucket and folder, Supabase Docs
- Storage buckets: public buckets still enforce access control on uploads, Supabase Docs
- storage.foldername() returns the folder path as an array, Supabase Docs
- Role impersonation in the SQL and Table editors, Supabase Blog
- Feature preview: RLS Tester, Supabase Changelog
- API keys: the secret key bypasses every RLS policy, Supabase Docs
- Database access: where policies are in Lovable Cloud, Lovable Docs