3.4 Migration lifecycle
KMP has one platform schema and a fleet of tenant schemas. A release is not migration-complete until the central platform database and the core plus every loaded plugin history in each selected tenant have been verified.
Migration tracks
| Track | Location | Connection/history |
|---|---|---|
| Platform | app/config/PlatformMigrations |
platform connection |
| Tenant core | app/config/Migrations |
tenant phinxlog |
| Activities | app/plugins/Activities/config/Migrations |
plugin-specific legacy history table |
| Officers | app/plugins/Officers/config/Migrations |
plugin-specific legacy history table |
| Awards | app/plugins/Awards/config/Migrations |
plugin-specific legacy history table |
| Waivers | app/plugins/Waivers/config/Migrations |
plugin-specific legacy history table |
| Infrastructure plugins with migrations | the loaded plugin’s config/Migrations |
plugin-specific legacy history table |
Migrations.legacyTables remains enabled because fleet inspection,
backup/restore compatibility, and released databases depend on the per-scope
history tables. Do not consolidate or rename them as routine cleanup.
Migration filenames begin with a 14-digit UTC timestamp and a descriptive class
name, for example 20260828120000_AddExampleField.php. The version must be
unique within its owning migration scope and history table; timestamps are
compared as strings. Different scopes may share a timestamp.
Choose the owner first
- Central registry, platform identity, encrypted secret metadata, fleet jobs, schedules, central audit/telemetry, or managed backup metadata: platform.
- Core member/branch/RBAC/gathering/workflow data: tenant core.
- Data used only by a domain plugin: that tenant plugin.
Do not add tenant_id to core/plugin tables to avoid making a platform table.
Do not add member or plugin data to the platform database to enable fleet
reporting.
Writing a migration
- Read the newest migrations in the same track and the applicable
AGENTS.md. - Add one forward migration; never edit a migration that may have shipped.
- Use
App\Migrations\CrossEngineMigrationTraitand existing helpers where the codebase still supports both PostgreSQL and a legacy database engine. - Make schema changes, data backfills, and constraint tightening explicit and ordered. Backfill before making a populated column non-null.
- Add indexes for measured access/concurrency paths and name constraints consistently.
- Keep data transforms idempotent where practical and bounded for large tenant databases.
- Update table/entity/policy/service code and tests in the same change.
A migration must not call tenant HTTP controllers, depend on an authenticated request, read a host header, fetch network resources, or expose secret values. Use a dedicated post-migration command only when a transform cannot safely run inside the migration transaction, and make the release dependency explicit.
Current tenant catalog
TenantMigrationCatalog discovers timestamped PHP migrations from the core app
and every loaded plugin. For each scope it compares the expected versions with
the correct history table and reports:
- missing/pending versions;
- unexpected versions that indicate history drift;
- the highest applied version across scopes; and
- the release target (the highest shipped version).
The recorded tenants.schema_version is an operational summary, not a
replacement for inspecting every history table. A tenant is current only when
all expected scopes are present and no unexpected history exists.
Local development
The supported clean path is:
# From the repository root; destructive to local databases
./dev-reset-db.sh --seed
This rebuilds the local application/platform/test databases, loads the supported baseline seed, applies remaining core/plugin/platform changes, registers the local tenants, reconciles current workflow data, and creates the second tenant. Use it after adding migrations to exercise the complete bootstrap path.
For targeted fleet inspection inside the app container:
bin/cake tenant migrate --all --include-suspended --status
For one local tenant:
bin/cake tenant migrate --tenant kmp --status
bin/cake tenant migrate --tenant kmp --dry-run
bin/cake tenant migrate --tenant kmp
Exactly one selector—--tenant <slug> or --all—is required. A suspended
tenant is excluded unless --include-suspended is present.
Managed release execution
The migration job prepares the platform first, imports missing secret values, ensures backup-key readiness, then migrates active and suspended tenants:
bin/cake platform_migrate migrate
bin/cake platform secrets import-env
bin/cake platform backup-keys ensure --allow-read-only
bin/cake tenant migrate --all --include-suspended --fail-fast
The deployment runbook contains additional schema-cache and model-cache steps
and is authoritative for exact release order. Do not run only bin/cake
migrations migrate against a hosted default connection; that bypasses fleet
selection, locks, pre-migration recovery markers, complete plugin inspection,
and central job records.
Per-tenant safety sequence
For a standard migration, TenantMigrateCommand:
- validates trusted tenant database configuration;
- inspects all core/plugin history and refuses unexpected drift;
- records a central platform job;
- takes a tenant-specific PostgreSQL advisory lock;
- skips a fully current tenant without creating an unnecessary backup;
- otherwise creates an encrypted pre-migration logical backup marker;
- runs pending migration scopes in the tenant connection context;
- clears relevant schema caches;
- re-inspects every history scope and requires the exact target; and
- records the verified tenant schema and job outcome.
--marker-only is useful for an intentional recovery checkpoint. The
--skip-pre-migration-marker and --fake options bypass safety evidence and are
for explicitly reviewed emergency recovery only. They are not release
shortcuts. --target, --date, and rollback-style operations can make a tenant
incompatible with the running application and require an approved recovery
plan.
Failure and retry behavior
A failed tenant remains unavailable if its recorded/current schema is behind the
application requirement. With --all, other tenants may continue unless
--fail-fast is set; managed releases use fail-fast so a gate cannot silently
promote a partially migrated fleet.
Before retrying:
- read the scrubbed platform job result and application logs;
- confirm the advisory lock owner/process is gone;
- inspect every migration scope with
--status; - verify the encrypted marker and key readiness when a change began;
- fix forward or restore according to the runbook; and
- rerun the normal command so final verification and schema recording occur.
Never delete history rows, manually advance schema_version, or use --fake
only to turn a failed gate green.
Suspended tenants
Suspension blocks normal tenant traffic and queue draining, but not explicitly selected fleet maintenance. Releases include suspended tenants so backups remain restorable into the running code and reactivation can require the exact current catalog. Archived tenants follow the retention/recovery policy and are not implicitly selected for normal release migrations.
Plugin rules
- Keep migrations inside the owning plugin namespace and directory.
- Assume every loaded plugin migration runs for every tenant, even if a kingdom does not actively use that feature.
- Coordinate foreign keys only through stable core/plugin contracts.
- Do not reorder active plugins casually; migration/catalog names and bootstrap providers depend on stable loading.
- A plugin removal needs an explicit data retention, schema, restore, and history-table compatibility plan.
Verification checklist
For a migration change, verify at least:
- a clean PostgreSQL database reaches the current schema;
- an existing seeded database upgrades correctly;
- platform and tenant migrations were placed in the correct track;
- all loaded plugin histories report current;
- a second tenant upgrades without reading the first tenant’s data/secrets;
- constraints/indexes match table validation and query behavior;
- backup/restore compatibility is updated when serialized schema changes; and
- targeted tests plus PHPCS/PHPStan pass for changed PHP.
Cross-cutting migration changes should run bash bin/verify.sh when practical.
The managed deployment gates and POC verification remain required before
production promotion.