Kulcare Docs
DeveloperAuthentication Documentation04 featuresOnboarding

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 TypeOnboarding StepsWhat Happens After
Practitioner3 steps: Profile Basics, Professional Identity, Profile Import URLLand on Connect with progressive profile completion prompts
Facility Owner1 step: Profile BasicsLand on Connect with "Set up your facility" CTA
StaffNo wizardLand directly on Connect, profile completion via existing pages
VendorNo wizardLand 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 IdentityProfileCompletionBanner on 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_completed

How 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:

  1. user.onboarding_completed is false
  2. user.signed_up_as is 'practitioner' or 'facility'
  3. 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 dismisses

API 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: 500

Stores 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 :detailed view now includes onboarding_completed -- flows through to all auth responses (login, verification, profile switch).
  • PractitionerBlueprint :detailed view now includes experience_years, external_profile_url, and raw specialties JSONB (with fallback to specialties_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

RouteBehavior
/onboardingRedirects to /connect (modal handles onboarding)
/connectConnect dashboard with OnboardingModal overlay for incomplete users
/connect/facility-setupPlaceholder facility setup page
/connect/profile/practitionerExisting 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.

  • 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)

On this page