Skip to main content

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.work subdomain framing above is the historical design; the running platform uses path-based routes. Update planned in a follow-up ADR revision.