Row-Level Security (RLS) is the foundation of Supabase's security model. Testing RLS policies ensures your users can only access their own data. With supabase-test, you can simulate different users and roles to verify your security policies work correctly.
In this lesson, you'll add RLS policies to your pets table and test them by switching between user contexts.
Supabase uses RLS to secure multi-tenant applications. Users should only access their own data, and anonymous users should have limited permissions. Without testing, you risk:
supabase-test makes RLS testing simple with the setContext() method—switch between users and roles to verify your security policies.
Adding RLS to to your Supabase schema
Add RLS policies to your existing pets table:
pgpm add rls_pets --requires pets_app
Edit deploy/rls_pets.sql:
-- Deploy: rls_pets-- requires: pets_app-- Add owner_id column to track pet ownersALTERTABLE pets.pets
ADDCOLUMN owner_id UUID;-- Enable RLS on the pets tableALTERTABLE pets.pets ENABLEROWLEVEL SECURITY;-- Grant schema usage to Supabase rolesGRANTUSAGEONSCHEMA pets TO anon;GRANTUSAGEONSCHEMA pets TO authenticated;-- Grant table permissionsGRANTSELECTONTABLE pets.pets TO anon;GRANTSELECT,INSERT,UPDATE,DELETEONTABLE pets.pets TO authenticated;-- Enable RLS on both tablesALTERTABLE pets.pets ENABLEROWLEVEL SECURITY;-- Create RLS policies for users table-- Users can view their own dataCREATE POLICY "Users can view own data"ON pets.pets
FORSELECTUSING(auth.uid()= owner_id);-- Users can update their own dataCREATE POLICY "Users can update own data"ON pets.pets
FORUPDATEUSING(auth.uid()= owner_id);-- Users can insert their own dataCREATE POLICY "Users can insert own data"ON pets.pets
FORINSERTWITHCHECK(auth.uid()= owner_id);-- Users can delete their own dataCREATE POLICY "Users can delete own data"ON pets.pets
FORDELETEUSING(auth.uid()= owner_id);-- Grant permissions to anon usersGRANTUSAGEONSCHEMA pets TO anon;GRANTALLON pets.pets TO anon;-- Grant permissions to authenticated usersGRANTUSAGEONSCHEMA pets TO authenticated;GRANTALLON pets.pets TO authenticated;-- Grant permissions to service role (for admin operations)GRANTUSAGEONSCHEMA pets TO service_role;GRANTALLON pets.pets TO service_role;-- Create indexes for better performanceCREATEINDEXIFNOTEXISTS idx_users_owner_id ON pets.pets(owner_id);
Understanding Supabase Test Clients
supabase-test provides two database clients, just like pgsql-test:
pg - Superuser client for setup/teardown operations
db - User client for testing with Supabase roles (anon, authenticated)
Use pg to insert test data as superuser. Use db with setContext() to test RLS policies.
Testing Supabase RLS Policies
Create __tests__/rls.test.ts:
import{ getConnections,PgTestClient, insertUser
}from'supabase-test';let db:PgTestClient;let pg:PgTestClient;letteardown:()=>Promise<void>;let user1:any;let user2:any;let user3:any;const users =[{ id:'550e8400-e29b-41d4-a716-446655440001', email:'tutorial1@example.com'},{ id:'550e8400-e29b-41d4-a716-446655440002', email:'tutorial2@example.com'},{ id:'550e8400-e29b-41d4-a716-446655440003', email:'tutorial3@example.com'}];beforeAll(async()=>{({ pg, db, teardown }=awaitgetConnections());// insert users using pg PgTestClient to bypass RLS (required to insert into supabase auth.users) user1 =awaitinsertUser(pg, users[0].email, users[0].id); user2 =awaitinsertUser(pg, users[1].email, users[1].id); user3 =awaitinsertUser(pg, users[2].email, users[2].id);});afterAll(async()=>{awaitteardown();});beforeEach(async()=>{await db.beforeEach();});afterEach(async()=>{await db.afterEach();});it('should allow user to create their own user record',async()=>{// set context to simulate authenticated user db.setContext({ role:'authenticated','request.jwt.claim.sub': user1.id});// user can create their own petconst pet =await db.one(`INSERT INTO pets.pets (name, breed, owner_id)
VALUES ($1, $2, $3)
RETURNING id, name, breed, owner_id`,['Fido','Labrador', user1.id]);expect(pet.name).toBe('Fido');expect(pet.breed).toBe('Labrador');expect(pet.owner_id).toBe(user1.id);});it('should prevent user1 from updating user2\'s record',async()=>{// user2 creates a pet db.setContext({ role:'authenticated','request.jwt.claim.sub': user2.id});const pet =await db.one(`INSERT INTO pets.pets (name, breed, owner_id)
VALUES ($1, $2, $3)
RETURNING id, name, breed, owner_id`,['Buddy','Golden Retriever', user2.id]);expect(pet.owner_id).toBe(user2.id);// user1 tries to update user2's pet - should throw db.setContext({ role:'authenticated','request.jwt.claim.sub': user1.id});awaitexpect( db.one(`UPDATE pets.pets
SET name = $1
WHERE id = $2
RETURNING id, name, breed, owner_id`,['Hacked Name', pet.id])).rejects.toThrow();});it('should allow users to see only their own data in list queries',async()=>{// set context to user1 db.setContext({ role:'authenticated','request.jwt.claim.sub': user1.id});// create multiple users as adminawait db.one(`INSERT INTO pets.pets (name, breed, owner_id)
VALUES ($1, $2, $3)
RETURNING id`,['Fido','Labrador', user1.id]);// set context to user1 db.setContext({ role:'authenticated','request.jwt.claim.sub': user2.id});await db.one(`INSERT INTO pets.pets (name, breed, owner_id)
VALUES ($1, $2, $3)
RETURNING id`,['Buddy','Golden Retriever', user2.id]);// set context to user1 db.setContext({ role:'authenticated','request.jwt.claim.sub': user3.id});await db.one(`INSERT INTO pets.pets (name, breed, owner_id)
VALUES ($1, $2, $3)
RETURNING id`,['Rex','German Shepherd', user3.id]);// set context to user1 db.setContext({ role:'authenticated','request.jwt.claim.sub': user1.id});// user1 should only see their own record in a list queryconst allUsers =await db.many(`SELECT id, name, breed, owner_id FROM pets.pets ORDER BY name`);expect(allUsers.length).toBe(1);expect(allUsers[0].owner_id).toBe(user1.id);expect(allUsers[0].name).toBe('Fido');expect(allUsers[0].breed).toBe('Labrador');});
The setContext() method sets the role and user context for the current transaction. All subsequent queries run with that authentication context.
Run the tests:
pnpmtest
Key Takeaways
pg client - Use for superuser operations (setup/teardown)
db client - Use for testing with Supabase roles (anon, authenticated)
setContext() sets authentication context for the current transaction
Use pgpm add to add RLS policies as proper database changes
Test isolation ensures RLS context doesn't bleed between tests
What's Next
You've added RLS policies to your Supabase table and tested basic user access scenarios. Next, you'll learn how to seed Supabase test databases with auth.users and realistic data using efficient loading strategies.