← All decisions

Tenant config: repo bootstraps, DB authoritative at runtime

accepted

0004 — Tenant config: repo bootstraps, DB authoritative at runtime

  • Status: accepted
  • Date: 2026-05-06
  • Deciders: Derek

Context

Each tenant has settings: org name, domains, theme name, enabled modules, integration credentials, content type seed, branding assets. Two failure modes to avoid:

  • DB-only config. Nothing in git, no diff for proposals, no review trail, can’t bootstrap a new tenant from a config file. Any change to a tenant’s config is invisible to engineers.
  • Repo-only config. Admins can’t change anything without a deploy. Kills the “you own this” promise and forces engineering to be a bottleneck on every tenant change.

Decision

A two-layer config model:

  1. tenants/<slug>/tenant.config.ts — bootstrap source of truth. Validated via Zod schema in packages/tenant-config. Used to create the org row at onboarding time. Lives in git (or a private per-tenant repo).
  2. organizations row in the DB — runtime source of truth. Mutable from the admin UI. Drives the live system once the tenant exists.

A nightly job compares the two and surfaces drift. Drift isn’t an error — admins are supposed to change runtime values from the UI. Drift is informational: “this tenant’s runtime diverges from its bootstrap config; if you re-bootstrap from tenant.config.ts, here’s what would change.”

Consequences

Easier:

  • A new tenant is cp -R tenants/_example tenants/new-org && edit && pnpm new-tenant new-org — one command end-to-end
  • Engineers get diff/review on tenant changes that ride in PRs
  • Admins get UI-driven changes for everything else — no “wait for a deploy”
  • Disaster recovery: re-bootstrap a tenant from its config file; restored data is immutable

Harder:

  • Drift is a real concept; we explain it in the admin UI and provide tools to reconcile
  • Two writers, one truth — the contract is that tenant.config.ts is authoritative at bootstrap time only; after that, the DB wins until explicitly re-bootstrapped

Alternatives considered

  • DB-only with admin UI. Loses git review for engineering changes. Bootstrap becomes “click through admin UI” — bad for AI iteration and bad for repeatability.
  • Repo-only. Admins file PRs to change their own org’s name. Hard veto.
  • Single source via “config is data, data is config” (Hasura-style). Conceptually pure but the operational complexity isn’t worth it at this scale.