Wave 3 Migration Guide — Re-architecture Release Train
Wave 3 is a coordinated release train: the Manager, proxy, CLI, and integration libraries change together, backed by a single versioned data migration. It re-homes applications to the project level, makes tenants mean customer organizations, introduces first-class Environments, and renders every deployment artifact instead of hand-authoring it.
Pre-1.0 breaking change — upgrade the whole stack together
This is one release train. An old proxy against a new Manager (or the reverse) is not supported and fails loudly — the config renderer stamps a minimum proxy version and the proxy plugin rejects configs it does not understand, rather than silently misrouting. Upgrade the Manager, proxy, CLI, and libraries to the same release, and run the data migration once.
What changed, at a glance
| Concern | Old model | New model (Wave 3) |
|---|---|---|
| Application home | Belongs to exactly one Tenant (createApplication(tenantId)) | Belongs to a Project; defined once ((slug, project) unique) |
| Tenant meaning | Doubled as an environment (tenant.type = local|dev|qas|run) | Customer organization (account); tenant.type removed |
| Environments | Modeled as tenants | First-class Environment entity (dev | qas | run) per project |
| App ↔ tenant | One app row per tenant | Tenant2Application enablement join (enabled, settings) |
| Config cascade leaf | Application's own settings | Tenant2Application settings (per tenant × app) |
| Config route | GET /api/frontend-config/:application_id | GET /api/frontend-config/:application_id/:tenant_id (old kept 1 release) |
| Cross-tenant copy | application promote between tenants | Removed; copy config between environments |
| Backend registration | Per tenant | Once per (application, environment) via environment-scoped machine user |
| Trigger delivery | (Intended) per-tenant | Once per deployment; backend fans out over enabled tenants |
| Machine user scope | Tenant | (project, environment) |
| Registered backend | — | Gains an environment edge |
| Tenant roles | Fixed enum (readonly|normal|tester|developer|maintainer|owner) | Configurable per project over a fixed permission vocabulary |
| Proxy vhosts | Hand-authored Caddyfile per app | Rendered from Manager data, pushed via Caddy admin API |
| Deployment artifacts | Hand-authored HCL + Caddyfile | Rendered from a spec / install.yaml |
| Tenant selection | Unsigned cookie, no membership check | HMAC-signed, membership-checked, hosted selector |
Two install shapes to migrate
Existing installs abused the single-tenant model in one of two ways. Both are migrated by the versioned data migration; the mapping differs.
Shape A — Tenant-as-environment
You created a tenant per environment (dev, qas, run) and cloned each application into each tenant, using application promote to copy config between them.
| Old shape | Migrates to |
|---|---|
Tenant dev / qas / run (tenant.type set) | Environment rows dev / qas / run on the project |
App shop cloned in dev, qas, run (3 rows) | One project-level Application shop |
| Each clone's settings | Copied onto the corresponding Deployment / environment config |
application promote dev → qas | Removed — copy config between environments instead |
Machine user in tenant run | Machine user scoped to (project, run environment) |
| Registered backend per tenant | One registration per (application, environment) |
| Domains on the promoted app | Deployment.domains for (application, environment) |
Result: three cloned app rows collapse into one application with three deployments — one per environment.
Shape B — Tenant-as-account (the MyTask consumer)
You created a tenant per customer and cloned the application per customer (mytask-app cloned per tenant), and separately abused tenants for staging/production.
| Old shape | Migrates to |
|---|---|
| Tenant per customer (Customer A, Customer B, …) | Tenant = customer organization (unchanged meaning, type dropped) |
App mytask-app cloned per customer tenant (N rows) | One project-level Application mytask-app |
| Each customer clone's settings | Tenant2Application row per (customer tenant, app) carrying those settings |
| The "which apps this customer has" grant | Tenant2Application.enabled |
| Separate staging/production tenants | Environment rows qas / run |
| Per-customer backend registration | One registration per (application, environment); backend fans out over enabled tenants |
| Per-customer frontend-config source | One tenant-qualified route .../:application_id/:tenant_id |
SPA deep-linking a bare /_tenant | Signed cookie + hosted selector + available_tenants in window.__PRODUCTIFY__ |
In-app RBAC (five roles + CanPerform) | Unchanged — apps keep owning capability; the platform only forwards the membership fact (opt-in) |
Result: N cloned app rows collapse into one application enabled for N customer tenants, each carrying its own config on the enablement leaf.
User & role mapping (both shapes)
Seeded tenant roles are created per project, and old user2tenant.role enum rows are mapped:
Old user2tenant.role | New seeded tenant role |
|---|---|
owner | owner (all permissions) |
maintainer | admin (+ members / invites / apps.configure) |
| everything else | member (apps.read) |
System user.role and project roles stay fixed enums. See the Access Model for the fixed permission vocabulary and precedence.
Migration steps
1. Back up and run the versioned migration
The schema change lands via versioned migrations, not boot-time auto-migrate. After deploying the new Manager binary, apply the migration and the bundled data migration:
# Apply the versioned schema + data migration
atlas migrate apply --dir file://ent/migrate/migrations --url "$PFY_DB_URL"Baseline first if your DB predates versioned migrations
Installs created before versioned migrations must be baselined to the 20260717220031 baseline revision before applying, so Atlas does not attempt to re-create existing tables:
atlas migrate apply --dir file://ent/migrate/migrations --url "$PFY_DB_URL" \
--baseline 20260717220031The data migration collapses cloned app rows to project-level applications, converts old per-tenant rows into Tenant2Application, maps tenant.type onto Environments, re-points machine users and registered backends to environments, and seeds tenant roles. Take a database backup first; the change is irreversible without one.
2. Reconcile deployments and render config
Applications now carry a deployment spec per environment. Populate it (UI or pfy deployment set), then render:
pfy deployment get --env run -o yaml # inspect the migrated spec
pfy nomad render --env run # render the app job
pfy platform render --target nomad # render the platform stack from install.yamlThe proxy vhosts are rendered from Manager data and pushed automatically; force a reconcile and check for drift:
pfy proxy sync
pfy proxy diffSee Generated Artifacts for the full workflow and the first-consumer deployment migration note.
3. Move consumers to the tenant-qualified config route
The tenant-less config route is kept for one release, deprecated:
- Old:
GET /api/frontend-config/:application_id— serves project + application defaults only. - New:
GET /api/frontend-config/:application_id/:tenant_id— full project → tenant → tenant2application cascade; 404 if the app is not enabled for the tenant.
Update integration libraries to the tenant dimension (getFrontendConfig(appId, tenantId) / FrontendConfigForTenant) and register once per (application, environment). The old route will be removed in the following release.
4. Version handshake (fail loudly, don't misroute)
Confirm the proxy and Manager are on the same release. The rendered proxy config carries a minimum proxy version; a proxy that predates it rejects the config and keeps its last good state rather than serving a stale or wrong route. Watch the Manager's proxy-config status (and pfy proxy diff) after the upgrade — a persistent rejection means a version mismatch, not a transient reload failure.
Acceptance check
The migration is complete when, on the new stack:
- One proxy and one registered backend serve app A for tenants T1/T2 and app B for T1.
- Onboarding tenant T3 to app A is Manager rows only — no proxy edit, no backend redeploy.
- A user in T1 ∩ T2 switches tenants via the selector and the backend sees the new
X-Tenant-ID. - There is zero hand-written HCL or Caddyfile in the repo or on the box.
See Also
- Multi-tenancy — the new model in depth
- Configuration Layers — the cascade and its new leaf
- Access Model — configurable roles and the fixed permission vocabulary
- Generated Artifacts — install.yaml, specs, and the
pfyrender/deploy commands