Two-Tenant Staging POC

Use bin/cake tenant_poc to prove that one app revision can serve two tenants with distinct hosts and databases. The harness is repo-local, idempotent, and does not require Azure credentials; it only needs the app configured for a staging-like PostgreSQL platform datasource.

Safety gates

The command is disabled unless both are true:

export KMP_ENABLE_TENANT_POC=true
bin/cake tenant_poc --yes ...

If APP_ENV=production or CAKE_ENV=production, it also requires --allow-production and KMP_ALLOW_PRODUCTION_TENANT_POC=true. Do not use production customer hosts for the POC.

Local PostgreSQL run

From app/, point platform and default at a local PostgreSQL instance, use the local file secret store, then run:

export KMP_DB_DRIVER=postgres
export PLATFORM_DB_HOST=127.0.0.1
export PLATFORM_DB_USERNAME=kmp_platform_admin
export PLATFORM_DB_PASSWORD='<from local secret manager>'
export PLATFORM_DB_DATABASE=kmp_platform_staging
export KMP_ENABLE_TENANT_POC=true

bin/cake platform_migrate migrate
bin/cake tenant_poc --yes --create-database \
  --tenant-a poc-alpha --host-a poc-alpha.staging.example.test \
  --tenant-b poc-beta --host-b poc-beta.staging.example.test \
  --db-prefix kmp_poc --smoke-table members

The command provisions or updates tenant metadata, creates tenant roles/databases when --create-database is supplied, runs tenant migrations, resolves both hosts through the platform registry, and performs SELECT COUNT(*) FROM members in each tenant database.

For staging environments where the databases and roles are pre-created by infrastructure, replace --create-database with --skip-create-database.

Staging runbook

  1. Deploy the app revision with tenant resolution still disabled for normal traffic unless the staging POC window explicitly enables it.
  2. Configure PostgreSQL platform metadata and tenant DB admin credentials using environment variables or the staging secret store. Do not commit secrets.
  3. Choose two DNS names, for example:
    • poc-alpha.staging.kmp.example.org
    • poc-beta.staging.kmp.example.org
  4. Point both hosts at the staging app ingress.
  5. Run platform migrations: bin/cake platform_migrate migrate.
  6. Run the POC command with KMP_ENABLE_TENANT_POC=true and --yes, using --skip-create-database if infrastructure pre-created the tenant DBs.
  7. Re-run bin/cake tenant_poc --yes --verify-only ... after any ingress or app setting change to verify the same hosts still resolve and smoke successfully.

Tenant migration canary gate

The optional bin/tenant_migration_canary.sh provides an explicit staging rehearsal before promoting a release with tenant migrations. The active Azure workflow does not call this script automatically. When invoked, it provisions a disposable canary-* tenant database, runs tenant migration status, dry-run, and idempotent migrate checks, verifies the smoke table, then drops the canary tenant metadata, database role, database, jobs, and writable secret-store entry. It never uses --all, so real tenants are not selected.

Safety behavior:

Local/staging run from app/:

export KMP_DB_DRIVER=postgres
export PLATFORM_DB_HOST=127.0.0.1
export PLATFORM_DB_USERNAME=kmp_platform_admin
export PLATFORM_DB_PASSWORD='<from local secret manager>'
export PLATFORM_DB_DATABASE=kmp_platform_staging
export KMP_ENABLE_TENANT_CANARY=true

bash bin/tenant_migration_canary.sh

Optional controls:

Workflow-ready hook:

- name: Tenant migration canary
  working-directory: app
  env:
    KMP_ENABLE_TENANT_CANARY: "true"
    KMP_DB_DRIVER: postgres
    PLATFORM_DB_HOST: ${{ secrets.PLATFORM_DB_HOST }}
    PLATFORM_DB_USERNAME: ${{ secrets.PLATFORM_DB_USERNAME }}
    PLATFORM_DB_PASSWORD: ${{ secrets.PLATFORM_DB_PASSWORD }}
    PLATFORM_DB_DATABASE: ${{ secrets.PLATFORM_DB_DATABASE }}
  run: bash bin/tenant_migration_canary.sh

Platform Admin reserved-host surface

KMP includes a privileged /platform-admin surface in the same web application that serves tenant traffic. The current Azure template does not provision a separate admin Container App and does not trust upstream identity headers. Isolate the surface with a reserved hostname in KMP_PLATFORM_ADMIN_HOSTS and keep tenant hosts out of that list.

Minimum settings:

export KMP_PLATFORM_ADMIN_PORTAL_ENABLED=true
export KMP_PLATFORM_ADMIN_HOSTS='platform.kmp.localhost'
export KMP_PLATFORM_ADMIN_ALLOWED_STATUSES='active'
export KMP_PLATFORM_DATA_CONSOLE_ENABLED=false

Operators sign in at /platform-admin/login with platform-user email/password and TOTP. The app enforces allowed account status, lockout, and a host-bound session. The portal is mutating: authorized operators can create/provision tenants, change lifecycle state, configure safe registry values, queue backups and restores, and operate platform jobs.

Views intentionally omit secret values, database passwords, reusable KEKs, wrapped DEKs, credential-bearing object URLs, and raw job errors. Data Console routes remain separately controller-gated; hiding the navigation link is not a security control.

Backup and restore guardrails

Tenant and platform database backup requests are queued as audited platform_jobs; web requests do not run long backup or restore work inline. The three-minute unified worker claims tenant_backup, platform_database_backup, and tenant_restore jobs. The daily platform-job-retention schedule prunes completed/failed job history according to its configured classes; bin/cake platform jobs prune runs the same bounded cleanup manually.

The tenant-visible trust dashboard remains a roadmap item. Keep it distinct from the implemented Platform Admin surface in Platform Operations and Tenant Trust Surface.

Acceptance criteria