Skip to content

Multi-tenancy — three enforcement layers

Cavaridge™ enforces tenant isolation at three layers simultaneously. Any one is insufficient. All three are required.

Every tenant-scoped table has a tenant_id UUID FK to the shared tenants table, with a Row-Level Security policy via the shared auth.tenant_visible() function. A leaked or buggy API can never bypass RLS — the database itself rejects the read.

Every Express route mounts tenantGuard() middleware, which:

  1. Extracts the authenticated tenant from the JWT,
  2. Verifies the tenant has the requested role within the requested resource’s tenant tree, and
  3. Sets req.tenantId for downstream handlers — and the database connection’s app.current_tenant_id GUC, which RLS reads.

Without tenantGuard, RLS sees no tenant and returns zero rows. So even when middleware is forgotten, the failure mode is “no data” — not “wrong data.”

Every UI wraps in TenantProvider, which propagates the resolved tenant context through React context. Components receive tenant-scoped data via hooks, never via global stores.

apps/ceres (Cavaridge Nurse Tools) is the only public, no-auth, no-tenant app. PROD-01 added apps/marketing and PROD-04 added apps/docs as additional auth-exempt carve-outs (registered in packages/naming/manifest.json). Every other app is tenant-scoped.

  • Don’t define tenants in any app — use the shared one in packages/auth/.
  • Don’t hand-roll RLS — use auth.tenant_visible().
  • Don’t read req.user.tenantId directly — use req.tenantId after tenantGuard() runs.

See docs/architecture/CVG-UTM-CONFORMANCE-v1.0.0-20260315.md for the full UTM conformance spec.