Symptoms
- In the browser console,
supabase.from('profiles').select('*')returns other people’s emails, payment flags, or notes. - A second test account can update the first account’s row.
- The Supabase table editor shows policies named “Enable read access for all users” or a policy whose qualifier is
true. - The app “had to” use the service role in the client to make the dashboard render — that is a different bug, but they travel together.
Why Lovable, Bolt, and v0 do this
The generator’s job is a demo that renders a table. The fastest path is: create the table, turn on RLS because the docs said to, then add USING (true) so the select does not come back empty. Preview looks finished. The anon key, which is in your Vite env on purpose, can now read the whole table from any origin that can load your JS.
Some stacks skip ENABLE ROW LEVEL SECURITY entirely. On Supabase, a table without RLS is reachable with the anon key as soon as it exists. There is no “I’ll add policies later” mode that stays private.
The actual fix
Do this in the SQL editor, then re-test with the anon key, not the service role. The service role bypasses RLS; a green test with it proves nothing.
alter table public.profiles enable row level security; drop policy if exists "public read profiles" on public.profiles; drop policy if exists "Enable read access for all users" on public.profiles; create policy "profiles_select_own" on public.profiles for select to authenticated using (auth.uid() = id); create policy "profiles_update_own" on public.profiles for update to authenticated using (auth.uid() = id) with check (auth.uid() = id);
Match id (or user_id) to the column that stores auth.users.id. If your generator used user_id, write auth.uid() = user_id.
Split SELECT / INSERT / UPDATE / DELETE. A FOR ALL USING (true) is how people get a “fixed” app that anyone can delete.
Paid flags, invoices, and admin columns do not belong in a table the browser updates. Let a server webhook (service role, server only) set status = 'paid'.
USING (true) and for ENABLE ROW LEVEL SECURITY. Both hits matter. The first is theatre; the second missing is an open door.How to know you are done
- Two real users. User A cannot select user B’s row with the anon key.
- Logged-out
select('*')returns zero rows (or only rows you explicitly made public). - The service role is not in
src/. See the service_role page.
Starter SQL with a profiles + orders example ships in the $39 kit as supabase/rls-starter.sql.