Skip to content

ADR-0002: Tenancy and row-level security ​

Status: accepted (2026-10-12)

Decision ​

  • Every tenant-owned table has a non-null tenant_id uuid and row-level security with both ENABLE and FORCE ROW LEVEL SECURITY.
  • Policies compare tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid for USING and WITH 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 any tenant_id table without forced RLS.
  • Roles:
    • quavon_migrator owns the schema and runs migrations (DATABASE_MIGRATOR_URL).
    • quavon_app is a non-owner role without BYPASSRLS, limited to DML (DATABASE_URL). All request-driven code uses it.
    • quavon_system has BYPASSRLS and DML only, no DDL (DATABASE_SYSTEM_URL). Only withSystem uses 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 runs SELECT set_config('app.tenant_id', $1, true) (equivalent to SET LOCAL), so the setting never leaks across pooled connections.
  • Cross-tenant system work (schedulers, reconciliation, installation lifecycle) uses withSystem(fn) on the quavon_system connection. withSystem fails if no system connection is configured.
  • audit_log is append-only and hash-chained; a trigger rejects UPDATE and DELETE.

Consequences &ZeroWidthSpace;

  • A missing withTenant fails closed (no rows) instead of leaking data.
  • The RLS isolation suite runs in CI on every change and is a required check.

The open core is released under the Apache License 2.0.