Kulcare Docs
Uhi

UHI Search Flow Architecture

Last updated: 2026-06-25
Scope: First search (broadcast) and second search (P2P) — FE + BE implementation details.


1. Architecture Overview

┌──────────────────────────────────────────────────────────────────────┐
│                          UHI Network                                  │
│                                                                       │
│  ┌──────────┐     broadcast     ┌──────────┐     broadcast    ┌─────┐│
│  │ Kulcare  │ ──── search ────▶ │  UHI     │ ──── search ───▶ │ HSPA││
│  │  EUA     │                   │ Gateway  │                  │  A  ││
│  │ (patient │ ◀── on_search ─── │          │ ◀── on_search ── │     ││
│  │  app)    │                   └──────────┘                  └─────┘│
│  │          │                                                    ... │
│  │          │   ┌──────────────────────────────────────────────┐    │
│  │          │   │ P2P direct (second search)                    │    │
│  │          │───│ POST /search ───────────────▶ HSPA B         │    │
│  │          │   │ POST /on_search ◀──────────── (direct resp)  │    │
│  │          │   └──────────────────────────────────────────────┘    │
│  └──────────┘                                                       │
│                                                                      │
│  ┌──────────┐    Kulcare also acts as HSPA for its own directory    │
│  │ Kulcare  │    (Raj Kumar, City Hospital, etc.)                   │
│  │  HSPA    │                                                       │
│  └──────────┘                                                       │
└──────────────────────────────────────────────────────────────────────┘

Kulcare operates in both roles:

RoleEndpoint prefixWhat it does
EUA (patient app)/uhi/eua/Searches for doctors, sends P2P requests
HSPA (clinic directory)/uhi/hspa/Returns its own catalog (Raj Kumar, City Hospital)

2. Database Schema

uhi.requests — Outbound API calls

ColumnTypePurpose
idbigint PK
transaction_iduuidUHI transaction (shared by search + on_search pair)
message_iduuidUnique per message (same as transaction_id for simplicity)
actionstringsearch, on_search, on_init, on_confirm, etc.
directionstringoutbound (from Kulcare to network) or inbound (from network to Kulcare)
endpointtextFull URL the request was sent to
payloadjsonbFull request body (context + message)
responsejsonbResponse body from the HTTP call
status_codeintHTTP status code
statusstringpending, success, failed
user_iduuidFor P2P searches only — the patient's user ID for Ably routing

uhi.webhooks — Inbound callbacks

ColumnTypePurpose
idbigint PK
transaction_iduuidUHI transaction
message_iduuidSame as transaction_id
actionstringsearch, on_search, on_init, etc.
sourcestringgateway (broadcast) or eua_p2p (direct P2P)
source_idstringBroadcast: subscriber ID (healthSmarttech.hspa). P2P: numeric provider ID (20). ⚠️ Inconsistent — see §6
source_uristringHSPA's base URL (e.g. https://qa.healthcarehms.com/abdm/api/uhi)
payloadjsonbFull request body
statusstringpending → processed or failed
processed_datajsonbMetadata set after processing (provider_count, consumer_uri, etc.)

3. First Search (Broadcast) Flow

Sequence Diagram

Patient (FE)          Kulcare BE (EUA)         UHI Gateway         Kulcare HSPA        Other HSPAs
     │                      │                      │                    │                   │
     │ 1. Search form        │                      │                    │                   │
     │─────────────────────▶│                      │                    │                   │
     │ POST /api/uhi/eua/   │                      │                    │                   │
     │        search         │                      │                    │                   │
     │                      │                      │                    │                   │
     │                      │ 2. broadcast_search   │                    │                   │
     │                      │─────────────────────▶│                    │                   │
     │                      │ POST /search          │                    │                   │
     │                      │ (Gateway URL)         │                    │                   │
     │                      │                      │ 3. Forward search   │                   │
     │                      │                      │───────────────────▶│                   │
     │                      │                      │──────────────────────────────────▶│
     │                      │                      │                    │                   │
     │                      │                      │ 4. on_search (ACK)  │                   │
     │                      │                      │◀───────────────────│                   │
     │                      │                      │◀──────────────────────────────────│
     │                      │                      │                    │                   │
     │                      │                      │ 5. Forward on_search│                   │
     │                      │ 6. POST /uhi/on_search◀───────────────────│                   │
     │                      │◀─────────────────────│ (to all EUAs)      │                   │
     │                      │                      │                    │                   │
     │                      │ 7. Ably publish       │                    │                   │
     │                      │    private:user:{id}  │                    │                   │
     │  8. Real-time update  │                      │                    │                   │
     │◀─────────────────────│                      │                    │                   │
     │                      │                      │                    │                   │

