Guide Supabase
Can other users change or delete my users' data? How to check and lock it down
Short answer
A tester messages you: "I just edited someone else's profile." Or you're lying awake wondering if one user of your Lovable, Bolt, Replit or Cursor app could delete another user's posts or orders. It can happen, even when reading is locked down. The good news: the fix can be a few lines of SQL, and you can check for it today.
The short answer
Supabase uses Row Level Security (RLS) rules, called policies, to decide who can do what with each row. Each policy covers one action: read (select), add (insert), change (update) or delete (delete), or all of them at once. [S1] [S2]
A safe read rule doesn't protect changes or deletes. If your update or delete policy doesn't check who owns the row, any signed-in user may be able to change or delete everyone's rows.
The 2-minute check
- In the app: make two test accounts, user A and user B. As user B, create something (a post, a profile edit, an order). Sign in as user A and try to edit or delete it. If it works, you have this problem.
In the dashboard: open your Supabase project and find your table's policies (the RLS policies page in the dashboard). For each table, look at the policies for UPDATE, DELETE and ALL. Watch for:
- a rule that just says
true, likeusing (true) - a rule that doesn't mention
auth.uid()at all - a rule that checks a column your users can set themselves, such as
role,is_adminoruser_idsent from the app
- a rule that just says
Why this happens
Three things cause it:
- No ownership check. The update or delete policy lets any signed-in user through, for example
using (true). That can come from a "just make it work" prompt. See the unrestricted access policy guide. - Update with a loose
with check. For updates,usingdecides which existing rows a user can touch, andwith checkdecides what the row may look like afterwards. Ifwith checkis loose (likewith check (true)), or another update policy allows it, a user may be able to edit their own row and changeuser_idto someone else's, handing the row away or planting it on another account. If you leavewith checkout, Postgres reuses theusingrule as the check, so that alone doesn't open this hole. [S2] - Trusting what the app sends. A policy like "allow if
role = 'admin'" only helps if users can't setrolethemselves. If the app sends the role oruser_idand the database believes it, anyone can send a different value. The browser is not a safe place to decide who someone is.
The fix: keep RLS on and check the owner
Please don't switch RLS off to "fix" anything. Instead, make every write check that the signed-in user owns the row. Replace your_table with your table's name, and user_id with the column that stores who owns the row.
-- Keep RLS on
alter table public.your_table enable row level security;
-- Change: only your own rows, and you can't hand them to someone else
create policy "Users update own rows"
on public.your_table for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
-- Delete: only your own rows
create policy "Users delete own rows"
on public.your_table for delete
to authenticated
using ((select auth.uid()) = user_id);
-- Add: you can only create rows that belong to you
create policy "Users insert own rows"
on public.your_table for insert
to authenticated
with check ((select auth.uid()) = user_id);
Then remove the old loose policies. Policies add up: if an old using (true) update policy is still there, it can still let everyone in, even next to your new strict one. [S2] Drop it by name:
drop policy "name of the old policy" on public.your_table;
A few plain-language notes:
(select auth.uid())is the ID of the user who's signed in right now. Supabase recommends wrapping it inselectfor speed. [S1]- Your app needs a matching read (
select) policy too, so users can find their own rows before editing them. [S1] - Only add the policies your app actually uses. If users don't delete anything, skip the delete policy.
More patterns (teams, shared rows, admins) are in our RLS policy examples guide.
Lock down columns users shouldn't touch
Even with the owner check, a user can change any column in their own row. That's a problem for columns like role, is_admin, plan or credits.
Two ways to protect them:
Column grants. Let signed-in users update only the columns they need: [S3] [S4]
revoke update on public.profiles from authenticated, anon; grant update (display_name, avatar_url) on public.profiles to authenticated;Now an attempt to change
is_admingets "permission denied". Note: therevokehas to come first, because a table-wide grant still covers every column.- A separate table. Keep
roleoris_adminin a table users can read but not write, and change it only from your server or the dashboard.
Related: Lovable admin page security and users getting a paid plan for free.
Re-test as user A and user B
- Signed out: try to edit or delete a row. It should fail or change nothing.
- Signed in as user A: you can edit and delete your own rows. The app still works.
- Signed in as user A, targeting user B's row: edits and deletes should change nothing. Also try changing your own row's
user_idto user B's. That should be blocked.
A blocked update or delete on someone else's row can just change 0 rows with no error, so check the data afterwards rather than waiting for an error message. Trying to reassign your own row to someone else gives an error: new row violates row-level security policy. [S1]
One key to keep off the frontend
The service_role key (or a newer secret key) bypasses RLS entirely. Every policy above means nothing to it. It belongs on your server only (an edge function or backend), not in browser code. If it's in your frontend, assume it's been seen and rotate it. [S5] See Supabase service role vs anon key.
If data was already changed
Fix the policies first, so it can't keep happening. Then look at your project's backups in the Supabase dashboard (what's available depends on your plan). [S6] Our app hacked, what now guide walks through the next steps.
Not sure every table's write rules are right?
Locking one table is step one. Making sure no table lets users change each other's data is step two. Run the free check, or get the $49 Database Check: you run our read-only script in your SQL editor and send us the results. We need no passwords, no keys and no login to your account, and we change nothing. A person goes through every table and storage bucket, and you get a plain red, yellow and green report with the order to fix things in. Get the $49 Database Check
A person reviews and approves every report. It's not a penetration test or a security guarantee.
Sources
- [S1] Supabase, Row Level Security (policies per action, (select auth.uid()), update needs select, with check error)
- [S2] PostgreSQL, CREATE POLICY (USING vs WITH CHECK, permissive policies combine with OR)
- [S3] PostgreSQL, GRANT (column privileges)
- [S4] Supabase, Column Level Security
- [S5] Supabase, Understanding API keys (service_role / secret keys bypass RLS, server-side only)
- [S6] Supabase, Database Backups