How to Test Supabase RLS Policies

Share
How-To Guide

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:

1

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;
2

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;
3

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:

1

Open DevTools Network tab

Open your app in a browser, sign in as a test user, and open DevTools (F12) → Network tab.

2

Observe Supabase requests

Filter for requests to your Supabase URL. Look at the response data to verify only authorized data is returned.

3

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

How-To Guides

How to Test Supabase RLS Policies