Step-by-Step

Step 1: FE — UhiSearchPage

  • File: patient/src/features/uhi/pages/UhiSearchPage.tsx
  • User fills search form (doctor name, city, specialty, date range)
  • Calls POST /api/uhi/eua/search with intent payload
  • Intimately subscribes to Ably channel private:user:{currentUserId}

Step 2: BE — Eua::SearchController#create

  • File: backend/domains/uhi/controllers/eua/search_controller.rb
  • Creates SearchService with EUA credentials
  • Calls broadcast_search(search_params:, transaction_id:)
  • Returns { success: true, transaction_id } to FE
  • File: backend/domains/uhi/services/search_service.rb
  • Builds search payload with context (city, domain, country, consumer_id)
  • Signs auth header with EUA private key (Ed25519)
  • Logs outbound via UhiLogger.log_outbound → creates uhi.requests row
  • POSTs to https://uhigatewaysandbox.abdm.gov.in/api/v1/uhi/search
  • Gateway forwards to all registered HSPAs
  • Each HSPA receives the search and returns on_search via Gateway
  • For Kulcare's own HSPA: Hspa::SearchController#create receives the search
  • Calls CatalogService#send_on_search which builds the catalog response
  • Sends on_search back via Gateway

Step 6: BE — Eua::OnSearchController#create

  • File: backend/domains/uhi/controllers/eua/on_search_controller.rb
  • Receives POST /uhi/on_search from Gateway
  • Logs webhook via UhiLogger.log_inbound → creates uhi.webhooks row
  • Extracts subscriber_id from context (set by original broadcast search)
  • Calls RealtimePublisher.publish_on_search with user_id, providers

Step 7: BE — RealtimePublisher

  • File: backend/domains/uhi/services/realtime_publisher.rb
  • Publishes to Ably channel private:user:{user_id}
  • Message format: { type: 'on_search', transaction_id, providers, source: 'hspa' }

Step 8: FE — useUhiRealtime

  • File: patient/src/features/uhi/hooks/useUhiRealtime.ts
  • Subscribes to Ably channel for the current user
  • Accumulates providers from multiple HSPA responses
  • When search_complete message arrives, transitions to complete state
  • UhiSearchCompleteState renders provider cards (UhiProviderCard)

4. Second Search (P2P) Flow

Sequence Diagram

Patient (FE)          Kulcare BE (EUA)              HealthSmarttech HSPA
     │                      │                              │
     │ 1. Select doctor      │                              │
     │    → SlotSelection    │                              │
     │                      │                              │
     │ 2. POST /api/uhi/eua/│                              │
     │    search/p2p         │                              │
     │    {provider_uri,     │                              │
     │     provider_id,      │                              │
     │     transaction_id}   │                              │
     │─────────────────────▶│                              │
     │                      │                              │
     │                      │ 3. direct_search             │
     │                      │    user_id: current_user.id  │
     │                      │─────────────────────────────▶│
     │                      │ POST /search                  │
     │                      │ (direct to HSPA URL)          │
     │                      │                              │
     │                      │      4. HealthSmarttech       │
     │                      │      builds catalog with     │
     │                      │      actual time slots       │
     │                      │                              │
     │                      │ 5. POST /uhi/on_search        │
     │                      │◀─────────────────────────────│
     │                      │ (direct to kulcare EUA)      │
     │                      │                              │
     │                      │ 6. Ably publish               │
     │                      │    private:user:{id}          │
     │                      │                              │
     │ 7. Poll every 5s      │                              │
     │ GET /api/uhi/eua/     │                              │
     │ search/{tx}/results   │                              │
     │ ?provider_id=20       │                              │
     │ &sourceId=health...   │                              │
     │─────────────────────▶│                              │
     │                      │                              │
     │ 8. Returns catalog    │                              │
     │    with fulfillments  │                              │
     │◀─────────────────────│                              │
     │                      │                              │
     │ 9. UhiSlotGrid        │                              │
     │    renders time slots │                              │

