← Data architecture

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

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

  1. Read the newest migrations in the same track and the applicable AGENTS.md.
  2. Add one forward migration; never edit a migration that may have shipped.
  3. Use App\Migrations\CrossEngineMigrationTrait and existing helpers where the codebase still supports both PostgreSQL and a legacy database engine.
  4. Make schema changes, data backfills, and constraint tightening explicit and ordered. Backfill before making a populated column non-null.
  5. Add indexes for measured access/concurrency paths and name constraints consistently.
  6. Keep data transforms idempotent where practical and bounded for large tenant databases.
  7. 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:

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:

  1. validates trusted tenant database configuration;
  2. inspects all core/plugin history and refuses unexpected drift;
  3. records a central platform job;
  4. takes a tenant-specific PostgreSQL advisory lock;
  5. skips a fully current tenant without creating an unnecessary backup;
  6. otherwise creates an encrypted pre-migration logical backup marker;
  7. runs pending migration scopes in the tenant connection context;
  8. clears relevant schema caches;
  9. re-inspects every history scope and requires the exact target; and
  10. 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:

  1. read the scrubbed platform job result and application logs;
  2. confirm the advisory lock owner/process is gone;
  3. inspect every migration scope with --status;
  4. verify the encrypted marker and key readiness when a change began;
  5. fix forward or restore according to the runbook; and
  6. 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

Verification checklist

For a migration change, verify at least:

Cross-cutting migration changes should run bash bin/verify.sh when practical. The managed deployment gates and POC verification remain required before production promotion.