Appearance
Multi-tenancy & routing
Chapter 3. You've seen the services. Now the defining question: how does one shared system serve thousands of stores without ever mixing them up?
How one shared platform serves many stores, tells them apart safely, and routes each request — without ever letting one store read another. This is the heart of the "Shopify for India, but cheaper" idea.
The one mental shift
- Old way: one app per store — a store is a separate server. Isolation is physical; cost is O(N).
- Ratio-3.0: one shared app serves all stores; a store is just data keyed by a
tenant_id. Isolation is enforced in code; cost is O(1) — adding a store means inserting rows, not starting a server.
So every request must answer two questions a separate server used to answer for free:
- Which tenant is this for? → resolution
- Which page of that tenant do I render? → routing
End-to-end: from hostname to page
Resolution — how a request finds its tenant (security-critical)
A shopper hits a hostname, of two kinds:
- Platform subdomain — every store gets one automatically (e.g.
acme.ratiodev.in). Covered by one wildcard TLS cert, so it works instantly at onboarding. - Custom domain — optional (e.g.
shop.acme.com). Needs its own cert (see custom domains).
The steps:
- The request hits the edge.
- The edge looks up
host → tenantIdin Workers KV (TENANTS), falling back to the DB only on a miss — and only matching a verified domain. - The edge forwards to the origin over a private path and injects a trusted header
x-ratio-tenant. - The origin trusts only that header — it never reads the public
Hostto decide the tenant.
The rule to burn in
tenantId is never derived from anything a shopper can set — not the Host at the origin, not a query param, not a cookie. The network boundary (a private origin that fails closed without x-edge-auth) is what makes the injected header trustworthy.
Routing — how it then finds the page
The origin now knows tenantId + a path. There are no per-store file routes — routing is data-driven. First match wins:
The point: a merchant can publish a brand-new route just by adding data — no code, no rebuild. Reserved paths always win, so a merchant page can never shadow /cart or /api/*.
Isolation — how stores never leak into each other
One shared app; a store is data keyed by tenant_id. Isolation is enforced in code, not by separate servers:
| Layer | How it works |
|---|---|
| Storage | One shared Postgres; every row keyed tenant_id; S3/KV keys namespaced per store/host |
| The one gate | A tenant-scoped repository (forTenant(id)) — every read/write injects tenantId; a query with no tenantId can't be expressed → it throws, never a global scan |
| Proof | A blocking CI test: "tenant A can never return tenant B" — fails the build if it regresses |
| Routing | Only verified = true host→store claims count; an unverified squat never serves |
| Compute | Stateless origin — any replica serves any store; the store comes only from the trusted edge header |
| Secrets | Rows store a secretRef; the real credential is pulled into memory only for the call, never saved or logged |
| Fairness | Per-tenant + per-/assistant rate limits (in Postgres), plus a per-request render budget |
| Edge auth | The origin is private; it needs x-edge-auth and refuses if unset |
Tenant lifecycle
- Onboard: insert the tenant row (
pending) → seed the base theme + default pages → assign a platform subdomain →active. Seconds; no repo/build/deploy. - Suspend: serve a "store paused" page; keep the data. (Suspension is enforced at the origin, not by deleting the KV route.)
- Delete: purge cache → delete rows/blobs/secret → tombstone. Must be provably complete (India-DPDP hard-delete).
Status today: read, but not written
The origin serves only status = 'active' and 404s anything else, but no code path currently writes suspended / pending / deleting — the enforcement is in place; those transitions are done out-of-band for now. Deletion is a hard delete with a proof, not a deleting state.
Deeper reading
Confluence: S2 — Multi-tenancy & Routing (deep dive) and the System Architecture (S0–S8) page. See the ADR index.