ADR-0002: Multi-Tenancy Implementation Strategy
Note: this ADR references the warp-storage and warp-catalog crates, which are part of Warp’s commercial distribution and not included in this open-source repository. The rationale is published for transparency.
Date: 2026-05-22 Status: ACCEPTED Accepted: 2026-05-22 Deciders: Yasir Ahmad (CTO)
Context
CONTRACTS.md C-03 requires tenant_id on every
execution, every workflow, every node config, every storage row,
every log line. No single-tenant shortcuts.
Three SaaS tenancy patterns can satisfy this:
- Pool (row-level): shared tables;
tenant_idcolumn on every row; isolation by query predicate + Postgres Row-Level Security. - Bridge (schema-per-tenant): one schema per tenant in the same database. Stronger isolation, more ops overhead (migrations across N schemas).
- Silo (database-per-tenant): one database per tenant. Maximum isolation. Expensive at scale, useful for enterprise / regulated.
The decision affects every storage query and every test we write from Phase 0 onward. Getting this wrong is the multi-tenant retrofit debt C-03 was written to prevent.
Options
| Pattern | Pros | Cons |
|---|---|---|
| Pool | Cheapest, scales horizontally, fewest moving parts | Bugs leak across tenants if app-level scoping fails |
| Bridge | Migration cost per tenant; harder to reason about | Some isolation gain, still single DB blast radius |
| Silo | Strongest isolation; backup-per-tenant trivial | High cost; cross-tenant analytics impossible without ETL |
Decision
Pool model, with two enforcement layers:
-
App-level: every storage query passes through a
TenantScopedConnectionthat injectstenant_idinto every query. Raw query construction is forbidden at the type level — you cannot get a connection without aTenantContext. -
DB-level: Postgres Row-Level Security policies on every tenant-scoped table as defense-in-depth. If app-level scoping ever has a bug, RLS prevents the leak from reaching the wire.
This matches the model Agora already runs on, so the operational playbook is shared.
The C-03 contract test
A single test is the contract for C-03 compliance:
- Fixture creates Tenant A and Tenant B with overlapping primary
keys (same
customer_id, sameorder_id). - The test exercises every read path in the catalog, including paths reached through application code that has been intentionally bugged (e.g., a query builder that forgets to scope).
- The test passes only if zero rows from Tenant B are ever returned to Tenant A, even when the application layer is broken.
If this test passes, C-03 holds. If it ever fails, C-03 is broken
and the build is broken. There is no third state. This test runs on
every PR touching warp-storage.
Enterprise / Silo deferral: customers requiring true database isolation are a Phase 4+ concern. We design the storage layer so that Silo is achievable by changing the connection pool, not by rewriting queries.
Consequences
Positive:
- Cheapest path to Phase 1
- Two independent failure layers (app + RLS) means one bug doesn’t leak data
- Migration path to Silo is preserved by the
TenantScopedConnectionabstraction
Negative:
- RLS adds query planning overhead — measure and accept the cost
- Cross-tenant aggregation (e.g., Lamar Tech analytics) needs a privileged code path that explicitly opts out of
TenantScopedConnection— that path is a security review checkpoint - Backup-per-tenant requires logical export, not pg_dump
Outstanding implementation questions
Resolved by acceptance:
- Pool model commitment for first 1,000 merchants with Silo path reserved for Phase 4+ enterprise tier — YES.
Deferred to implementation:
- Should logs carry
tenant_idas a structured field or a tag? (Affects log aggregation cost and ease of redaction.) - Is RLS overhead acceptable on hot paths like
WhatsAppSenddelivery receipts? Measure before deciding to bypass for that table specifically. If a bypass is needed, it MUST come through a security review checkpoint, not a one-off engineering call.
Enforcement
- CI: a
cargo checklint denies rawsqlx::query!calls outsideTenantScopedConnection. - CI: the cross-tenant leak test described above.
- Code review: any PR that introduces a new tenant-scoped table must add the RLS policy in the same migration.