← All decisions jacob@stephens.page
Decision Record

Per-app copy-deployed sync services over a shared multi-tenant backend

ADR 0012 · Accepted ยท in production · ~577 words

Context

A proven account+sync backend already existed for one app: email+password auth, opaque server-side sessions (httpOnly cookie for web, bearer token for the native WebView), email verification, password reset, rate limiting, and a per-user cloud-save store. A second app now needed the same accounts-and-sync capability.

The instinct is to promote it into a shared multi-tenant backend: one process, one database, an app_id column, both apps' users behind one auth system. That looks like the "platform" choice and reuses the most code.

But the two apps disagree on the part that matters. The first stores row-structured records and syncs with row-level last-write-wins plus tombstones. The second stores a single opaque save blob and needs whole-object conflict detection; row-merging an opaque blob is meaningless and manufactures corrupt hybrids. So "share the backend" shares only the auth/session tables (a few hundred lines of idempotent DDL that copy verbatim), not the sync engine, while forcing one process to carry two incompatible sync models behind an app_id discriminator. And it couples the blast radius: a bad migration or runaway DELETE on the second app's release cadence now reaches the first app's production data.

Decision

Copy-deploy the skeleton as a separate service per app:

Consequences

When I'd revisit

If the fleet grows enough that hand-applying auth-skeleton fixes across copies becomes the dominant maintenance cost, extract the auth/session layer into a shared, versioned library (still copy-run, shared code) before considering a shared runtime. A genuine need for cross-app identity - one account spanning several apps, single sign-on - is the case that justifies a multi-tenant backend, where the coupling buys something the per-app copies can't. Short of those, the per-service isolation wins.

One of a set of architecture decision records. Source markdown lives in the infrastructure-patterns repo, which is the canonical copy.