# Hospital Onboarding & Self-Service Setup **Date:** 2026-04-06 **Status:** Plan — pending implementation **Owner:** helix-engage --- ## Goal Make onboarding a new hospital a one-command devops action plus a guided self-service flow inside the staff portal. After running the script, the hospital admin should be able to log into a fresh workspace and reach a fully operational call center by filling in 6 setup pages — without anyone touching env vars, JSON files, or running shell commands a second time. ## Non-goals - Per-tenant secrets management (env vars stay infra-owned for now). - Self-service Cloudflare Turnstile / Ozonetel account provisioning. Operator pastes pre-existing credentials. - Multi-hospital routing inside one sidecar. One sidecar per workspace; multi-tenancy is handled by the platform. - Bulk CSV import of doctors / staff. Single-row form CRUD only. - Email infrastructure for invitations beyond what core already does. --- ## User journey ### T0 — devops, one-command bootstrap (~30 seconds) ```bash ./onboard-hospital.sh \ --create \ --display-name "Care Hospital" \ --subdomain care \ --admin-email admin@carehospital.com \ --admin-password 'TempCare#2026' ``` Script signs up the admin user, creates and activates the workspace, syncs the helix-engage SDK, mints an API key, writes a sidecar `.env`, and prints a credentials handoff block. Done. ### T1 — hospital admin first login (~10 minutes) Admin opens the workspace URL, signs in with the temp password. App detects an unconfigured workspace and routes them to `/setup`. A 6-step wizard walks them through: 1. **Hospital identity** — confirm display name, upload logo, pick brand colors → writes to `theme.json` 2. **Clinics** — add at least one branch (name, address, phone, timings) → creates Clinic records on platform 3. **Doctors** — add at least one doctor (name, specialty, clinic, visiting hours) → creates Doctor records on platform 4. **Team** — invite supervisors and CC agents by email → triggers core's `sendInvitations` mutation 5. **Telephony** — paste Ozonetel + Exotel credentials, default DID, default campaign → writes to `telephony.json` 6. **AI assistant** — pick provider (OpenAI / Anthropic), model, optional system prompt override → writes to `ai.json` After step 6, admin clicks "Finish setup" and lands on the home dashboard. Setup state is recorded in `setup-state.json` so the wizard never auto-shows again. ### T2 — hospital admin returns later (any time) Each setup page is also accessible standalone via the **Settings** menu. Admin can edit any of them at any time. Settings hub shows green checkmarks for completed sections and yellow badges for sections still using defaults. ### T3 — agents and supervisors join Supervisors and agents accept their invitation emails, set their own passwords, and land on the home dashboard. They're already role-assigned by the admin during T1 step 4, so they see the right pages immediately. --- ## Architecture decisions ### 1. Script does identity. Portal does configuration. - **In script:** anything requiring platform-admin credentials (signup, workspace activation, SDK sync, API key creation). One-time, devops-only. - **In staff portal:** anything that operates inside the workspace (clinics, doctors, team, sidecar config files). Self-serve, repeatable. This keeps the script's blast radius small and means the hospital admin never needs platform-admin access. ### 2. Two distinct frontend → backend patterns **Pattern A — Direct GraphQL to platform** (for entities the platform owns) - Clinics, Doctors, Workspace Members - Frontend uses `apiClient.graphql(...)` with the user's JWT - Already established by `settings.tsx` for member listing - No sidecar code needed **Pattern B — Sidecar admin endpoints** (for sidecar-owned config files) - Theme (`theme.json`), Widget (`widget.json`), Telephony (`telephony.json`), AI (`ai.json`), Setup state (`setup-state.json`) - Frontend uses `apiClient.fetch('/api/config/...')` - Sidecar persists to disk via `*ConfigService` mirroring `ThemeService` - Already established by `branding-settings.tsx` and `WidgetConfigService` **Rule:** if it lives in a workspace schema on the platform, use Pattern A. If it's a sidecar config file, use Pattern B. Don't mix. ### 3. Telephony config moves out of env vars `OZONETEL_*`, `SIP_*`, `EXOTEL_*` env vars become bootstrap defaults that seed `data/telephony.json` on first boot, then never read again. All runtime reads go through `TelephonyConfigService.getConfig()`. Six read sites refactor (auth.controller, ozonetel-agent.service, ozonetel-agent.controller, kookoo-ivr.controller, agent-config.service, maint.controller). ### 4. AI config moves out of env vars Same pattern. `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` stay in env (true secrets), but `AI_PROVIDER` / `AI_MODEL` move to `data/ai.json`. `WidgetChatService` and any other AI-using services read from `AiConfigService`. ### 5. Setup state lives in its own file `data/setup-state.json` tracks completion status for each of the 6 setup steps + a global `wizardDismissed` flag. Frontend reads it on app load to decide whether to show the setup wizard. Each setup page marks its step complete on save. ```json { "version": 1, "wizardDismissed": false, "steps": { "identity": { "completed": false, "completedAt": null }, "clinics": { "completed": false, "completedAt": null }, "doctors": { "completed": false, "completedAt": null }, "team": { "completed": false, "completedAt": null }, "telephony": { "completed": false, "completedAt": null }, "ai": { "completed": false, "completedAt": null } } } ``` ### 6. Member invitation stays email-based The platform only supports email-based invitations (`sendInvitations` → email link → invitee sets password). We use this as-is. No new core mutation. Trade-off: admin can't bulk-create users with pre-set passwords, but the email flow is acceptable for hospital onboarding (admin types in the team's emails once, they each get a setup link). ### 7. Roles are auto-synced by SDK `HelixEngage Manager` and `HelixEngage User` roles are defined in `FortyTwoApps/helix-engage/src/roles/` and created automatically by `yarn app:sync`. The frontend's role dropdown in the team invite form queries the platform for the workspace's roles via `getRoles` and uses real role IDs (no email-pattern hacks). --- ## Backend changes (helix-engage-server) ### New services / files | File | Purpose | |---|---| | `src/config/setup-state.defaults.ts` | Type + defaults for `data/setup-state.json` | | `src/config/setup-state.service.ts` | Load / get / mark step complete / dismiss wizard | | `src/config/telephony.defaults.ts` | Type + defaults for `data/telephony.json` (Ozonetel + Exotel + SIP) | | `src/config/telephony-config.service.ts` | File-backed CRUD; `onModuleInit` seeds from env vars on first boot | | `src/config/ai.defaults.ts` | Type + defaults for `data/ai.json` | | `src/config/ai-config.service.ts` | File-backed CRUD; seeds from env on first boot | | `src/config/setup-state.controller.ts` | `GET /api/config/setup-state`, `PUT /api/config/setup-state/steps/:step`, `POST /api/config/setup-state/dismiss` | | `src/config/telephony-config.controller.ts` | `GET/PUT /api/config/telephony` with secret masking on GET | | `src/config/ai-config.controller.ts` | `GET/PUT /api/config/ai` with secret masking | ### Modified files | File | Change | |---|---| | `src/config/config-theme.module.ts` | Register the 3 new services + 3 new controllers | | `src/config/widget.defaults.ts` | Drop `hospitalName` field (the long-standing duplicate) | | `src/config/widget-config.service.ts` | Inject `ThemeService`, read `brand.hospitalName` from theme at the 2 generateKey call sites | | `src/widget/widget.service.ts` | `getInitData()` reads captcha site key from `WidgetConfigService` instead of `process.env.RECAPTCHA_SITE_KEY` | | `src/auth/agent-config.service.ts:49` | Read `OZONETEL_CAMPAIGN_NAME` from `TelephonyConfigService` | | `src/auth/auth.controller.ts:141, 255` | Read `OZONETEL_AGENT_PASSWORD` from `TelephonyConfigService` | | `src/ozonetel/ozonetel-agent.service.ts:199, 235, 236` | Read `OZONETEL_DID`, `OZONETEL_SIP_ID` from `TelephonyConfigService` | | `src/ozonetel/ozonetel-agent.controller.ts:39, 42, 192` | Same | | `src/ozonetel/kookoo-ivr.controller.ts:11, 12` | Same | | `src/maint/maint.controller.ts:27` | Same | | `src/widget/widget-chat.service.ts` | Read `provider` and `model` from `AiConfigService` instead of `ConfigService` | | `src/ai/ai-provider.ts` | Same — provider/model from config file, API keys still from env | --- ## Frontend changes (helix-engage) ### New pages | Page | Path | Purpose | |---|---|---| | `setup/setup-wizard.tsx` | `/setup` | 6-step wizard, auto-shown on first login when setup incomplete | | `pages/clinics.tsx` | `/settings/clinics` | List + add/edit clinic records (slideout pattern) | | `pages/doctors.tsx` | `/settings/doctors` | List + add/edit doctors, assign to clinics | | `pages/team-settings.tsx` | `/settings/team` | Member list + invite form + role editor (replaces current `settings.tsx` member view) | | `pages/telephony-settings.tsx` | `/settings/telephony` | Ozonetel + Exotel + SIP form (consumes `/api/config/telephony`) | | `pages/ai-settings.tsx` | `/settings/ai` | AI provider/model/prompt form (consumes `/api/config/ai`) | | `pages/widget-settings.tsx` | `/settings/widget` | Widget enabled/embed/captcha form (consumes `/api/config/widget`) | | `pages/settings-hub.tsx` | `/settings` | Index page listing all setup sections with completion badges. Replaces current `settings.tsx`. | ### Modified pages | File | Change | |---|---| | `src/pages/login.tsx` | After successful login, fetch `/api/config/setup-state`. If incomplete and user is workspace admin, redirect to `/setup`. Otherwise existing flow. | | `src/pages/branding-settings.tsx` | On save, mark `identity` step complete via `PUT /api/config/setup-state/steps/identity` | | `src/components/layout/sidebar.tsx` | Add Settings hub entry; remove direct links to individual settings pages from main nav (move them under Settings) | | `src/providers/router-provider.tsx` | Register the 7 new routes | | `src/pages/integrations.tsx` | Remove the Ozonetel + Exotel cards (functionality moves to `telephony-settings.tsx`); keep WhatsApp/FB/Google/website cards for now | ### New shared components | File | Purpose | |---|---| | `src/components/setup/wizard-shell.tsx` | Layout: progress bar, step navigation, footer with prev/next | | `src/components/setup/wizard-step.tsx` | Single-step container — title, description, content slot, validation hook | | `src/components/setup/section-card.tsx` | Settings hub section card with status badge | | `src/components/forms/clinic-form.tsx` | Reused by clinics page + setup wizard step 2 | | `src/components/forms/doctor-form.tsx` | Reused by doctors page + setup wizard step 3 | | `src/components/forms/invite-member-form.tsx` | Reused by team page + setup wizard step 4 | | `src/components/forms/telephony-form.tsx` | Reused by telephony settings + setup wizard step 5 | | `src/components/forms/ai-form.tsx` | Reused by ai settings + setup wizard step 6 | The pattern: each settings page renders the same form component the wizard step renders. Wizard steps just wrap the form in `` and add prev/next navigation. Standalone settings pages wrap the form in a normal page layout. Form is the source of truth; wizard and settings page are two presentations of the same thing. --- ## Onboarding script changes `onboard-hospital.sh` is already 90% there. Three minor changes: 1. **Drop the `--sidecar-env-out` default behavior** — print a structured "credentials handoff" block at the end with admin email, temp password, workspace URL, sidecar `.env` content. Operator copies what they need. 2. **Change the credentials block format** — make it copy-pasteable as a single email body so the operator can email it to the hospital owner directly. 3. **Add `setup-state.json` initialization** — the script writes a fresh `setup-state.json` to the sidecar's `data/` directory as part of step 6, so the first frontend load knows nothing is configured yet. --- ## Phasing Each phase is a coherent commit. Don't ship phases out of order. ### Phase 1 — Backend foundations (config services + endpoints) **Files:** 9 new + 4 modified backend files. No frontend. - New services: `setup-state`, `telephony-config`, `ai-config` - New defaults files for each - New controllers for each - Module wiring - Drop `widget.json.hospitalName` (the original duplicate that started this whole thread) - Migrate the 6 Ozonetel read sites to `TelephonyConfigService` - Migrate the AI provider/model reads to `AiConfigService` - First-boot env-var seeding: each new service reads its respective env vars on `onModuleInit` and writes them to its config file if the file doesn't exist **Verifies:** sidecar still serves all existing endpoints, env-var-driven Ozonetel still works (because the seeding picks up the same values), `data/telephony.json` and `data/ai.json` exist on first boot. **Estimate:** 4-5 hours. ### Phase 2 — Settings hub + first-run detection **Files:** 2 new pages + 4 modified frontend files + new shared `section-card` component. - `settings-hub.tsx` replaces `settings.tsx` as the `/settings` route - Move the existing member-list view from `settings.tsx` into a new `team-settings.tsx` (read-only for now; invite + role editing comes in Phase 3) - `login.tsx` fetches setup-state after successful login and redirects to `/setup` if incomplete - `setup/setup-wizard.tsx` shell renders the 6 step containers (with placeholder content for now) - Sidebar redesign: collapse all settings into one Settings entry that opens the hub - Router updates to register the new routes **Verifies:** clean login → setup wizard appearance for fresh workspace; Settings hub navigates to existing pages; nothing breaks for already-set-up workspaces. **Estimate:** 3-4 hours. ### Phase 3 — Entity CRUD pages (Pattern A — direct platform GraphQL) **Files:** 3 new pages + 3 new form components + 1 modified team page. - `clinics.tsx` + `clinic-form.tsx` — list with add/edit slideout - `doctors.tsx` + `doctor-form.tsx` — list with add/edit, clinic dropdown sourced from `clinics` - `team-settings.tsx` becomes interactive — add invite form via `sendInvitations`, real role dropdown via `getRoles`, role assignment via `updateWorkspaceMemberRole` **Verifies:** admin can create clinics, doctors, and invite team members from the staff portal without touching the database. **Estimate:** 5-6 hours. ### Phase 4 — Sidecar-config CRUD pages (Pattern B — sidecar admin endpoints) **Files:** 3 new pages + 3 new form components. - `telephony-settings.tsx` + `telephony-form.tsx` — Ozonetel + Exotel + SIP fields - `ai-settings.tsx` + `ai-form.tsx` — provider, model, temperature, system prompt - `widget-settings.tsx` + `widget-form.tsx` — wraps the existing widget config endpoint with a real form **Verifies:** admin can edit telephony, AI, and widget config from the staff portal. Changes take effect without sidecar restart (since services use in-memory cache + file write). **Estimate:** 4-5 hours. ### Phase 5 — Wizard step composition **Files:** 6 wizard step components, each thin wrappers around the Phase 3/4 forms. - `wizard-step-identity.tsx` - `wizard-step-clinics.tsx` - `wizard-step-doctors.tsx` - `wizard-step-team.tsx` - `wizard-step-telephony.tsx` - `wizard-step-ai.tsx` Each wraps the corresponding form, adds wizard validation (required fields enforced for setup completion), and on save calls `PUT /api/config/setup-state/steps/` to mark the step complete. **Verifies:** admin can complete the entire setup wizard end-to-end on a fresh workspace. After step 6, redirected to home dashboard. Setup state file shows all 6 steps complete. **Estimate:** 2-3 hours. ### Phase 6 — Polish - Onboarding script credentials handoff block format - "Resume setup" CTA on home dashboard if any step is incomplete - Loading states, error toasts, optimistic updates - Setup-state badges on the Settings hub - Validation: clinic count > 0 required for booking flow, doctor count > 0 required for booking flow, etc. - E2E smoke test against the Care Hospital workspace I already created **Estimate:** 2-3 hours. --- ## Total estimate **20-26 hours of focused implementation work** spanning ~30 new files and ~15 modified files. Realistic over 3-4 working days with checkpoints at each phase boundary. --- ## Out of scope (explicit) - Self-service Cloudflare Turnstile signup (operator pastes existing site key) - Self-service Ozonetel account creation (operator pastes credentials) - Bulk import of doctors / staff (single-row form only) - Per-tenant secrets management (env vars stay infra-owned for AI keys, captcha secret, HMAC secret) - Workspace deletion / archival - Multi-hospital admin (one admin per workspace; switching workspaces is platform-level) - Hospital templates ("clone from Ramaiah") — useful follow-up but not required for the first real onboarding - Self-service password reset for invited members (handled by the existing platform reset-password flow) - Onboarding analytics / metrics - Email branding for invitation emails (uses platform default for now) --- ## Open questions before phase 1 1. **Sidecar config file hot-reload** — when an admin updates `telephony.json` via the new endpoint, does the change need to take effect immediately (in-memory cache invalidation, no restart) or is a sidecar restart acceptable? Decision affects whether services need a "refresh" hook. **Recommendation: in-memory cache only, no restart needed** — already how `ThemeService` works. 2. **Setup state visibility** — should the setup-state file be a simple flag set or should it track *who* completed each step and *when*? Recommendation: track `completedAt` timestamp + `completedBy` user id for audit trail. 3. **Auto-mark "identity" step complete from existing branding** — if the workspace already has a `theme.json` with a non-default `brand.hospitalName`, should the wizard auto-skip step 1? **Recommendation: yes** — don't make admins re-confirm something they already configured. 4. **What if the admin invites a team member who already exists on the platform?** Does `sendInvitations` add them to the workspace, or fail? Need to verify before Phase 3. If it fails, we may need a "find or invite" wrapper. 5. **Logo upload** — do we accept a URL only (admin pastes a CDN link) or do we need real file upload to MinIO? **Recommendation: URL only for Phase 1**, file upload as Phase 6 polish. --- ## Risks - **`yarn app:sync` may sometimes fail to register HelixEngage roles cleanly** if a workspace was activated but never had its first sync — this would block the team page's role dropdown. Mitigation: script runs sync immediately after activation, before exiting. - **Frontend role queries require user JWT, not API key** — `settings.tsx` already noted this with the "Roles are only accessible via user JWT" comment. The team-settings page has to use direct GraphQL with user auth, not the sidecar proxy. - **Migrating Ozonetel env vars to a config file mid-session can break a running sidecar** if someone's actively using the call desk during deploy. Mitigation: deploy during low-usage window; the new service falls back to env vars if the config file is missing. - **Setup wizard auto-redirect could trap users in a loop** if `setup-state.json` write fails. Mitigation: wizard always has a "Skip for now" link in the top right that sets `wizardDismissed: true`.