ADR-0003 — Single-tenant per shop
- Status: Accepted
- Date: 2026-04-15
- Decision-makers: Tom Anderson
Context
How are multiple shops' data isolated from each other?
Two main models:
- Multi-tenant: one big database (or set of databases) shared across all shops, with
tenant_idcolumns gating each row to its shop. Standard SaaS pattern. - Single-tenant: each shop gets its own database (and its own Worker). No shop_id columns; isolation is at the deployment layer.
The trade is well-known: multi-tenant is operationally cheaper at scale; single-tenant offers stronger isolation guarantees and simpler reasoning per shop.
For Helm specifically:
- Trust matters — shop owners are handing us their entire customer database
- Compliance matters — data residency, erasure, audit access all become single-shop operations
- Failure isolation matters — a query mistake in Shop A must not affect Shop B
- Cloudflare's per-Worker + per-D1 model makes single-tenant operationally cheap
Decision
Single-tenant per shop. Each shop deployment is one Worker, one D1 database, one R2 bucket. No tenant_id / shop_id columns anywhere in the schema.
Consequences
Positive:
- A bug in the Worker cannot reach another shop's data; they're on different storage with different bindings
- Per-shop backups, restores, exports are trivial (single file or single bucket)
- Compliance (GDPR-style erasure, data residency) is straightforward
- Stronger trust pitch to shop owners
- The schema reads like a single-app schema — simpler to reason about
Negative:
- Schema migrations run N times (once per shop), not once globally. Mitigation: keep migrations fast; deploy script handles the fan-out.
- No cross-shop queries. Mitigation: an analytics pipeline (when needed) copies anonymized data to a separate multi-tenant store; the operational DBs stay single-tenant.
- Cost-per-shop floor (small Cloudflare minimums per Worker + per D1). Mitigation: amortizable over the per-shop revenue.
- Per-shop ops surface scales linearly. Mitigation: the observability discipline keeps "1 alert across all shops" doable.
Notes
This decision is structural and very hard to reverse. A late move to multi-tenant would require schema changes (add tenant_id everywhere), data merge across N databases, and careful query rewriting. We commit to this with eyes open.
See also
- Single-tenant per shop principle
- Deployment topology
- ADR-0006: Data ownership commitment
- ADR-0025: Federated multi-location — how chain capability is achieved without violating this commitment