How to Test Supabase RLS Policies
Verify your Row Level Security is working correctly
TL;DR
TL;DR: Test RLS in the Supabase SQL Editor by setting request.jwt.claims.sub to simulate different users. Verify that users can only see their own data and cannot access other users' data. Test all four operations: SELECT, INSERT, UPDATE, DELETE. Also test from your actual app using DevTools.
Method 1: SQL Editor Testing
The Supabase SQL Editor lets you simulate authenticated users:
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
Test your actual application as a real user:
Open DevTools Network tab
Open your app in a browser, sign in as a test user, and open DevTools (F12) → Network tab.
Observe Supabase requests
Filter for requests to your Supabase URL. Look at the response data to verify only authorized data is returned.
Try to manipulate requests
In the Console, try to fetch data you shouldn't have access 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
Write tests that verify RLS behavior:
// 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
For each table with RLS, verify:
- 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 most important tests are the ones that should fail. Always verify that users CANNOT access data they shouldn't. A passing SELECT test doesn't mean much if unauthorized users can also SELECT the same data.
Common Testing Mistakes
- Testing with service_role key: The service_role key bypasses RLS. Always test with the anon key and authenticated users.
- Only testing happy paths: Make sure to test unauthorized access attempts.
- Forgetting INSERT policies: Users might be able to insert data claiming to be another user.
- Not testing after policy changes: Re-run tests whenever you modify RLS policies.
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 Supabase dashboard uses the service_role key, which bypasses RLS. This is by design so you can manage all data. Your app uses the anon key with RLS applied.
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?
Check: 1) RLS is enabled on the table, 2) You have the correct policy for the operation, 3) The policy condition uses the right column name, 4) You're testing with the right user context.
Related guides:How to Set Up Supabase RLS · How to Write RLS Policies · Supabase Security Guide