Custom Onboarding by User Type
Overview
The custom onboarding system provides a role-specific post-signup experience for Kulcare users. Rather than a one-size-fits-all wizard, each user type receives a tailored onboarding flow that collects only the information relevant to their role. The onboarding appears as a modal overlay on top of the Connect dashboard, so users feel like they are already inside the platform rather than stuck in a separate flow.
This system replaces the legacy 10-step onboarding wizard (documented in onboarding_implementation.md) which was designed for manual profile creation by the Kulcare team. In the new identity system, user profiles (Practitioner, Vendor, Staff) are auto-created at OTP verification via ProfileOrchestrationService. The onboarding modal enriches these auto-created profiles with additional professional details.
Onboarding Flows by User Type
| User Type | Onboarding Steps | What Happens After |
|---|---|---|
| Practitioner | 3 steps: Profile Basics, Professional Identity, Profile Import URL | Land on Connect with progressive profile completion prompts |
| Facility Owner | 1 step: Profile Basics | Land on Connect with "Set up your facility" CTA |
| Staff | No wizard | Land directly on Connect, profile completion via existing pages |
| Vendor | No wizard | Land directly on Connect, VendorProfilePage for completion |
Key Design Decisions
- Modal overlay, not a separate page: The onboarding renders as a Dialog on top of Connect so users see the dashboard behind it and can dismiss at any time.
- Skippable: Users can click "I'll do this later" to complete onboarding without filling in data. The
IdentityProfileCompletionBanneron Connect handles reminders. - Data pre-fills on re-open: If a user has already saved some data, the modal loads it from the backend and pre-fills the forms.
- No dual-role detection at signup: Facility owners who are also doctors add practitioner credentials later from Connect, not during onboarding.
Architecture
Component Structure
frontend/apps/shell/src/
components/identity/
OnboardingModal.tsx # Modal container with step routing
pages/identity/
OnboardingPage.tsx # Redirect only (/onboarding -> /connect)
onboarding/
ProfileBasicsStep.tsx # Step 1: Photo, bio, email
ProfessionalIdentityStep.tsx # Step 2: Specialization, experience, registration
ProfileImportStep.tsx # Step 3: External profile URL
frontend/apps/connect/src/
components/identity/
FacilitySetupCTA.tsx # CTA card for facility owners
IdentityProfileCompletionBanner.tsx # Updated with dual-role prompt
pages/
FacilitySetupPage.tsx # Placeholder facility setup page
backend/domains/identity/
controllers/
my_practitioner_controller.rb # Self-update endpoint (no tenant context)
policies/
my_practitioner_policy.rb # Own-record authorization
services/
save_onboarding_service.rb # Onboarding data persistence (64KB limit)
blueprints/
practitioner_blueprint.rb # Includes experience_years, external_profile_url
user_blueprint.rb # Includes onboarding_completedHow the Modal Renders
The OnboardingModal component is mounted globally inside AppRoutes.tsx (the shell's root routing component), outside of any specific route. It renders on every page but returns null early when not needed:
// AppRoutes.tsx
export function AppRoutes() {
return (
<>
<OnboardingModal />
<Routes>
{/* All app routes */}
</Routes>
</>
)
}The modal decides whether to show based on three conditions:
user.onboarding_completedisfalseuser.signed_up_asis'practitioner'or'facility'- The user hasn't dismissed it in the current session
When all three are true, the Dialog opens as a modal overlay. The step components are lazy-loaded so they don't add to the initial bundle for users who have already completed onboarding.
Data Flow
User signs up
--> OTP verification
--> ProfileOrchestrationService creates Practitioner/Vendor/Staff record
--> User lands on /connect
--> OnboardingModal checks onboarding_completed (false for new users)
--> Modal opens with step 1
Step 1 (Profile Basics):
--> Photo: presigned URL upload to S3, then PATCH /identity/users/:id with avatar_url
--> Email: PATCH /identity/users/:id
--> Bio: PATCH /identity/my/practitioner (practitioners only)
Step 2 (Professional Identity, practitioners only):
--> Specialization + experience + registration: PATCH /identity/my/practitioner
Step 3 (Profile Import, practitioners only):
--> External URL: PATCH /identity/my/practitioner with external_profile_url
--> Or skip
Completion:
--> POST /identity/users/:id/onboarding (marks onboarding_completed = true)
--> Auth store updated, modal dismissesAPI Endpoints
GET /identity/my/practitioner
Retrieves the current user's own practitioner profile. No tenant context required.
Authentication: Required (Bearer token)
Response: PractitionerBlueprint :detailed view, or { data: null } if no practitioner record exists.
Used by: OnboardingModal to pre-fill forms with existing data via the useMyPractitioner React Query hook.
PATCH /identity/my/practitioner
Updates the current user's own practitioner profile. No tenant context required.
Authentication: Required (Bearer token)
Authorization: MyPractitionerPolicy -- users can only update their own practitioner record.
Permitted params:
:first_name, :last_name, :display_name, :gender, :birth_date,
:avatar_url, :bio, :experience_years, :external_profile_url,
languages: [],
qualifications: [:code, :issuer, :year, :specialty],
identifiers: [:system, :value, :state],
specialties: [:id, :name, :is_primary]Used by: ProfessionalIdentityStep (specialization, experience, registration), ProfileImportStep (external URL), ProfileBasicsStep (bio for practitioners).
POST /identity/users/:user_id/onboarding
Saves onboarding progress and marks completion. Existing endpoint from the legacy system, reused here.
Authentication: Required (Bearer token)
Payload: Onboarding state including current_step, total_steps, completed_steps, and user_type.
Completion logic: When current_step == total_steps, SaveOnboardingService calls user.complete_onboarding! which sets onboarding_completed = true and onboarding_completed_at.
Size limit: 64KB maximum on onboarding data payload.
Frontend Components
OnboardingModal
The central orchestrator. Determines which steps to show based on user.signed_up_as, fetches existing practitioner data via useMyPractitioner(), and manages step navigation.
Key behaviors:
- Uses React Query for practitioner data (caching, deduplication, automatic retry)
- Step components are lazy-loaded via
React.lazy - Prevents outside click dismiss (users must explicitly skip or complete)
- Hides the Dialog's default close button via
[&>button:last-child]:hidden
ProfileBasicsStep
Collects profile photo (via S3 presigned URL upload), short bio (practitioners only), and email address (if not already set). The photo upload checks response.ok on the S3 PUT response before confirming success. Bio and email are saved in parallel via Promise.all.
ProfessionalIdentityStep
Collects primary specialization (static dropdown of common specialties), years of experience, and registration number (optional). All data saves to the practitioner record via PATCH /identity/my/practitioner.
ProfileImportStep
Allows users to paste a URL to their profile on Practo, Healthgrades, or any other platform. In v1, the URL is simply stored on the practitioner record. Scraping/auto-fill from the URL is planned for v2. Users can skip this step without entering anything.
FacilitySetupCTA
A prominent card shown on the Connect dashboard for facility owners who have a draft tenant (created at signup but not yet activated). Links to a placeholder facility setup page. The full facility creation flow is planned as a separate feature.
IdentityProfileCompletionBanner (Updated)
Updated to show a dual-role prompt for facility owners: "Are you also a healthcare professional? Set up your practitioner profile." This links to the existing PractitionerProfilePage on Connect, allowing facility owners who are also doctors to add their credentials.
Database Changes
Migration: external_profile_url
add_column 'identity.practitioners', :external_profile_url, :string, limit: 500Stores the user's external profile URL (Practo, Healthgrades, etc.). Validated as a valid HTTP(S) URL on the model, matching the same pattern used for avatar_url.
Serialization Changes
UserBlueprint:detailedview now includesonboarding_completed-- flows through to all auth responses (login, verification, profile switch).PractitionerBlueprint:detailedview now includesexperience_years,external_profile_url, and rawspecialtiesJSONB (with fallback tospecialties_from_qualifications).
Plan Tier Change
Facilities::SetupService now creates draft tenants with plan_tier: 'free' instead of 'starter'. This aligns with the decision that facilities start on a free plan and upgrade to paid plans for CMS/Growth/Hospital features.
Routing
| Route | Behavior |
|---|---|
/onboarding | Redirects to /connect (modal handles onboarding) |
/connect | Connect dashboard with OnboardingModal overlay for incomplete users |
/connect/facility-setup | Placeholder facility setup page |
/connect/profile/practitioner | Existing practitioner profile page (linked from dual-role prompt) |
After OTP verification, all users are routed to /connect. The OnboardingModal automatically appears for practitioners and facility owners who haven't completed onboarding.
Dev Tools
In development mode (import.meta.env.DEV), a "Reset Onboarding" button appears at the bottom of the Connect HomePage and DashboardPage. Clicking it sets onboarding_completed: false in the client-side auth store, causing the modal to reappear. This is a client-side-only toggle -- it does not reset the backend flag, so a page refresh will restore the completed state.
Related Documentation
- Legacy onboarding:
docs/guides/developer/identity/04-features/onboarding/onboarding_implementation.md-- the 10-step wizard this feature replaces - Brainstorm:
docs/brainstorms/2026-03-17-custom-onboarding-by-user-type-brainstorm.md - Implementation plan:
docs/plans/2026-03-17-feat-custom-onboarding-by-user-type-plan.md - Auth plan:
docs/pending/2026-03-10-feat-new-identity-signup-onboarding-auth-plan.md
Future Work
- Facility setup form: Full facility activation flow (name, type, location, hours) -- currently a placeholder page with sales contact link
- Profile URL scraping (v2): Build Practo and Healthgrades scrapers to auto-fill profile data from pasted URLs
- Legacy wizard deprecation: Remove the 10-step wizard from the shell app once all users are on the new identity system
- Country-specific fields: Adapt Step 2 based on country (registration council for India, NPI for US)