11-registration-and-onboardingwave: W6filled40 citations

User Onboarding — Registration and Initial Setup Flows

Audiences: developer, internal, clinical-buyer

User Onboarding — Registration and Initial Setup Flows

Three parallel front-door flows — invitation-led staff onboarding, self-service patient sign-up, and partner application — funnel into one verification + approval pipeline before a user ever sees a portal shell.

Business Purpose

Onboarding is the moment a buyer decides whether the HMS feels professional or homemade — and the moment a clinic admin decides whether the platform respects their hiring workflow. Dudoxx HMS ships three distinct onboarding entry points that share one verification backbone:

  • Invitation-led (the default for clinical staff) — a clinic admin invites a doctor or nurse by email; the new user lands on a tokenized form with email and role pre-filled and cannot self-elevate.
  • Self-service patient sign-up — a patient picks their clinic from a list, enters identity + DOB, and waits for clinic-admin approval if the org requires it.
  • Partner application — a six-step certification timeline for resellers and implementation partners, separate from clinical-user provisioning.

Each path lands in the same gated state machine: register → email-verify → phone-verify → [optional] admin-approve → first-login wizard. The structural value is that a clinic never has to write its own user-provisioning code, and a regulator can audit a single approval queue across both invited staff and walk-in patient registrations.

Audiences

  • Investor: shows that user acquisition is not a single form — there are three segmented funnels (staff, patients, partners) that each respect a different commercial motion. The invitation path is the lock-in surface for clinic deals; the self-service path is the wedge for solo practices.
  • Clinical buyer (clinic admin): explains exactly which knobs they own — registration toggle, approval requirement, password policy, email/phone verification — and where the Pending Approvals queue lives in their portal.
  • Developer/partner: documents the two registration components (one bare, one invitation-aware), the shared onboardingApi client surface, the Trusted Gateway pattern for public token validation, the E.164 phone-normalization hop, and the wizard-step state machine for invitation flow.
  • Internal (ops/support): locates the four route surfaces (/auth/register, /register?invitation=..., /auth/verify-email, /auth/verify-phone) plus the clinic-admin approvals page, and explains the redirects between them.

Architecture

Three entry routes, one backend funnel

Self-service:   /[locale]/auth/register        → POST /auth/register             → email-verify
Invited:        /[locale]/register?invitation= → POST /onboarding/register       → phone-verify
Partner:        /[locale]/public/partners/apply (separate certification track)
                                                ↓
                              verify email → verify phone → [admin approve] → login → first-login tour

The proxy at ddx-web/src/proxy.ts:129-133 treats /auth/register (the self-service form) as an AUTH_ROUTE — public, but authenticated users are bounced away. The invitation route /register is not in that list; an authenticated user hitting an invitation link is force-signed-out (see Data Flow below) so the invitee actually becomes the new account owner, not the existing session user.

Two registration components, two purposes

FileSurfaceTriggerAPI endpoint
ddx-web/src/app/[locale]/auth/register/page.tsx:34-148 + RegisterForm.tsx:24-557Self-service public sign-upAnyone visiting /auth/registerauthApi.register(...)
ddx-web/src/app/[locale]/register/page.tsx:95-183 + InvitationRegisterForm.tsx:91-745Invitation-token sign-upClinic admin sends email inviteonboardingApi.register(...)

The two forms look similar but enforce different invariants: the self-service form lets the user pick their role and clinic; the invitation form locks the email and organization to whatever the invitation token carries and forces phone verification before submission (InvitationRegisterForm.tsx:263-268).

Per-role onboarding tours after login

Once the gated funnel completes, each role lands in a portal-specific "first-login" experience. For clinicians, the curated 10-minute tour is seeded as help content (ddx-seeder-ts/content/help/modules/doctor/01-getting-started/03-first-login.yaml:1-108) covering display name + specialty, timezone, signature upload, mandatory 2FA enablement, and notification preferences. Patient and other-role tours follow the same dashboard-tour content pattern (ddx-seeder-ts/content/help/modules/patient/01-getting-started/01-dashboard-tour.yaml). The tours are content, not gates — they are surfaced in the help drawer rather than blocking access to the portal shell.

Honest disclosure: as of 2026-05-18 the per-role interactive first-login wizard is minimal in the frontend. The doctor 2FA enrolment is documented as policy in the help-content tour and is enforced server-side, but there is no full-screen wizard in ddx-web that walks every role through tour steps after first login — the content is available on-demand in the help drawer instead.

Tech Stack & Choices