Step-by-Step

Step 1-2: FE — UhiSlotSelectionPage

  • File: patient/src/features/uhi/pages/UhiSlotSelectionPage.tsx
  • User taps "View Slots" on a provider card
  • URL: /uhi/slots?tx={transaction_id}&providerId={20}&sourceId={healthSmarttech.hspa}
  • On mount, calls POST /api/uhi/eua/search/p2p with:
    {
      "intent": { "provider": { "id": "20" }, "fulfillment": { ... } },
      "provider_uri": "https://qa.healthcarehms.com/abdm/api/uhi",
      "transaction_id": "6f9a4791-..."
    }

Step 3: BE — Eua::SearchController#create_p2p

  • File: backend/domains/uhi/controllers/eua/search_controller.rb
  • Extracts provider_uri from params
  • Calls service.direct_search(provider_uri:, search_params:, transaction_id:, user_id: current_user.id)
  • The user_id is critical — it's stored on the uhi.requests row and used to route the on_search callback
  • File: backend/domains/uhi/services/search_service.rb
  • Builds P2P search payload with provider details (echoes provider.id, items, categories from first search)
  • Signs auth header
  • Logs outbound via UhiLogger.log_outbound(..., user_id: user_id) → sets user_id on uhi.requests
  • POSTs directly to {provider_uri}/search (bypasses Gateway)

Step 4-5: HealthSmarttech responds

  • HealthSmarttech processes the P2P search
  • Sends on_search directly to https://api-stg-uhi.kulcare.com/uhi/eua/on_search
  • Response contains actual time slots for the requested provider

Step 6: BE — Eua::OnSearchController receives P2P

  • Same controller as broadcast, but context differs:
    • provider_id: "20" (numeric, not subscriber ID) ⚠️
    • consumer_id: "kulcare.eua"
    • provider_uri: "https://qa.healthcarehms.com/abdm/api/uhi"
  • user_id routing:
    # 1. Try subscriber_id from context (set during broadcast search)
    publish_user_id = @raw_payload.dig('context', 'subscriber_id')
    
    # 2. Fallback: look up from UhiRequest where user_id was set during P2P search
    if publish_user_id.blank?
      req = Uhi::UhiRequest.find_by(transaction_id: tx, action: 'search')
      publish_user_id = req&.user_id
    end
    
    # 3. Fallback: numeric consumer_id
    if publish_user_id.blank?
      consumer_id = @raw_payload.dig('context', 'consumer_id')
      publish_user_id = consumer_id if consumer_id.to_i > 0
    end
  • Publishes to Ably: RealtimePublisher.publish_on_search(user_id:, transaction_id:, providers:)
  • Logs webhook with source_id: from context's provider_id (⚠️ "20" for P2P, "healthSmarttech.hspa" for broadcast)

Step 7-8: FE polls /results

  • After P2P search POST returns, FE starts polling every 5 seconds
  • GET /api/uhi/eua/search/{tx}/results?provider_id=20&sourceId=healthSmarttech.hspa
  • Backend /results endpoint:
    # Filter by source_id first
    webhooks = webhooks.where(source_id: params[:sourceId])  # "healthSmarttech.hspa"
    
    # P2P webhooks have source_id="20" (numeric), not "healthSmarttech.hspa"
    # Fallback: if no results, take most recent webhook for this tx
    if webhooks.empty?
      latest = Uhi::UhiWebhook.where(transaction_id: tx, action: 'on_search')
                             .order(created_at: :desc).first
      webhooks = [latest]
    end
  • Returns catalog with providers and their fulfillments (time slots)

