How to Test Supabase RLS Policies
Verify your Row Level Security is working correctly
TL;DR
TL;DR:
In the Supabase SQL Editor, set request.jwt.claims.sub to impersonate a user, then check that they see their own rows and nothing else. Do it for all four operations: SELECT, INSERT, UPDATE, DELETE. Then repeat the whole thing from your real app in DevTools, because the SQL Editor won't catch a client that's holding the wrong key.
Method 1: SQL Editor Testing
The SQL Editor can pretend to be any of your users. This is the fastest of the three methods and the one to start with.
Get user IDs for testing
-- List all users and their IDs
SELECT id, email, created_at
FROM auth.users
ORDER BY created_at DESC
LIMIT 10;
Simulate a specific user
-- Set the current user for testing
SET request.jwt.claims.sub = 'user-uuid-here';
-- Now queries will run as if this user is authenticated
SELECT * FROM todos;
Test access controls
-- As User A, try to see their own data
SET request.jwt.claims.sub = 'user-a-uuid';
SELECT * FROM todos; -- Should show User A's todos
-- Try to see User B's data directly
SELECT * FROM todos WHERE user_id = 'user-b-uuid';
-- Should return empty (RLS blocks it)
-- Try to update User B's data
UPDATE todos SET completed = true WHERE user_id = 'user-b-uuid';
-- Should update 0 rows
-- Reset when done
RESET request.jwt.claims.sub;
Method 2: Browser DevTools Testing
Passing in the SQL Editor isn't quite the same as being safe in production. Your app talks to Supabase over the network with a specific key, so test it there too.
Open DevTools Network tab
Open your app in a browser, sign in as a test user, then open DevTools (F12) and go to the Network tab.
Observe Supabase requests
Filter for requests to your Supabase URL and read the responses. You're looking for rows that came back but shouldn't have.
Try to manipulate requests
Now go to the Console and ask for something you're not entitled to:
// Try to fetch another user's data
const { data, error } = await supabase
.from('todos')
.select('*')
.eq('user_id', 'another-user-uuid');
console.log(data); // Should be empty
console.log(error); // Should show RLS error or empty result
Method 3: Automated Testing
Manual testing catches today's bug. Automated tests catch the one you reintroduce next month editing a policy.
// rls.test.ts
import { createClient } from '@supabase/supabase-js';
describe('RLS Policies', () => {
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
let userAClient: ReturnType<typeof createClient>;
let userBClient: ReturnType<typeof createClient>;
beforeAll(async () => {
// Sign in as User A
const { data: sessionA } = await supabase.auth.signInWithPassword({
email: 'user-a@test.com',
password: 'test-password'
});
userAClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ global: { headers: { Authorization: `Bearer ${sessionA.session?.access_token}` } } }
);
// Sign in as User B
const { data: sessionB } = await supabase.auth.signInWithPassword({
email: 'user-b@test.com',
password: 'test-password'
});
userBClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ global: { headers: { Authorization: `Bearer ${sessionB.session?.access_token}` } } }
);
});
test('User A cannot see User B data', async () => {
const { data } = await userAClient
.from('todos')
.select('*')
.eq('user_id', 'user-b-uuid');
expect(data).toHaveLength(0);
});
test('User A can only see own data', async () => {
const { data } = await userAClient
.from('todos')
.select('*');
// All returned rows should belong to User A
data?.forEach(row => {
expect(row.user_id).toBe('user-a-uuid');
});
});
test('User A cannot update User B data', async () => {
const { data, error } = await userAClient
.from('todos')
.update({ completed: true })
.eq('user_id', 'user-b-uuid')
.select();
expect(data).toHaveLength(0);
});
});
RLS Testing Checklist
Go table by table. For each one with RLS enabled, confirm all six:
- SELECT: Users can only see rows they should access
- INSERT: Users can only create rows with their own user_id
- UPDATE: Users can only modify their own rows
- DELETE: Users can only delete their own rows
- Anonymous: Unauthenticated users see only public data
- Cross-user: Users cannot access other users' data by guessing IDs
Test Negative Cases
The tests that matter are the ones that should come back empty. "User A can see their todos" passes just as happily with RLS switched off entirely. The test with teeth is User A reaching for User B's row and getting nothing.
Common Testing Mistakes
- Testing with the service_role key. This one invalidates everything else on the page. The service_role key bypasses RLS by design, so every test you run with it passes. Test with the anon key and a real signed-in user.
- Only testing happy paths. Unauthorized access attempts are the point.
- Forgetting INSERT policies. A SELECT policy stops people reading other users' rows. It does nothing to stop them writing a row that claims to belong to someone else.
- Not re-testing after a policy change. Policies are code, and you just changed the code.
Use CheckYourVibe: Run a scan to automatically detect missing or misconfigured RLS policies in your Supabase project.
Why can I see all data in the Supabase dashboard?
The dashboard uses the service_role key, which bypasses RLS on purpose so you can administer your data. Your app uses the anon key, where RLS does apply. Seeing everything in the dashboard tells you nothing about whether your policies work.
How do I test as an anonymous user?
In the SQL Editor, don't set request.jwt.claims.sub. Or create a Supabase client without signing in. This simulates an unauthenticated user with the anon role.
What if my RLS test fails unexpectedly?
Work down the list: RLS is actually enabled on the table, a policy exists for that specific operation, the policy condition names the right column, and you're testing under the user context you think you are. That last one catches more of these than the other three combined.
Related guides:How to Set Up Supabase RLS · How to Write RLS Policies · Supabase Security Guide