Row-Level Security (RLS)#

What is RLS?#

Row-Level Security (RLS) is a database-level security feature that ensures users can only access their own data. See the Overview for why this matters and how it works with route protection.

Quick Start#

1. Setup#

Create .env.test in the backend/ directory:

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_PUBLISHABLE_KEY=your_publishable_key
SERVICE_ROLE_KEY=your_service_role_key

Install dependencies:

npm install

2. Run Tests#

# Run all RLS tests
npm run test:rls

# Run specific test file
npm run test:rls tests/rls.user.test.ts

# Watch mode during development
npm run test -- --watch

3. Expected Output#

✓ tests/rls.user.test.ts (5)
✓ tests/rls.plan.test.ts (4)
✓ tests/rls.session.test.ts (3)
✓ tests/rls.account.test.ts (2)
✓ tests/rls.favourites.test.ts (3)
✓ tests/rls.feedback.test.ts (3)

Test Files  6 passed (6)

Understanding the Tests#

For how RLS policies work and why both security layers are important, see the Overview.

Each test file validates that a specific table is protected:

TableWhat’s Tested
userCan only view/modify own profile
plansCan only access own study plans
sessionCan only see own sessions
accountCannot access other users’ OAuth tokens
favouritesCan only see own favorite courses
feedbackCan only view/edit own feedback

Example Test Pattern#

it('should prevent User 2 from seeing User 1 profile', async () => {
  // Create User 1's data
  const user1Data = await createTestData(ctx.USER_1_SESSION)
  
  // Try to access as User 2
  await setUser2Session(ctx.supabase, ctx.USER_2_SESSION)
  const { data } = await ctx.supabase
    .from('user')
    .select('*')
    .eq('id', user1Data.id)
  
  // RLS blocks access
  expect(data).toEqual([])
})

Common Issues#

IssueSolution
Tests fail with “permission denied”Call setUser1Session() or setUser2Session() before queries
Tests return data for other usersVerify RLS policy includes user_id = auth.uid() in WHERE clause
Cleanup fails with foreign key errorsThis is normal—test data is isolated between runs
Tests are slowExpected—each test runs fresh setup/teardown for isolation

Key Files#

FilePurpose
tests/rls.setup.tsCore setup/teardown logic
tests/rls.session-helper.tsFunctions to set user sessions
tests/rls.*.test.tsTests for each protected table
vitest.config.tsVitest configuration

Best Practices#

Do:

  • Run tests before pushing changes
  • Test both positive (user can access their data) and negative (user can’t access others’ data) cases
  • Add tests when creating new protected tables
  • Keep .env.test secure (add to .gitignore)

Don’t:

  • Share SERVICE_ROLE_KEY (it bypasses RLS)
  • Run tests in parallel (use sequential mode)
  • Commit .env.test to git

Further Reading#

Complete Security Picture#

This is Layer 2 (database). See: