Your Supabase delete policy lets any signed-in user delete data
The Supabase delete policy using (auth.role() = 'authenticated') reads like "only my users". In practice it lets any account delete or edit any row, and if your project allows signups, anyone can get an account.
QUICK CHECKLIST
- Every delete and update policy compares auth.uid() with the row owner
- Every policy names its role with to authenticated
- Updates have a with check clause
- No later migration disables RLS
- Signups off if only you should log in
Why this matters
Symbiotic Security scanned 1,072 Supabase apps built with Lovable, v0, Bolt, Replit and Windsurf and published the results in June 2026. They report 172 sites that let anyone delete records without logging in, and 172 that let anyone change existing records. Those numbers are for visitors who never signed in. A policy that only checks for a login lets in everyone who spends ten seconds creating an account.
Three patterns to look for
Logged in means allowed. The policy checks that there is a user, not that the row is theirs:
create policy "Authenticated users can delete" on public.posts for delete using (auth.role() = 'authenticated'); create policy "Users can delete their own codes" on public.codes for delete using (auth.uid() is not null);
The second example is from a public repo: its name says "their own", but the expression only checks for a login. Storage policies have the same shape: bucket_id = 'avatars' and auth.role() = 'authenticated' lets any user delete anyone's files in that bucket.
Deletes for everyone. Without a to role, a policy applies to public, which includes signed-out visitors: for delete using (is_custom = true) lets anyone with your anon key delete every custom row.
RLS switched off later. An early migration enables RLS, and a later "quick fix" runs alter table ... disable row level security to get rid of a permission error. Every policy on that table stops counting (the same result as a table created without RLS).
"My app has no signup page"
This is a common reason these policies feel safe, especially on portfolio and CMS projects where the owner is the only one who logs in. The Supabase signup endpoint doesn't need your page. Unless you switched off "Allow new users to sign up" in the Auth settings, anyone can call supabase.auth.signUp({ email, password }), or the Auth API directly, with the anon key your app already ships.
What we found in public repos
We searched public GitHub code for delete policies that use auth.role() = 'authenticated' and read the SQL files from 28 repos. Our check flagged 18 of them, with 78 policies between them that let any signed-in user delete or change rows that aren't theirs. One of them was open to visitors who never signed in. A typical case was a portfolio's admin tables (profiles, projects, site settings, contact messages) with for all using (auth.role() = 'authenticated'). In a second sample of 15 repos with auth.uid() is not null, 9 were flagged. In a control sample of 15 repos whose delete policies compare auth.uid() with the row's owner, it flagged none.
We picked these files because they matched the pattern, so this is not a rate across all Supabase apps. We read every flagged policy by hand.
How to write a safe Supabase delete policy
Compare the user with the row's owner
Name the role, too:
create policy "own rows: delete" on public.posts for delete to authenticated using ((select auth.uid()) = user_id);
Add with check to updates
Use the same ownership check in
with check, so a user can't hand their row to someone else. Insert policies need it too, see Supabase insert policies.Check a real admin role for admin tables
If only you should write to a table, check an admin role from
app_metadataor a roles table. Don't useuser_metadatafor that, because users can edit it themselves. Or switch off public signup if nobody else needs an account.Write one policy per operation
Supabase's docs recommend separate select, insert, update and delete policies and say a
for allpolicy "hides which operation each rule was meant to cover."Test with a second user
A delete blocked by a
usingclause raises no error and just matches zero rows. "No error" proves nothing, so test withreturningand check that the row is still there.
create policy "delete" on public.posts for delete using (auth.role() = 'authenticated');
create policy "own rows: delete" on public.posts for delete to authenticated using ((select auth.uid()) = user_id);
Fix prompt you can paste
Review every RLS policy in supabase/migrations for DELETE, UPDATE and ALL. Flag any policy whose USING clause only checks auth.role() = 'authenticated' or auth.uid() IS NOT NULL, any write policy without a TO role that never checks auth.uid(), storage.objects policies that only check bucket_id and login, and any later DISABLE ROW LEVEL SECURITY. Rewrite each one with TO authenticated and an ownership check ((select auth.uid()) = user_id), add WITH CHECK to updates, use app_metadata or a roles table for admin-only tables, and write pgTAP tests where a second user tries to delete the first user's row and the row is still there afterwards.
How Leakcade checks this
Our policy check reads your SQL migrations in order. It flags DELETE, UPDATE and ALL policies that only check for a login, write policies open to signed-out visitors without any auth check, and tables where RLS is switched off again in a later migration. Policies with using (true) and tables created without RLS are covered by our auth-route check. Changes made only in the Supabase dashboard don't show up in migrations, and the report tells you to compare.
On the 15 repos that scoped deletes to the row's owner, the check stayed quiet.
Further reading
- Symbiotic Security: We scanned 1,072 vibe coded apps
- Supabase docs: Row Level Security
- Supabase docs: General auth configuration
FAQ
How do I let users delete only their own rows in Supabase?
Create an RLS delete policy for the authenticated role with using ((select auth.uid()) = user_id), where user_id is the column that stores the owner.
Why does my Supabase delete return no error but delete nothing?
RLS filtered the row out. A delete that no policy allows matches zero rows and doesn't raise an error. Add .select() or returning to see what was actually deleted.
What's the difference between auth.role() and auth.uid()?
auth.role() tells you whether the request is signed in (authenticated) or not (anon). auth.uid() returns the user's id, which you can compare with the row's owner.
How do I disable signups in Supabase?
Turn off "Allow new users to sign up" in the Supabase Auth settings. Existing users can still log in.
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms