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

  1. 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.
  2. 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, like using (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_admin or user_id sent from the app

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, using decides which existing rows a user can touch, and with check decides what the row may look like afterwards. If with check is loose (like with check (true)), or another update policy allows it, a user may be able to edit their own row and change user_id to someone else's, handing the row away or planting it on another account. If you leave with check out, Postgres reuses the using rule 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 set role themselves. If the app sends the role or user_id and 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 in select for 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:

  1. 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_admin gets "permission denied". Note: the revoke has to come first, because a table-wide grant still covers every column.

  2. A separate table. Keep role or is_admin in 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

  1. Signed out: try to edit or delete a row. It should fail or change nothing.
  2. Signed in as user A: you can edit and delete your own rows. The app still works.
  3. Signed in as user A, targeting user B's row: edits and deletes should change nothing. Also try changing your own row's user_id to 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