Kulcare Docs
Uhi

UHI First & Second Search — Architecture Audit

Audit Date: 2026-06-25 Scope: First search (broadcast) and second search (P2P) — FE + BE implementation vs UHI spec v2.0


1. Architecture Overview

Network Roles

RoleSubscriber IDDescription
Kulcare EUAkulcare.euaPatient-facing app that searches for doctors
Kulcare HSPAkulcare.hspaKulcare's own provider catalog (doctors in Kulcare network)
External EUAseua-nha, aarogyaSetu.eua, etc.Third-party patient apps that may search Kulcare's HSPA
External HSPAshealthSmarttech.hspa, eSushrutLite.hspa, hspa-nha, etc.Third-party providers in the UHI network

Kulcare operates in both EUA and HSPA roles on the same system. External EUAs can search Kulcare's HSPA, and Kulcare's EUA can search external HSPAs.

Bidirectional Communication

Kulcare's backend handles both roles simultaneously:

Kulcare as EUA (initiating):

  • /uhi/eua/search — broadcasts search to all HSPAs via Gateway
  • /uhi/eua/search/p2p — sends P2P search to specific HSPA
  • /uhi/eua/on_search — receives catalog responses from HSPAs
  • /uhi/eua/on_init, /on_confirm, etc. — receives booking callbacks from HSPAs

Kulcare as HSPA (responding):

  • /uhi/hspa/search — receives searches from ANY EUA (Gateway broadcast or direct P2P)
  • /uhi/hspa/on_search — sends catalog to requesting EUA
  • /uhi/hspa/init, /confirm, etc. — receives booking requests from EUAs

External EUAs that may search Kulcare's HSPA: eua-nha, aarogyaSetu.eua, etc. External HSPAs that Kulcare's EUA may search: healthSmarttech.hspa, eSushrutLite.hspa, hspa-nha, etc.

First Search (Broadcast) Flow

Patient → Kulcare BE (EUA) → UHI Gateway → All HSPAs (including Kulcare HSPA)
                                                              ↓
                                                    Each HSPA responds
                                                              ↓
Kulcare ← Gateway ← on_search callbacks from all HSPAs
   ↓
