Skip to main content

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_id columns 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