Phase 2 of hospital onboarding & self-service plan
(docs/superpowers/plans/2026-04-06-hospital-onboarding-self-service.md
checked in here for the helix-engage repo).
Frontend foundations for the staff-portal Settings hub and 6-step setup
wizard. Backend was Phase 1 (helix-engage-server commit).
New shared components (src/components/setup/):
- wizard-shell.tsx — fullscreen layout with left step navigator, progress
bar, and Skip-for-now affordance
- wizard-step.tsx — single-step wrapper with Mark Complete + Prev/Next/
Finish navigation, completion badge
- section-card.tsx — Settings hub card with title/description/icon, links
to a section page, optional status badge mirroring setup-state
New pages:
- pages/setup-wizard.tsx — top-level /setup route, fullscreen (no AppShell),
loads setup-state from sidecar, renders the active step. Each step has a
placeholder body for now; Phase 5 swaps placeholders for real form
components from the matching settings pages. Already functional end-to-end:
Mark Complete writes to PUT /api/config/setup-state/steps/<step>, Skip
posts to /dismiss, Finish navigates to /.
- pages/team-settings.tsx — moved the existing workspace member listing out
of the old monolithic settings.tsx into its own /settings/team route. No
functional change; Phase 3 will add the invite form + role editor here.
- pages/settings-placeholder.tsx — generic "Coming in Phase X" stub used by
routes for clinics, doctors, telephony, ai, widget until those pages land.
Modified pages:
- pages/settings.tsx — rewritten as the Settings hub (the new /settings
route). Renders SectionCards in 3 groups (Hospital identity, Care
delivery, Channels & automation) with completion badges sourced from
/api/config/setup-state. The hub links to existing pages (/branding,
/rules) and to placeholder pages for the not-yet-built sections.
- pages/login.tsx — after successful login, calls getSetupState() and
redirects to /setup if wizardRequired. Failures fall through to / so an
older sidecar without the setup-state endpoint still works.
- components/layout/sidebar.tsx — collapsed the Configuration group
(Rules Engine + Branding standalone entries) into the single Settings
entry that opens the hub. Removes the IconSlidersUp import that's no
longer used.
New types and helpers (src/lib/setup-state.ts):
- SetupState / SetupStepName / SetupStepStatus types mirroring the sidecar
shape
- SETUP_STEP_NAMES constant + SETUP_STEP_LABELS map (title + description
per step) — single source of truth used by the wizard, hub, and any
future surface that wants to render step metadata
- getSetupState / markSetupStepComplete / markSetupStepIncomplete /
dismissSetupWizard / resetSetupState helpers wrapping the api-client
Other:
- lib/api-client.ts — added apiClient.put() helper for the setup-state
step update mutations (PUT was the only verb missing from the existing
get/post/graphql helpers)
- main.tsx — registered new routes:
/setup (fullscreen, no AppShell)
/settings (the hub, replaces old settings.tsx)
/settings/team (moved member listing)
/settings/clinics (placeholder, Phase 3)
/settings/doctors (placeholder, Phase 3)
/settings/telephony (placeholder, Phase 4)
/settings/ai (placeholder, Phase 4)
/settings/widget (placeholder, Phase 4)
Tested via npx tsc --noEmit and npm run build (clean, only pre-existing
chunk-size and dynamic-import warnings unrelated to this change).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
19 KiB
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)
./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:
- Hospital identity — confirm display name, upload logo, pick brand colors → writes to
theme.json - Clinics — add at least one branch (name, address, phone, timings) → creates Clinic records on platform
- Doctors — add at least one doctor (name, specialty, clinic, visiting hours) → creates Doctor records on platform
- Team — invite supervisors and CC agents by email → triggers core's
sendInvitationsmutation - Telephony — paste Ozonetel + Exotel credentials, default DID, default campaign → writes to
telephony.json - 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<any>(...)with the user's JWT - Already established by
settings.tsxfor 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
*ConfigServicemirroringThemeService - Already established by
branding-settings.tsxandWidgetConfigService
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.
{
"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 <WizardStep> 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:
- Drop the
--sidecar-env-outdefault behavior — print a structured "credentials handoff" block at the end with admin email, temp password, workspace URL, sidecar.envcontent. Operator copies what they need. - 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.
- Add
setup-state.jsoninitialization — the script writes a freshsetup-state.jsonto the sidecar'sdata/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
onModuleInitand 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.tsxreplacessettings.tsxas the/settingsroute- Move the existing member-list view from
settings.tsxinto a newteam-settings.tsx(read-only for now; invite + role editing comes in Phase 3) login.tsxfetches setup-state after successful login and redirects to/setupif incompletesetup/setup-wizard.tsxshell 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 slideoutdoctors.tsx+doctor-form.tsx— list with add/edit, clinic dropdown sourced fromclinicsteam-settings.tsxbecomes interactive — add invite form viasendInvitations, real role dropdown viagetRoles, role assignment viaupdateWorkspaceMemberRole
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 fieldsai-settings.tsx+ai-form.tsx— provider, model, temperature, system promptwidget-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.tsxwizard-step-clinics.tsxwizard-step-doctors.tsxwizard-step-team.tsxwizard-step-telephony.tsxwizard-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/<step> 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
-
Sidecar config file hot-reload — when an admin updates
telephony.jsonvia 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 howThemeServiceworks. -
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
completedAttimestamp +completedByuser id for audit trail. -
Auto-mark "identity" step complete from existing branding — if the workspace already has a
theme.jsonwith a non-defaultbrand.hospitalName, should the wizard auto-skip step 1? Recommendation: yes — don't make admins re-confirm something they already configured. -
What if the admin invites a team member who already exists on the platform? Does
sendInvitationsadd them to the workspace, or fail? Need to verify before Phase 3. If it fails, we may need a "find or invite" wrapper. -
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:syncmay 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.tsxalready 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.jsonwrite fails. Mitigation: wizard always has a "Skip for now" link in the top right that setswizardDismissed: true.