Ably → Patient (real-time)
  1. Patient fills search form → POST /uhi/eua/search
  2. Kulcare BE logs UhiRequest with user_id, sends to Gateway
  3. Gateway broadcasts to all registered HSPAs (including Kulcare's own HSPA)
  4. Each HSPA logs webhook, builds catalog, sends on_search via Gateway
  5. Kulcare receives on_search via Gateway, logs UhiWebhook, publishes to Ably
  6. Patient receives real-time updates, then polls /results for complete data

Note: External EUAs (e.g., eua-nha) also send searches to Kulcare's HSPA via Gateway. Kulcare's HSPA receives these at /uhi/hspa/search and responds with its catalog.

Second Search (P2P) Flow

Patient → Kulcare BE (EUA) → External HSPA (direct)
                                         ↓
                              on_search (direct callback)
                                         ↓
                              Kulcare → Ably → Patient
  1. Patient selects a provider → navigates to slot selection
  2. POST /uhi/eua/search/p2p with provider_uri, transaction_id, intent
  3. Kulcare BE logs UhiRequest with user_id, sends directly to HSPA
  4. External HSPA responds with time slots
  5. Kulcare receives on_search directly, publishes to Ably, patient polls /results

2. Database Schema

uhi.requests — Outbound API calls

ColumnTypePurpose
idbigint PK
transaction_iduuidUHI transaction (same across search → init → confirm)
message_iduuidUnique per message
actionstringsearch, init, confirm, etc.
directionstringAlways outbound
endpointtextFull URL called
payloadjsonbFull request body
responsejsonbResponse body
status_codeintHTTP status
statusstringpending, success, failed
user_iduuidPatient's user ID for Ably routing

uhi.webhooks — Inbound callbacks

Note: Webhooks are received at two endpoint groups:

  • /uhi/eua/on_* — Receives callbacks FROM external HSPAs (they respond to Kulcare's searches)
  • /uhi/hspa/* — Receives requests FROM external EUAs (they search Kulcare's catalog)
ColumnTypePurpose
idbigint PK
transaction_iduuidUHI transaction
message_iduuidSame as transaction_id
actionstringon_search, on_init, etc.
sourcestringgateway (broadcast) or eua_p2p (direct P2P)
source_idstringEUA subscriber_id that initiated the search (consumer_id)
source_uristringHSPA's base URL
payloadjsonbFull request body
statusstringpending → processed or failed
processed_datajsonbMetadata (provider_count, etc.)

3. Issues Found

🔴 CRITICAL — Must Fix

Issue 1: Ably Event Field Name Mismatch

File: patient/src/hooks/useUhiRealtime.ts

The frontend hook expects total_count but the backend sends total_providers.

// useUhiRealtime.ts — LINE 36
interface UhiSearchCompleteEvent {
  type: 'search_complete'
  transaction_id: string
  total_count: number  // ❌ WRONG — backend sends total_providers
  timestamp: string
}
# backend/domains/uhi/services/realtime_publisher.rb — LINE 47
channel.publish('search_complete', {
  total_providers: total_providers,  // ✓ CORRECT per UHI spec
  ...
})

Impact: UhiSearchCompleteEvent.total_count is always undefined. This could break any FE logic that relies on this field (e.g., showing total provider count).

Fix: Change frontend interface to use total_providers.


Issue 2: source_id Set to Wrong Value in EUA Callbacks

Files:

  • backend/domains/uhi/models/uhi_webhook.rb (affects all EUA inbound callbacks)
  • backend/domains/uhi/controllers/eua/on_search_controller.rb
  • backend/domains/uhi/controllers/eua/on_init_controller.rb
  • backend/domains/uhi/controllers/eua/on_confirm_controller.rb
  • backend/domains/uhi/controllers/eua/on_status_controller.rb
  • backend/domains/uhi/controllers/eua/on_update_controller.rb
  • backend/domains/uhi/controllers/eua/on_cancel_controller.rb
  • backend/domains/uhi/controllers/eua/on_message_controller.rb

Bug: source_id is being set to context.provider_id (the HSPA that responded), but it should be context.consumer_id (the EUA that initiated the request).

# Current (WRONG)
source_id: payload.dig('context', 'provider_id')  # = "healthSmarttech.hspa"
# Should be
source_id: payload.dig('context', 'consumer_id')  # = "kulcare.eua"

Context fields in callbacks:

  • context.consumer_id = "kulcare.eua" (the EUA that sent the original search)
  • context.provider_id = "healthSmarttech.hspa" (the HSPA that is responding)

Impact:

  • Webhook source_id logs are incorrect — they show the responder instead of the initiator
  • Any logic relying on source_id to identify the EUA will break
  • The /results endpoint filters by source_id which may not work correctly

Note: HSPA controllers are correct — they correctly use consumer_id as source_id:

# backend/domains/uhi/controllers/hspa/search_controller.rb — CORRECT
source_id: @raw_payload.dig('context', 'consumer_id')  # = "kulcare.eua"

Fix: Update UhiWebhook.log_inbound to use consumer_id first, then update all EUA controllers to pass correct value.


Issue 3: UhiProvider TypeScript Interface Missing items Field

File: patient/src/features/uhi/types/uhi.types.ts

The UhiProvider interface is missing the items array which is used in P2P search and booking.

// Current — MISSING items
export interface UhiProvider {
  id: string;
  source_id?: string;
  descriptor: { name: string; code?: string };
  categories?: UhiCategory[];
  fulfillments?: UhiFulfillment[];
  // items is used throughout but not defined!
  // items?: UhiItem[];  // MISSING
}

Usage throughout codebase:

  • UhiSlotSelectionPage.tsx: provider.items?.find((i) => i.fulfillment_id === slot.id)
  • UhiProviderCard.tsx: provider.items?.[0]?.price?.value
  • uhiApi.ts P2P search: sends params.items in intent
  • search_service.rb: echoes items in P2P payload

Impact: TypeScript errors when accessing items, potential runtime issues.

Fix: Add items?: UhiItem[] to UhiProvider interface.


🟡 FEATURES — Improvements

Feature 4: Slot Selection Page — URL Date Persistence

File: patient/src/features/uhi/pages/UhiSlotSelectionPage.tsx

Currently, when a patient refreshes the slot selection page, the selected date is lost.

Current URL: /uhi/slots?tx={tx}&providerId={id}&sourceId={sourceId}

Requested URL: /uhi/slots?tx={tx}&providerId={id}&sourceId={sourceId}&date={YYYY-MM-DD}

Implementation:

  1. Add date query param to URL
  2. Read date on mount and use it as initial active date
  3. Update URL when user changes date
  4. Validate date is within the broadcast search's date_from → date_to range

Feature 5: Slot Selection Page — Date Dropdown Instead of Tabs

File: patient/src/features/uhi/components/UhiSlotGrid.tsx

Currently uses 7-day tab selector. Change to a date dropdown picker for better UX when:

  • Date range is larger than 7 days
  • Patient wants to navigate to a specific date

Feature 6: Slot Selection Page — Date Range Validation

File: patient/src/features/uhi/pages/UhiSlotSelectionPage.tsx

The date range shown in the slot grid should be limited to the original broadcast search's date range (date_from → date_to).

Currently:

  • P2P search always sends 7 days or the selected date
  • No validation that selected date is within broadcast search window

Requested behavior:

  1. Store broadcast search date range in URL or state
  2. Validate selected date is within range
  3. Show message if date is outside range
  4. Disable date picker options outside range

4. Correctly Implemented ✅

First Search (Broadcast)

FieldSpecImplementationStatus
context.domainnic2004:85111✅ Correct✅
context.countryIND✅ Correct✅
context.citystd:011✅ Correct✅
context.actionsearch✅ Correct✅
context.core_version0.7.1✅ Correct✅
context.consumer_idEUA subscriber_id✅ Correct✅
context.consumer_uriEUA callback URL✅ Correct✅
context.transaction_idUUID✅ Correct✅
context.message_idUUID✅ Correct✅
context.timestampISO 8601✅ Correct (IST, no Z)✅
message.intent.item.descriptor.codeConsultation✅ Correct✅
message.intent.item.descriptor.nameConsultation✅ Correct✅
message.intent.fulfillment.typePhysical✅ Correct✅
message.intent.fulfillment.start.time.timestampISO 8601✅ Correct✅
message.intent.fulfillment.end.time.timestampISO 8601✅ Correct✅
message.intent.category.descriptor.codeSpecialty code✅ Correct✅
ACK response format{ message: { ack: { status: "ACK" } } }✅ Correct✅

Second Search (P2P)

FieldSpecImplementationStatus
context.provider_idHSPA subscriber_id✅ Correct✅
context.provider_uriHSPA base URL✅ Correct✅
context.consumer_idEUA subscriber_id✅ Correct✅
message.intent.provider.idProvider ID from first search✅ Correct✅
message.intent.provider.categoriesEchoed from first search✅ Correct✅
message.intent.provider.itemsEchoed from first search✅ Correct✅
message.intent.provider.fulfillmentsEchoed from first search✅ Correct✅
message.intent.fulfillment.agent.idDoctor HPR ID✅ Correct✅

on_search Response (First & Second)

FieldSpecImplementationStatus
context.actionon_search✅ Correct✅
context.consumer_idEUA subscriber_id✅ Correct✅
context.provider_idHSPA subscriber_id✅ Correct✅
context.provider_uriHSPA base URL✅ Correct✅
context.transaction_idSame as request✅ Correct✅
message.catalog.providers[].idProvider ID✅ Correct✅
message.catalog.providers[].descriptor.nameFacility name✅ Correct✅
message.catalog.providers[].fulfillments[].idSlot UUID✅ Correct✅
message.catalog.providers[].fulfillments[].typePhysical✅ Correct✅
message.catalog.providers[].fulfillments[].agentDoctor info✅ Correct✅
message.catalog.providers[].items[].priceConsultation fee✅ Correct✅
message.catalog.providers[].items[].fulfillment_idLinks to slot✅ Correct✅

HSPA Code (Receiving Searches)

Kulcare's HSPA receives searches from ANY EUA including kulcare.eua, eua-nha, aarogyaSetu.eua, etc.

FeatureImplementationStatus
P2P detectionChecks provider_id presence✅ Correct
P2P routingDirect to consumer_uri✅ Correct
Broadcast routingVia Gateway✅ Correct
source_id in webhookUses consumer_id (the EUA that searched)✅ Correct

Real-Time (Ably)

FeatureImplementationStatus
Channel conventionprivate:user:{user_id}✅ Correct
publish_on_search payload{ type, transaction_id, providers }✅ Correct
Broadcast search routinguser_id from UhiRequest lookup✅ Correct

EUA Code (Receiving from External HSPAs)

Kulcare's EUA endpoints (/uhi/eua/on_*) receive callbacks from ANY HSPA including kulcare.hspa, healthSmarttech.hspa, eSushrutLite.hspa, hspa-nha, etc.

FeatureImplementationStatus
on_search routingLogs webhook, publishes to Ably by user_id✅ Correct
P2P user_id lookupVia UhiRequest.transaction_id✅ Correct

Other

FeatureImplementationStatus
transaction_id uniquenessUUID per search✅ Correct
transaction_id threadingSame across booking lifecycle✅ Correct
P2P user_id routingStored on UhiRequest, looked up via transaction_id✅ Correct

5. Action Items Summary

#SeverityTitleFiles
1🔴Fix Ably event field total_count → total_providersuseUhiRealtime.ts
2🔴Fix source_id to use consumer_id in EUA callbacksuhi_webhook.rb, all EUA controllers
3🔴Add items field to UhiProvider interfaceuhi.types.ts
4🟡Add date to slot selection URLUhiSlotSelectionPage.tsx
5🟡Change date tabs to dropdownUhiSlotGrid.tsx
6🟡Validate date range from broadcast searchUhiSlotSelectionPage.tsx

6. Reference Documentation

  • UHI Spec: docs/guides/uhi/UHI Physical Consultation - Onboarding Document_v2.0.docx.txt
  • Architecture Doc: docs/guides/uhi/search-flow-architecture.md

On this page