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_ADMINrole (the Provision button is hidden forPLATFORM_SUPPORTandPLATFORM_READONLY).- At least one
identity.db_serverrow with capacity belowcapacity_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
Open the wizard
Platform Admin → Tenants → New tenant(or navigate directly to/platform/tenants/new).Fill the form
Field Notes Slug Lowercase letters, digits, hyphens. 3–40 chars. Becomes the subdomain and the DB name ( tenant_<slug_with_underscores>).Display name Human-friendly; shown in the tenant switcher. First admin email Receives the invitation email after provisioning. Target db server Default (auto-place)runs the placement algorithm. Pick a specific server only if you have a reason (e.g., colocate with another tenant).Dry-run Stops after dry_runphase — no Key Vault write, no DB created. Use to validate placement + slug without side effects.Submit
The wizard returns a
jobIdimmediately and opens a progress drawer that pollsGET /platform/jobs/:jobIdevery 1.5 s. Phases advance through:placement_resolved→state_checked→secret_set→database_created→migrations_applied→master_registered→admin_seeded→complete.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
| Check | Expected |
|---|---|
GET /platform/jobs/:jobId | status=complete, all phases present |
identity.tenant row for the slug | status=active |
identity.tenant_audit row | event_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 DB | succeeds; \dn shows the seeded schemas |
| First sign-in as the admin | succeeds; password reset is forced |
Troubleshooting
error_code | Meaning | Fix |
|---|---|---|
E_INVALID_SLUG | Failed ^[a-z][a-z0-9-]{2,39}$ | Pick a valid slug. |
E_ALREADY_PROVISIONED | Master row exists for this slug | Pick a different slug or offboard the existing one. |
E_PARTIAL_STATE | Master + DB + Key Vault state diverge | Run offboard-tenant --slug <slug> --purge then retry. |
E_NO_CAPACITY | Every active db_server is at capacity_hint | Add a server (see Sharding) or raise the cap after a budget check. |
E_DB_SERVER_NOT_FOUND | Selected server is missing or inactive | Refresh the server list and pick again. |
E_ADMIN_CONN_UNRESOLVED | Key Vault could not resolve the server's admin_secret_ref | Verify the secret exists in the vault. |
E_MIGRATIONS_FAILED | knex migrate:latest exited non-zero | Inspect 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.