ADR-0002: Tenancy and row-level security ​
Status: accepted (2026-10-12)
Decision ​
- Every tenant-owned table has a non-null
tenant_id uuidand row-level security with bothENABLEandFORCE ROW LEVEL SECURITY. - Policies compare
tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuidforUSINGandWITH CHECK. An unset or reset setting (pooled connections keep an empty string after a transaction) matches no rows instead of raising a cast error. - New tenant tables call
quavon_enable_tenant_rls('<table>')in their migration; an integration test fails for anytenant_idtable without forced RLS. - Roles:
quavon_migratorowns the schema and runs migrations (DATABASE_MIGRATOR_URL).quavon_appis a non-owner role withoutBYPASSRLS, limited to DML (DATABASE_URL). All request-driven code uses it.quavon_systemhasBYPASSRLSand DML only, no DDL (DATABASE_SYSTEM_URL). OnlywithSystemuses it, and only services that need cross-tenant work receive its credentials.
- Tenant-scoped access goes through
withTenant(tenantId, fn), which opens a transaction and runsSELECT set_config('app.tenant_id', $1, true)(equivalent toSET LOCAL), so the setting never leaks across pooled connections. - Cross-tenant system work (schedulers, reconciliation, installation lifecycle) uses
withSystem(fn)on thequavon_systemconnection.withSystemfails if no system connection is configured. audit_logis append-only and hash-chained; a trigger rejectsUPDATEandDELETE.
Consequences ​
- A missing
withTenantfails closed (no rows) instead of leaking data. - The RLS isolation suite runs in CI on every change and is a required check.