Skip to main content

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.

The console host

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​

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​

  1. Run the bootstrap seed

    For dev environments the db-migrate-dev.yml workflow has a workflow_dispatch input seed_master=true that runs the knex seeds against the master DB on your behalf, including packages/rcm-db/src/seeds/master/002_dev_platform_user.ts. It needs the DEV_PLATFORM_ADMIN_PASSWORD secret (and reads DEV_PLATFORM_ADMIN_EMAIL as a repository variable) and fails loudly if the secret is unset. The seed is idempotent (upserts on email) and refuses to run when NODE_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 psql session 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" <<SQL
    INSERT 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_id
    FROM security.platform_user u, security.platform_role r
    WHERE 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'));
    SQL

    Capture the plaintext password (the value you passed to bcrypt) — it is shown only once and is required for the operator's first sign-in.

  2. Make the account an Owner

    Run promote-platform-owner against 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.

  3. Sign in to the Admin UI

    Open https://admin.<env>.medsuiteone.ai and sign in with the email and printed password. The first response will be a short-lived mfa_enrollment JWT — 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.

  4. Verify the audit trail

    SELECT event_type, occurred_at, details
    FROM identity.tenant_audit
    WHERE 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.

  5. 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 with MAIL_NOT_CONFIGURED and 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 with PLATFORM_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​

CheckExpected
security.platform_user count1 row
security.platform_role_assignment for that user2 rows, code in (PLATFORM_ADMIN, PLATFORM_OWNER)
security.platform_user_mfa_factor count≥ 1 row (TOTP or WebAuthn)
Login at admin.<env>.medsuiteone.aisucceeds, completes MFA, lands on tenant picker (will be empty); Platform users is in the sidebar
AuditBOOTSTRAP_PLATFORM_USER_CREATED → platform_owner.promoted → PLATFORM_MFA_ENROLLED → PLATFORM_LOGIN_SUCCESS

Troubleshooting​

SymptomLikely causeFix
Bootstrap script: "duplicate key value violates unique constraint" on emailAlready ran onceIf lost the password, run with --reset-password flag (script supports this for the first admin only).
Login returns 401 "Invalid credentials"Typo or wrong emailVerify with SELECT email FROM security.platform_user;.
Login succeeds but tenant picker is emptyExpected — no tenants yetContinue to Phase 2.
No Platform users in the sidebarThe account is not an Owner yetRun the promote step (runbook PLAT-45.1), then reload the console.
An invited admin says the email never cameMail not configured, or deliveryRunbook PLAT-47.2.

Next​

1.6 — DNS and TLS