Step 9: FE — UhiSlotGrid renders

  • UhiSlotGrid groups slots by date
  • 7-day date tab selector
  • Each slot card shows time, fee, type badge (Physical/In-Clinic/Tele)

5. Key Data Flow Details

user_id Threading (P2P)

Kulcare P2P searches set user_id on the outbound uhi.requests row. When the on_search callback arrives, the controller uses this to publish to the correct Ably channel:

create_p2p → direct_search(user_id: current_user.id)
  → UhiLogger.log_outbound(user_id: user_id)  // stored on uhi.requests
    → HealthSmarttech receives search
    → HealthSmarttech responds with on_search
      → OnSearchController: req = find_by(transaction_id:, action: 'search')
      → publish_user_id = req.user_id  // found!
      → Ably publish to private:user:{id}

Without this, P2P on_search responses would silently fail to reach the frontend.

source_id Inconsistency (⚠️)

Response typecontext.provider_idStored as source_id
Broadcast (via Gateway)healthSmarttech.hspahealthSmarttech.hspa
P2P (direct)20 (numeric role ID)20

The /results endpoint has a fallback: if filtering by sourceId yields nothing, it takes the most recent webhook for the transaction.

Timestamps

  • Search intent timestamps: No timezone indicator (e.g., 2026-06-24T12:30:00)
  • Kulcare HSPA response timestamps: IST wall-clock (converted via to_ist())
  • HealthSmarttech response timestamps: Appear to be UTC (e.g., 12:30 UTC = 18:00 IST)
  • Frontend: new Date(timestamp) interprets timezone-less strings as UTC
  • The UhiSlotGrid shows ALL slots (no future-only filtering) to avoid timezone ambiguity

Transaction Lifecycle

  1. Search initiated: uhi.requests row created (action: search, direction: outbound)
  2. Search received at HSPA: uhi.webhooks row created (action: search)
  3. on_search sent: uhi.requests row created (action: on_search, direction: outbound)
  4. on_search received at EUA: uhi.webhooks row created (action: on_search)
  5. FE polls: GET /results queries uhi.webhooks for the transaction

6. Frontend Component Map

UhiSearchPage
├── UhiSearchForm          ← User enters doctor name, city, specialty
├── UhiSearchLoadingState  ← Shown while waiting for Ably
├── UhiSearchCompleteState ← Shown when search_complete fires
│   └── UhiProviderCard[]  ← One card per provider from all HSPAs
│       └── "View Slots" button → navigates to UhiSlotSelectionPage
│
UhiSlotSelectionPage
├── Date tabs (7-day window)
├── UhiSlotGrid
│   └── UhiSlotCard[]     ← One card per time slot
│       └── "Book" button → UhiBookingConfirmationPage
│
UhiProviderCard
├── CatalogDescriptor     ← Platform badge (logo + name from HSPA)
├── Doctor info           ← Name, specialties, experience, languages
├── Location              ← Address from catalog
├── Fee                   ← From item.price
└── "View Slots" button

7. Known Issues & Gotchas

  1. P2P source_id mismatch: P2P webhooks store numeric provider_id as source_id, not the subscriber ID. /results fallback handles this.
  2. Timezone ambiguity: No timezone on timestamps from external HSPAs. Frontend interprets as UTC. Slot grid shows all slots to avoid filtering issues.
  3. Duplicate searches: EUA reference app sends 2 requests for each search (broadcast + P2P). Kulcare responds 1:1 to each.
  4. Ably vs polling: Broadcast results arrive via Ably. P2P results are polled every 5 seconds. Both channels populate the same provider/slot data.
  5. In-clinic type: HealthSmarttech sends type: "In-clinic" instead of "Physical". Added to types as valid variant.
  6. Image rendering: External HSPAs send placeholder text as catalog images. toDataUri() returns null for non-image strings.

On this page