Frog Wizard

Tenants are data, not code

When you have to scale to hundreds of tenants, managing each one of them in code stops being tractable.

A tenant is data. The infrastructure is code. Keep them in different places and onboarding a client becomes writing one JSON file, no new Terraform, no new pipeline job, no new repo.

It works by separating what from how, with a third layer binding them:

A brand-new env is six fields:

{
  "schemaVersion": "1.0",
  "clientId": "acme",
  "environment": "prod",
  "region": "us-east-1",
  "appVersion": "2025.4.1",
  "network": { "vpcCidr": "10.20.0.0/16", "azCount": 2 }
}

appVersion is the join key. Its manifest declares the infra that build needs:

{
  "appVersion": "2025.4.1",
  "moduleVersions": {
    "terraform": { "network": "3.2.0", "compute": "5.1.2", "data": "2.0.4" },
    "ansible":   { "app-config": "1.8.0", "app-deploy": "4.0.0" }
  }
}

So "newer versions need different infra" stops being a fork. The infra requirement travels with the app version; a client adopts a new shape by bumping one field.

The pipeline resolves the config against the manifest and the pinned modules, assembles an ephemeral workspace, and applies it. Nothing per-client is committed to git.

Overrides, feature-flag flips, and hotfixes are optional blocks on the same file, merged by precedence. The base case stays six fields.