Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

How to test Supabase RLS with two accounts (10 minutes, no tools)

To test Supabase Row Level Security (RLS), create two test users, add data as user A, then try to read and change it as user B and as a logged-out visitor. If B or a stranger gets A's rows, your policies are too loose. I

To test Supabase Row Level Security (RLS), create two test users, add data as user A, then try to read and change it as user B and as a logged-out visitor. If B or a stranger gets A's rows, your policies are too loose. If A can't see their own rows, a policy is missing.

What you need

  • Your own app, live or on staging with the same database rules.
  • Two test accounts you create: A and B.
  • Two browsers, or one normal and one private window.

Step 1: Create data as user A

Sign in as A and create one of everything your app stores: a project, a note, an upload. Open DevTools > Network, reload, and find the request to /rest/v1/.... Note the table and the row id.

Step 2: Try to read A's data as user B

  • Change the ID in the URL. Open A's page address with A's id. B should get an empty page or an error.
  • Replay the request. Right-click a similar /rest/v1/ request > Copy as fetch, paste it in the Console and change the filter to id=eq.<A's id>. B should get [].
  • Ask for everything. Replay without a user filter. B should only see B's rows.

Step 3: Try to change A's data as user B

Replay an update or insert with A's user_id in the body. It should fail or change 0 rows. If B can insert a row with A's user_id, the insert policy is missing with check (auth.uid() = user_id).

Step 4: Repeat logged out

Your public key is in the page, so a stranger can send the same request. Private tables should return [] or a permission error.

Step 5: The SQL editor way (optional)

The SQL editor normally bypasses RLS, so a plain select proves nothing. Impersonate a user and roll back:

begin;
set local role authenticated;
set local request.jwt.claims = '{"sub": "USER_B_UUID", "role": "authenticated"}';
select * from public.your_table where user_id = 'USER_A_UUID';  -- expect 0 rows
rollback;

Step 6: What RLS doesn't cover by itself

  • Views run as their owner unless created with security_invoker = true.
  • security definer functions run with their owner's rights. Call them as B with A's id.
  • Storage has its own policies. As B, open A's file URL. Private files should need a signed URL.

If a test fails

Paste this into your AI coder:

I tested my Supabase app with two users. User B could [read / change] user A's rows in [TABLE / VIEW / FUNCTION / BUCKET]. Please: 1) show me the current policies, 2) rewrite them so each user can only select, insert, update and delete rows where user_id = auth.uid(), with a matching WITH CHECK, 3) set views to security_invoker = true and check auth.uid() inside any security definer function, 4) give me the SQL and the two-account test to confirm the fix. Don't run anything until I say so.

Only test apps you own, with test accounts you created.

Free pre-launch checklist (14 checks to run before you share your link): https://milokits.com/checklist?ref=devto

We make Milo's Launch Check, a passive outside-in check for AI-built apps. It's launch week, so the Quick Check is $19 instead of $29 until 1 November (code QUICK19): https://milokits.com/?ref=devto#pricing

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.