Skip to main content

Provision a new tenant from the Platform Admin UI

Outcome

A new customer (tenant) has its own dedicated Postgres database, registered in master, with an admin user seeded and ready to sign in.

Prerequisites

  • PLATFORM_ADMIN role (the Provision button is hidden for PLATFORM_SUPPORT and PLATFORM_READONLY).
  • At least one identity.db_server row with capacity below capacity_hint. See Sharding & rebalancing if every active server is full.
  • Customer intake complete (legal name, slug, first admin email).
  • The cluster has Key Vault wired in. Without it, the runner returns 503 PROVISIONING_UNAVAILABLE — see step 6 of the platform-bootstrap troubleshooting page.

What happens under the hood

Steps

  1. Open the wizard

    Platform Admin → Tenants → New tenant (or navigate directly to /platform/tenants/new).

  2. Fill the form

    FieldNotes
    SlugLowercase letters, digits, hyphens. 3–40 chars. Becomes the subdomain and the DB name (tenant_<slug_with_underscores>).
    Display nameHuman-friendly; shown in the tenant switcher.
    First admin emailReceives the invitation email after provisioning.
    Target db serverDefault (auto-place) runs the placement algorithm. Pick a specific server only if you have a reason (e.g., colocate with another tenant).
    Dry-runStops after dry_run phase — no Key Vault write, no DB created. Use to validate placement + slug without side effects.
  3. Submit

    The wizard returns a jobId immediately and opens a progress drawer that polls GET /platform/jobs/:jobId every 1.5 s. Phases advance through:

    placement_resolvedstate_checkedsecret_setdatabase_createdmigrations_appliedmaster_registeredadmin_seededcomplete.

  4. Hand off the admin invite

    When the runner reports complete, an invitation email containing a first-sign-in link is queued for the address in step 2. Forward it to the customer's go-to admin contact.

Validation

CheckExpected
GET /platform/jobs/:jobIdstatus=complete, all phases present
identity.tenant row for the slugstatus=active
identity.tenant_audit rowevent_type=TENANT_PROVISIONED with the operator's platform_user_id
Key Vault secret tenant-db-<slug>exists, contains a postgres:// URL
Direct psql against the new DBsucceeds; \dn shows the seeded schemas
First sign-in as the adminsucceeds; password reset is forced

Troubleshooting

error_codeMeaningFix
E_INVALID_SLUGFailed ^[a-z][a-z0-9-]{2,39}$Pick a valid slug.
E_ALREADY_PROVISIONEDMaster row exists for this slugPick a different slug or offboard the existing one.
E_PARTIAL_STATEMaster + DB + Key Vault state divergeRun offboard-tenant --slug <slug> --purge then retry.
E_NO_CAPACITYEvery active db_server is at capacity_hintAdd a server (see Sharding) or raise the cap after a budget check.
E_DB_SERVER_NOT_FOUNDSelected server is missing or inactiveRefresh the server list and pick again.
E_ADMIN_CONN_UNRESOLVEDKey Vault could not resolve the server's admin_secret_refVerify the secret exists in the vault.
E_MIGRATIONS_FAILEDknex migrate:latest exited non-zeroInspect rcm-core logs for the underlying knex error; fix and re-run. The runner will roll back DB + secret automatically.
Break-glass: provision via the CLI

When the wizard is unreachable (deploy outage, Key Vault degradation), the canonical CLI is:

pnpm --filter @rcm/rcm-core provision-tenant \
--slug acme \
--display-name "Acme Health" \
--admin-email admin@acme.example

The CLI calls the same provisionTenant() orchestrator the runner does. Audit rows still get written, attributed to the OS user invoking the script.

Next

1.2 — Impersonate a tenant