FixLoop

Saved but nothing saved

Guide · October 14, 2026

You click Save. The button says it worked. Refresh, and the old value is back. Network looks like a 200 or 204. error is null. Postgres did not throw. The row did not move.

That is Row Level Security doing what it is supposed to do, quietly. An UPDATE whose USING clause matches no rows is a no-op: zero rows, no error. Lovable UIs that only check if (!error) toast success anyway.

Source: Row Level Security (UPDATE USING / WITH CHECK, the "denied by" table, auth.uid() is null when logged out). Related: empty arrays from RLS.

What it looks like

const { error } = await supabase
  .from("profiles")
  .update({ display_name })
  .eq("id", userId);

if (!error) toast.success("Saved"); // this lies

Typical companions:

If you instead see 42501 permission denied for table, stop. That is a missing GRANT, not this bug. Use the Oct 30 grants guide.

Three ways a write is denied

From the RLS docs, only two of the three raise an error:

  1. Missing GRANT → 42501. Policies never run.
  2. WITH CHECK failure → 42501. The new row is not allowed (you tried to reassign user_id, or INSERT a row that is not yours).
  3. USING filtered the row out → nothing. Zero rows matched. This is the silent save.

USING decides which existing rows you may touch. No match looks exactly like UPDATE … WHERE false. WITH CHECK decides what the row may look like after. If you omit WITH CHECK, USING is used for both.

A missing UPDATE policy is case 3: RLS is on, no policy means no visible rows, no error. Supabase also warns that an UPDATE needs a matching SELECT policy or it will not work as expected. Add both.

Why the client looks successful

PostgREST defaults to no representation on PATCH. supabase-js without .select() gives you { data: null, error: null } even when zero rows changed. Chain .select() and treat an empty array as failure:

const { data, error } = await supabase
  .from("profiles")
  .update({ display_name })
  .eq("id", userId)
  .select("id, display_name")
  .maybeSingle();

if (error) throw error;
if (!data) throw new Error("Update matched no rows (RLS or filter)");

Put that in the save handler. The toast should follow data, not the absence of error.

Step-by-step fix

1. Confirm you have a user

auth.uid() is null with no JWT. Then auth.uid() = user_id is false for every row, which is a silent USING miss. The docs call this out explicitly.

const { data: { session } } = await supabase.auth.getSession();
console.log(session?.user?.id);

Null here is an auth bug, not a policy bug. Production-only? Read preview vs production login. Network tab: Authorization on the PATCH should be the user access token, not the anon key twice.

2. Read the policies

Dashboard → Authentication is the wrong screen. Use Table Editor → the table → RLS, or:

select polname, cmd, roles, qual, with_check
from pg_policies
where tablename = 'profiles';

You want, at minimum:

alter table public.profiles enable row level security;

create policy "select own profile"
on public.profiles
for select
to authenticated
using ( (select auth.uid()) = user_id );

create policy "update own profile"
on public.profiles
for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

grant select, update on table public.profiles to authenticated;

Swap user_id for the real owner column. Lovable often writes id, user_id, and owner_id in different files. A policy on the wrong column is a silent USING miss.

Wrap auth.uid() in (select auth.uid()) as the docs recommend, so Postgres caches it per statement.

3. Common USING misses

Do not "fix" this by turning RLS off, and do not add using (true) on a table with private rows. That is the other incident: is my database public?

Quick checklist

Questions

Why does supabase.from().update() succeed but change nothing?

RLS USING matched no rows, so Postgres updated zero rows and raised nothing. Without .select(), supabase-js reports error: null. Check the returned row.

When does RLS raise an error instead of going silent?

Missing GRANT → 42501. WITH CHECK fail → 42501. USING miss → no error, zero rows. That table is in the RLS docs.

Do I need a SELECT policy to UPDATE?

Yes. The RLS guide says an UPDATE needs a corresponding SELECT policy or it will not work as expected. You also need SELECT if you want .select() to return the row.

Can a missing session cause a silent save?

Yes. auth.uid() is null, null = user_id is false, USING matches nothing. The client still talks with the anon key, so it does not 401.

Is this the same as a public database?

Opposite. RLS off (or using (true)) returns extra rows. A USING miss on UPDATE returns none. One leaks, one no-ops. Same dashboard, different policy.

New table and a hard 42501? Oct 30 grants. Login only broken on the live domain? preview vs production.

Save still a no-op after you check the policies? That is an app fix. We reproduce the write, patch the policy, and you pay only if it sticks. We'll confirm the scope first.

Get this fixed