VibeFix › Guides

Supabase insert not working? How to fix RLS errors and silent failures

Your app calls supabase.from(...).insert(...) and nothing saves. Maybe you get "new row violates row-level security policy", maybe data comes back null, maybe there is no error at all. AI app builders hit this constantly because the generated frontend code is fine; what's missing is a database policy, a session, or a grant. Here is how to find which one, with the exact SQL to fix it.

Updated October 18, 2026 · About 8 minutes to read

1. Read the error Supabase is already giving you

supabase-js does not throw when a query fails. It returns an error object, and if your code only looks at data, a rejected insert looks like "nothing happened". Log every field:

const { data, error } = await supabase
  .from('todos')
  .insert({ title: 'Test' })
  .select()

if (error) console.error(error.code, error.message, error.details, error.hint)

Then open DevTools → Network, trigger the insert, and find the POST to /rest/v1/todos. The status and response body tell you which section below applies:

2. "new row violates row-level security policy for table"

This is the most common one (Postgres code 42501). RLS is enabled on the table, which is what you want, but no policy allows this user to insert this row. With RLS on and no matching policy, every insert is rejected. Add an INSERT policy. Insert policies only take WITH CHECK, which tests the new row:

alter table public.todos enable row level security;

create policy "Users can insert their own todos"
on public.todos for insert
to authenticated
with check ( (select auth.uid()) = user_id );

The check only passes if the row's user_id equals the signed-in user's id. If your frontend doesn't send user_id, or sends a different value, the policy still fails. The cleanest fix is to let the database fill it in:

alter table public.todos
  alter column user_id set default auth.uid();

Now the client can insert { title: 'Test' } and the policy passes for the logged-in user. Run these in the Supabase SQL editor or as a migration. Lovable Cloud is built on Supabase, so the same SQL applies there.

3. Insert works but returns null, or fails only with .select()

In supabase-js v2, .insert() does not return the new row unless you chain .select(). So data === null with no error means the insert succeeded. Add .select() if you need the row (for example its new id).

The catch: returning the row is a read, so it also needs a SELECT policy. Without one, .insert().select() fails with the same "violates row-level security policy" message even though your insert policy is correct:

create policy "Users can view their own todos"
on public.todos for select
to authenticated
using ( (select auth.uid()) = user_id );

Upserts need more: .upsert() can insert or update, so it needs INSERT, UPDATE (with both using and with check) and usually SELECT policies.

4. The request isn't actually signed in

A policy written to authenticated never matches a request that arrives as the anon role, and in that case auth.uid() is null, so auth.uid() = user_id is never true. Check what the client sends:

const { data: { session } } = await supabase.auth.getSession()
console.log('signed in as', session?.user?.id ?? 'NOBODY (anon)')

Common ways this happens in AI-built apps:

5. "permission denied for table todos"

This also uses code 42501, but it isn't a policy problem. The role is missing a table privilege, so Postgres refuses before RLS is even checked. It shows up when tables are created by migrations that changed the default grants, or in a custom schema. Grant what the app needs:

grant select, insert, update, delete on public.todos to authenticated;

If the table is in a schema other than public, that schema must also be added to the exposed schemas in your project's API settings, and the role needs usage on it.

6. Data and schema errors

7. Sign-up fails with "Database error saving new user"

Many generated apps add a trigger that inserts a profiles row whenever someone signs up. If that trigger's insert fails (wrong column name, a not-null column with no value, missing public. prefix), the whole sign-up fails with this generic message. Look in the Supabase logs (Postgres) for the real error, then make sure the function looks like this:

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;
$$;

security definer lets it insert regardless of RLS, and the empty search_path is why every table name must be fully qualified.

8. Wrong project, wrong key, or missing env vars

If the Network tab shows Invalid API key, a request to undefined/rest/v1/..., or no rows in a table you can see full in the dashboard, the app may be talking to a different Supabase project. Confirm that the URL and the publishable/anon key come from the same project, that the variable names match what the code reads (VITE_ for Vite, NEXT_PUBLIC_ for Next.js), and that you redeployed after changing them on Vercel or Netlify.

Do not "fix" inserts by putting the service_role key in the browser. It bypasses RLS completely, so anyone who opens DevTools can read and delete your whole database. It belongs only in server code, and if it ever shipped to the frontend, rotate it.

9. Test policies the way your app runs them

"It works in the SQL editor" proves nothing: the editor runs as the postgres role, which bypasses RLS. Test as a real user instead, inside a transaction you roll back:

begin;
set local role authenticated;
select set_config('request.jwt.claims',
  '{"sub":"<user-uuid>","role":"authenticated"}', true);
insert into public.todos (title) values ('policy test');
rollback;

Then check the other direction with two real accounts: user B must not be able to read or edit user A's rows. Turning RLS off makes the error go away by making every row public to anyone holding your publishable key, so don't ship that.

FAQ

Why does my Supabase insert return null data?
In supabase-js v2, insert() doesn't return the new row unless you chain .select(). data: null with no error means the insert succeeded. If you add .select(), the table also needs a SELECT policy, or the request fails with a row-level security error.

What does "new row violates row-level security policy" mean?
Row-level security is enabled on the table and no INSERT policy allows this row for this user. Add an INSERT policy with a WITH CHECK condition such as (select auth.uid()) = user_id, make sure the request is signed in, and make sure user_id matches the signed-in user (or defaults to auth.uid()).

Is it safe to disable RLS so inserts work?
No. With RLS disabled, anyone with your public publishable/anon key can read, change or delete every row in that table. Keep RLS on and add the specific policies your app needs.

Can I use the service_role key to fix insert errors?
Only in server-side code. The service_role key bypasses all RLS, so it must never be in browser code. If it was shipped to the frontend, move it server-side and rotate it.

Why does the insert work in the SQL editor but not in my app?
The Supabase SQL editor runs as the postgres role, which bypasses RLS. Your app runs as anon or authenticated, so it is subject to your policies. Test with set local role authenticated inside a transaction to see what the app sees.

Still stuck? We'll fix it at a flat price.

VibeFix fixes broken Lovable, Bolt, Cursor, Replit and v0 apps, including Supabase RLS, auth and database bugs. Request a free check first, and we'll tell you what's wrong and which plan fits before you pay. If we can't fix it, you get a full refund.

Get a free check See pricing

Already know you need help? Quick Fix, $49 (one bug, 24h) · Standard Rescue, $129 · Deploy-Ready, from $199