Multi-tenancy — three enforcement layers
Cavaridge™ enforces tenant isolation at three layers simultaneously. Any one is insufficient. All three are required.
Layer 1 — DB (RLS)
Section titled “Layer 1 — DB (RLS)”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.
Layer 2 — API (middleware)
Section titled “Layer 2 — API (middleware)”Every Express route mounts tenantGuard() middleware, which:
- Extracts the authenticated tenant from the JWT,
- Verifies the tenant has the requested role within the requested resource’s tenant tree, and
- Sets
req.tenantIdfor downstream handlers — and the database connection’sapp.current_tenant_idGUC, 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.”
Layer 3 — UI (provider)
Section titled “Layer 3 — UI (provider)”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.
The carve-out
Section titled “The carve-out”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.
Pitfalls
Section titled “Pitfalls”- Don’t define
tenantsin any app — use the shared one inpackages/auth/. - Don’t hand-roll RLS — use
auth.tenant_visible(). - Don’t read
req.user.tenantIddirectly — usereq.tenantIdaftertenantGuard()runs.
See docs/architecture/CVG-UTM-CONFORMANCE-v1.0.0-20260315.md for the full UTM conformance spec.