ConcernChoiceRationale
Form validationzod v3 via registerSchemaSame library used on backend (zod/v4) — schema parity prevents drift
Phone formatE.164 normalization in client (InvitationRegisterForm.tsx:32-53)Twilio Verify rejects ambiguous formats; normalize once on submit
Token validationTrusted Gateway via backendPublicGet (register/page.tsx:60-93)Public endpoint must not require user session; gateway API key authenticates server-to-server
State machineLocal useState<FormStep> with `'form''phone-verification'
Bot defenseHoneypot input + autoComplete="off" (RegisterForm.tsx:55-56,508-520)No CAPTCHA on first-touch; relies on hidden field + rate-limit (HTTP 429 surfaced at RegisterForm.tsx:85-86)
Session handoffServer-side redirect to /api/auth/logout?callbackUrl=... (register/page.tsx:113-118)Cookies can only be mutated in Route Handlers, not in Server Components
Verification UXResend countdown (InvitationRegisterForm.tsx:131-136) + tracking input ID across the flow (requestId)Same requestId correlates registration → email-check → phone-check on the backend
i18nnext-intl (getTranslations('auth.register') server-side, useTranslations('auth.invitationRegister') client-side)en/fr/de catalogues; namespace per surface (auth.register, auth.invitationRegister, auth.verifyEmail, admin.approvals)
Design systemModern split-screen with semantic Tailwind tokens (RegisterForm.tsx:128-138)No bg-white/bg-blue-* — uses bg-primary, bg-background, border-border per project mandate

Data Flow

Self-service patient registration

1. User → GET /[locale]/auth/register                         (proxy.ts:131 — AUTH_ROUTE)
2. Server Component checks session → if signed in, redirect /portal  (register/page.tsx:27-30)
3. User fills form → registerSchema.safeParse                       (RegisterForm.tsx:67-77)
4. authApi.register({ email, password, role, organizationId, ... }) (RegisterForm.tsx:79-83)
5. Backend responds 200 → setSuccessMessage + setTimeout(3000)      (RegisterForm.tsx:91-95)
6. Redirect → /[locale]/auth/verify-email                            (RegisterForm.tsx:94)
7. /auth/verify-email reads ?token=, POSTs to authApi.verifyEmail   (verify-email/VerifyEmailForm.tsx:38-67)
8. On success → redirect to /auth/verify-phone after 3s              (VerifyEmailForm.tsx:56-59)
9. (If org.registrationRequiresApproval) appears in clinic-admin queue

Invitation-led staff onboarding

1. Clinic admin → POST /onboarding/invitations (out of scope here, owned by user-management)
2. Email link → /[locale]/register?invitation=<token>
3. Server Component validates token via Trusted Gateway          (register/page.tsx:60-93)
4. If user already logged in → /api/auth/logout?callbackUrl=...  (register/page.tsx:113-118)
5. Invalid token → renders "invitation expired/revoked" card     (register/page.tsx:124-153)
6. Valid token → renders InvitationRegisterForm with prefilled
   email + role badge + optional personal admin message          (InvitationRegisterForm.tsx:512-535)
7. Submit form → validate → switch to step="phone-verification"  (InvitationRegisterForm.tsx:254-268)
8. onboardingApi.sendPhoneVerification(E.164-phone, requestId)   (InvitationRegisterForm.tsx:181-220)
9. User enters 6-digit code → onboardingApi.verifyPhoneCode      (InvitationRegisterForm.tsx:222-252)
10. phoneVerified=true → onboardingApi.register(...) final POST  (InvitationRegisterForm.tsx:271-310)
11. If nextSteps.awaitApproval → success card + back-to-login    (InvitationRegisterForm.tsx:292-296)
12. Else → setTimeout 3s → router.push(/auth/login)              (InvitationRegisterForm.tsx:300-302)

Clinic-admin approval queue

When org.registrationRequiresApproval is true, registrations land at ddx-web/src/app/[locale]/portal/clinic-admin/approvals/page.tsx:66-84 rendered through PendingApprovalsClient (PendingApprovalsClient.tsx:67-80). The page exposes filter chips driven by searchParams.filter, a compact stats bar of pending/approvedToday/ rejectedToday/requiresAttention, and bulk approve/reject actions. The client calls onboardingApi.approveRequest, rejectRequest, and bulkAction (onboarding.ts:276-301).

The relevant SSE events (verification completion, approval status changes) ride the canonical SSE bus — see packages/ddx-sse-contract/ (@ddx/sse-contract, 4.1.0) for the publisher/consumer rules; this surface is a consumer, not a publisher.

Implicated Code

MANDATORY: ≥3 file:line citations.

  • ddx-web/src/app/[locale]/auth/register/page.tsx:25-148 — self-service register Server Component, split-screen layout, session-bounce on getSession()
  • ddx-web/src/app/[locale]/auth/register/RegisterForm.tsx:24-557 — client form, zod validation, honeypot, rate-limit handling, redirect to verify-email
  • ddx-web/src/app/[locale]/register/page.tsx:60-183 — invitation Server Component, Trusted Gateway token validation, force-logout for signed-in invitees
  • ddx-web/src/app/[locale]/register/InvitationRegisterForm.tsx:32-53 — E.164 phone normalization, +49 default for German local numbers
  • ddx-web/src/app/[locale]/register/InvitationRegisterForm.tsx:91-310 — three-step state machine (form → phone-verification → success), password strength meter, terms/privacy checkboxes
  • ddx-web/src/lib/api/clients/onboarding.ts:144-302OnboardingApiClient: validate invitation, register, send/verify email + phone, admin approve/reject/bulk
  • ddx-web/src/app/[locale]/auth/verify-email/page.tsx:14-22 and verify-email/VerifyEmailForm.tsx:38-67 — token-from-URL verification, auto-redirect to /auth/verify-phone on success
  • ddx-web/src/app/[locale]/portal/clinic-admin/approvals/page.tsx:66-84 — clinic-admin pending-approvals Server Component with Suspense boundaries and filter
  • ddx-web/src/app/[locale]/portal/clinic-admin/approvals/PendingApprovalsClient.tsx:62-80 — approvals client component, bulk-action wiring
  • ddx-web/src/proxy.ts:129-133AUTH_ROUTES list including /auth/register; bounces authenticated users away from sign-up pages
  • ddx-web/src/app/[locale]/public/partners/onboarding/page.tsx:39-248 — partner certification timeline (separate track from clinical-user onboarding)
  • ddx-seeder-ts/content/help/modules/doctor/01-getting-started/03-first-login.yaml:1-108 — seeded doctor first-login content (display name, timezone, signature, mandatory 2FA, notifications)
  • ddx-seeder-ts/content/help/modules/patient/01-getting-started/01-dashboard-tour.yaml:5 — patient first-login dashboard-tour content tag

Operational Notes

  • Verification order: a successful self-service registration redirects to /auth/verify-email first; a successful invitation registration verifies phone before the final POST. Do not reorder without coordinating with ddx-api's onboarding service.
  • Email + phone uniqueness: the backend (CR-029) routes both registration and patient creation through PatientCreationService. A registration that already has a phone match raises 409 — the client surfaces this via setGlobalError.
  • Rate limiting: the self-service path explicitly handles HTTP 429 (RegisterForm.tsx:85-86) by surfacing a localized rate-limit message. The invitation path inherits the same backend throttle but renders a generic error string.
  • Honeypot: the hidden website field at RegisterForm.tsx:508-520 is bait — do not remove or reposition. Removing it disables the cheapest bot filter and increases the approval-queue noise immediately.
  • Force logout on invitation: an authenticated session opening an invitation link triggers a server redirect to /api/auth/logout?callbackUrl=.... This is deliberate — cookies cannot be cleared in Server Components in Next.js 16; the Route Handler hop is required (register/page.tsx:111-118).
  • Skip / resume semantics: there is no resumable URL-driven multi-step state — refresh during phone verification on the invitation form re-renders step 'form' and the user resubmits. This is intentional for token security; a refresh that survives the unverified state would let an attacker resume someone else's invitation.
  • 2FA enrolment for clinicians: enforced by clinic policy, surfaced via the help tour, and required before signing prescriptions (first-login.yaml:101-108). Build alarms for "user signed prescription without 2FA" in the backend audit; this surface is not the only gate.
  • i18n keys: auth.register.* (self-service), auth.invitationRegister.* (invitation), auth.verifyEmail.*, auth.verifyPhone.*, admin.approvals.*. Always wrap new strings; zero hardcoded strings in TSX per project mandate (see context/REMINDERS.md:31).
  • Demo helpers: RegisterForm.tsx:113-126 exposes a "Fill with Fake Data" button gated by showDemoHelpers. This must be off in production builds — verify via NODE_ENV=production pnpm build before deploy.
  • User Management — practitioner/staff account model that the onboarding flow populates
  • Organization ManagementregistrationRequiresApproval, registrationApprovalRoles, password policy live on the Organization record
  • Tenant Provisioner — how a clinic itself is bootstrapped before any user can register against it
  • Multi-Portal Architecture — the role-aware shell users land in after first-login
  • Patient Portal — destination for self-service patient registrations
  • Doctor Portal — destination for invited clinicians; first-login help content is rendered here
  • i18n Multilingual — en/fr/de catalog routing used by every onboarding surface
    User Onboarding — Registration and Initial Setup Flows — Dudoxx Docs | Dudoxx