1.5 First platform admin
Outcome
You have a real security.platform_user row holding PLATFORM_ADMIN and
PLATFORM_OWNER, you have enrolled at least one MFA factor (TOTP or WebAuthn),
and you can sign in to the platform console at admin.<env>.medsuiteone.ai.
Every other platform admin is then invited from the console.
admin.dev.medsuiteone.ai · admin.stg.medsuiteone.ai · admin.medsuiteone.ai
(production drops the env label). This is apps/admin-app, served with
apps/admin-service answering /api/platform/* on the same host. The old
admin.rcm.medsuite.com — a variant build of the tenant app — no longer exists;
see 1.6 DNS and TLS.
Prerequisites
- 1.4 Master DB complete.
Why bootstrap is special
Platform users are managed in the console — Platform users (/users) invites
people, edits their roles, deactivates them and resets their MFA (#5863). But that
screen needs platform_users.manage, which only the PLATFORM_OWNER role holds, so
the console cannot make the first Owner. That one account is the only one created
from a shell: the bootstrap insert below, then promote-platform-owner to make it an
Owner. Do it while the firewall rule from 1.2 Infrastructure is
still in place.
Steps
Run the bootstrap seed
For dev environments the
db-migrate-dev.ymlworkflow has aworkflow_dispatchinputseed_master=truethat runs the knex seeds against the master DB on your behalf, includingpackages/rcm-db/src/seeds/master/002_dev_platform_user.ts. It needs theDEV_PLATFORM_ADMIN_PASSWORDsecret (and readsDEV_PLATFORM_ADMIN_EMAILas a repository variable) and fails loudly if the secret is unset. The seed is idempotent (upserts on email) and refuses to run whenNODE_ENV=production, so it is safe to leave wired up.For prod environments the same seed file is not used. Instead, an operator runs the equivalent insert directly against master through a one-time
psqlsession opened during the firewall window from 1.2 Infrastructure:export DATABASE_MASTER_URL="postgresql://rcm_master_admin:<password>@pg-rcm-master-prod.postgres.database.azure.com:5432/rcm_master?sslmode=require"PWHASH=$(node -e 'require("bcryptjs").hash(process.argv[1],10).then(h=>process.stdout.write(h))' "$(openssl rand -base64 24)")psql "$DATABASE_MASTER_URL" <<SQLINSERT INTO security.platform_user (email, password_hash, full_name, is_active)VALUES ('you@medsuite.com', '$PWHASH', 'Your Full Name', true)RETURNING platform_user_id;INSERT INTO security.platform_role_assignment (platform_user_id, platform_role_id)SELECT u.platform_user_id, r.platform_role_idFROM security.platform_user u, security.platform_role rWHERE u.email='you@medsuite.com' AND r.code='PLATFORM_ADMIN';INSERT INTO identity.tenant_audit (event_type, details)VALUES ('BOOTSTRAP_PLATFORM_USER_CREATED', jsonb_build_object('email','you@medsuite.com'));SQLCapture the plaintext password (the value you passed to bcrypt) — it is shown only once and is required for the operator's first sign-in.
Make the account an Owner
Run
promote-platform-owneragainst the same master DB — the exact commands, the dry run and the read-back are runbook PLAT-45.1 (docs/operations/runbook-platform.md):pnpm --filter @medsuite/admin-service exec tsx \src/scripts/promote-platform-owner.ts \--email you@medsuite.com --confirm you@medsuite.com --reason "first Owner for <env>"It only adds the role to an existing, active user; it is safe to re-run.
Sign in to the Admin UI
Open
https://admin.<env>.medsuiteone.aiand sign in with the email and printed password. The first response will be a short-livedmfa_enrollmentJWT — not a normal session. You will be required to:- Enroll TOTP (Google Authenticator, 1Password, etc.) and save recovery codes.
- Optionally enroll a WebAuthn authenticator (YubiKey, platform authenticator).
- Change the bootstrap-generated password.
Only then is a full platform JWT issued.
Verify the audit trail
SELECT event_type, occurred_at, detailsFROM identity.tenant_auditWHERE event_type IN ('BOOTSTRAP_PLATFORM_USER_CREATED','PLATFORM_LOGIN_SUCCESS','PLATFORM_MFA_ENROLLED','PLATFORM_PASSWORD_CHANGED','platform_owner.promoted')ORDER BY occurred_at DESC LIMIT 10;You should see the bootstrap row, the Owner promotion, the MFA enrollment, the password-change event, and your login.
Invite everyone else from the console
Do not repeat the SQL above for anyone else. Open Platform users → Invite user, enter their email and tick their roles (least privilege). They receive a single-use link (
/invite/<token>, valid 7 days), choose their own password, and enrol their own second factor — no password ever passes through you. The console needs mail configured on admin-service; without it the invite is refused withMAIL_NOT_CONFIGUREDand nothing is created (runbook PLAT-27, PLAT-47.2).Make a second Owner straight away. The platform refuses any change that would leave it with no active Owner (
409 PLATFORM_LAST_OWNER), and recovering from losing the only one is a shell job again (PLAT-45.2). Invite a second person withPLATFORM_OWNER, or give an existing admin that role with Edit roles.
Security notes
- The bootstrap script must be run from a workstation that has temporary access to the master DB (firewall rule from 1.2 Infrastructure step 6).
- After the first admin is created, disable the firewall rule so master DB is only reachable from the deployed services.
- The bootstrap-generated password is shown only once — paste it into a password manager or send to the user via your org's secure channel.
Validation
| Check | Expected |
|---|---|
security.platform_user count | 1 row |
security.platform_role_assignment for that user | 2 rows, code in (PLATFORM_ADMIN, PLATFORM_OWNER) |
security.platform_user_mfa_factor count | ≥ 1 row (TOTP or WebAuthn) |
Login at admin.<env>.medsuiteone.ai | succeeds, completes MFA, lands on tenant picker (will be empty); Platform users is in the sidebar |
| Audit | BOOTSTRAP_PLATFORM_USER_CREATED → platform_owner.promoted → PLATFORM_MFA_ENROLLED → PLATFORM_LOGIN_SUCCESS |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Bootstrap script: "duplicate key value violates unique constraint" on email | Already ran once | If lost the password, run with --reset-password flag (script supports this for the first admin only). |
| Login returns 401 "Invalid credentials" | Typo or wrong email | Verify with SELECT email FROM security.platform_user;. |
| Login succeeds but tenant picker is empty | Expected — no tenants yet | Continue to Phase 2. |
| No Platform users in the sidebar | The account is not an Owner yet | Run the promote step (runbook PLAT-45.1), then reload the console. |
| An invited admin says the email never came | Mail not configured, or delivery | Runbook PLAT-47.2. |