Row-Level Security
Overview
PostgreSQL Row-Level Security (RLS) provides a database-enforced guarantee that queries cannot return data belonging to a different tenant, even in the event of application-layer bugs.
How It Works
RLS policies reference a transaction-scoped PostgreSQL session variable. This variable is set at the start of each database transaction and automatically cleared when the transaction ends via SET LOCAL.
Fail-Closed Behavior
If the tenant context variable is not set, the RLS policy evaluates to false for all rows, returning zero results. This prevents data leaks from unscoped queries.
RLS-Enabled Tables
All core data tables have RLS policies enabled, including:
customersaccountstransactionsworkflowsworkflow_runs- Additional tables added via expansion migrations
Application Integration
Tenant Context Setting
Before executing tenant-scoped queries, the application sets the tenant context within a database transaction. The tenant ID is validated as a UUID before use, preventing injection attacks.
Transaction Wrapper
All repository operations that require RLS protection are wrapped in a transaction function that automatically sets and clears the tenant context. This is handled by the base repository class, ensuring consistent behavior across all data access paths.
Testing
RLS policies are verified in automated tests that confirm:
- Tenant A cannot see Tenant B's data
- Queries without a tenant context return zero rows
- UUID validation rejects malformed tenant identifiers