ADR 000: Multi-Tenant Architecture Design
Date: 2026-04-22
Status: Accepted โ partially superseded 2026-05-29 by ADR-020 (isolation matrix replaces the single-axis org_id framing) and ADR-015 (subdomain SSL strategy). Read this ADR for historical context; ADR-020 is canonical for isolation rules.
Source: Architecture decision record
Contextโ
h.work needs to serve multiple client organizations (tenants) from a single platform deployment while maintaining strict data isolation. The design must:
- Prevent any client org from seeing another org's data
- Allow internal roles (superadmin, AM, expert) to operate across orgs with appropriate scoping
- Scale to ~1,200 client orgs by Month 36 (PRD target)
- Maintain 72% gross margin โ per-org infrastructure is expensive
The core question: dedicated database per org vs. shared database with logical isolation (RLS)?
Decisionโ
Shared PostgreSQL database with logical multi-tenancy. The default scope is org-level; the canonical isolation matrix (which resources additionally require specialist_id, osa_id, or dual-scope expert_access) is now defined by ADR-020 and supersedes the single-axis framing originally captured here.
Isolation Mechanism (Two-Layer)โ
Layer 1 โ Application: OrgRlsInterceptor (NestJS APP_INTERCEPTOR) sets app.current_org_id via SET LOCAL on every request. When the request resolves to a Specialist context, it additionally sets app.current_specialist_id โ see ADR-020 for the dual-GUC model. All TypeORM queries are scoped through these GUCs.
Layer 2 โ Database: PostgreSQL RLS policies enforce isolation at the DB level. Internal roles bypass via explicit two-tier policy (not BYPASSRLS).
Org Identityโ
Most tenant entities carry org_id UUID NOT NULL as the base partition key. Per ADR-020: PII/Conversation/Message/ExpertQueueItem tables additionally require specialist_id NOT NULL; cost/audit tables key on osa_id FK. See the ADR-020 matrix for the per-resource-class rules.
Internal roles do not have org_memberships rows โ their cross-org access is governed by service-layer checks.
Org Identity (Path-Based Routing)โ
Org identity in the live product is path-based (/client/*) per CD-12 in OPEN_DECISIONS.md. The {slug}.h.work subdomain SSL strategy is captured by ADR-015; subdomain framing in the historical section below is preserved for context only.
Consequencesโ
Benefits:
- Operationally simple โ one DB to manage, one backup, one scaling unit
- Low marginal cost per new org โ no new infrastructure
- pgvector embeddings share the same instance; no per-tenant vector DB needed
Trade-offs:
- Noisy neighbor risk โ a large org's heavy queries can affect others. Mitigated by per-org rate limiting (planned) and query timeouts.
- RLS complexity โ policies must be maintained carefully; any gap in coverage creates a data leak risk. RLS policies have shipped on onboarding + Tavus-integration tables (per issue #3, closed 2026-05-24); broader coverage (conversations, messages, expert_queue) is tracked separately.
- Enterprise isolation requirement โ some enterprise clients may require dedicated DB instances. This is recorded as a future option (no active issue tracking; revisit if/when a real enterprise lead emerges).
Decision affirmed: GitHub issue #27 (DB isolation & agent security model) closed 2026-04-28, ratifying the shared-Postgres + RLS approach captured in this ADR. The architecture can evolve to hybrid (shared for SMB, dedicated for enterprise) if a real customer demands it.
Note: Org identity is path-based (
/client/*) in the live product per CD-12 in OPEN_DECISIONS.md. The{slug}.h.worksubdomain framing above is the historical design; the running platform uses path-based routes. Update planned in a follow-up ADR revision.