Skip to content

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

ConcernOld modelNew model (Wave 3)
Application homeBelongs to exactly one Tenant (createApplication(tenantId))Belongs to a Project; defined once ((slug, project) unique)
Tenant meaningDoubled as an environment (tenant.type = local|dev|qas|run)Customer organization (account); tenant.type removed
EnvironmentsModeled as tenantsFirst-class Environment entity (dev | qas | run) per project
App ↔ tenantOne app row per tenantTenant2Application enablement join (enabled, settings)
Config cascade leafApplication's own settingsTenant2Application settings (per tenant × app)
Config routeGET /api/frontend-config/:application_idGET /api/frontend-config/:application_id/:tenant_id (old kept 1 release)
Cross-tenant copyapplication promote between tenantsRemoved; copy config between environments
Backend registrationPer tenantOnce per (application, environment) via environment-scoped machine user
Trigger delivery(Intended) per-tenantOnce per deployment; backend fans out over enabled tenants
Machine user scopeTenant(project, environment)
Registered backendGains an environment edge
Tenant rolesFixed enum (readonly|normal|tester|developer|maintainer|owner)Configurable per project over a fixed permission vocabulary
Proxy vhostsHand-authored Caddyfile per appRendered from Manager data, pushed via Caddy admin API
Deployment artifactsHand-authored HCL + CaddyfileRendered from a spec / install.yaml
Tenant selectionUnsigned cookie, no membership checkHMAC-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 shapeMigrates 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 settingsCopied onto the corresponding Deployment / environment config
application promote dev → qasRemoved — copy config between environments instead
Machine user in tenant runMachine user scoped to (project, run environment)
Registered backend per tenantOne registration per (application, environment)
Domains on the promoted appDeployment.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 shapeMigrates 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 settingsTenant2Application row per (customer tenant, app) carrying those settings
The "which apps this customer has" grantTenant2Application.enabled
Separate staging/production tenantsEnvironment rows qas / run
Per-customer backend registrationOne registration per (application, environment); backend fans out over enabled tenants
Per-customer frontend-config sourceOne tenant-qualified route .../:application_id/:tenant_id
SPA deep-linking a bare /_tenantSigned 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.roleNew seeded tenant role
ownerowner (all permissions)
maintaineradmin (+ members / invites / apps.configure)
everything elsemember (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:

bash
# 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:

bash
atlas migrate apply --dir file://ent/migrate/migrations --url "$PFY_DB_URL" \
  --baseline 20260717220031

The 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:

bash
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.yaml

The proxy vhosts are rendered from Manager data and pushed automatically; force a reconcile and check for drift:

bash
pfy proxy sync
pfy proxy diff

See 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