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
| Role | Subscriber ID | Description |
|---|---|---|
| Kulcare EUA | kulcare.eua | Patient-facing app that searches for doctors |
| Kulcare HSPA | kulcare.hspa | Kulcare's own provider catalog (doctors in Kulcare network) |
| External EUAs | eua-nha, aarogyaSetu.eua, etc. | Third-party patient apps that may search Kulcare's HSPA |
| External HSPAs | healthSmarttech.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)- Patient fills search form → POST
/uhi/eua/search - Kulcare BE logs
UhiRequestwithuser_id, sends to Gateway - Gateway broadcasts to all registered HSPAs (including Kulcare's own HSPA)
- Each HSPA logs webhook, builds catalog, sends
on_searchvia Gateway - Kulcare receives
on_searchvia Gateway, logsUhiWebhook, publishes to Ably - Patient receives real-time updates, then polls
/resultsfor 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/searchand responds with its catalog.
Second Search (P2P) Flow
Patient → Kulcare BE (EUA) → External HSPA (direct)
↓
on_search (direct callback)
↓
Kulcare → Ably → Patient- Patient selects a provider → navigates to slot selection
- POST
/uhi/eua/search/p2pwithprovider_uri,transaction_id,intent - Kulcare BE logs
UhiRequestwithuser_id, sends directly to HSPA - External HSPA responds with time slots
- Kulcare receives
on_searchdirectly, publishes to Ably, patient polls/results
2. Database Schema
uhi.requests — Outbound API calls
| Column | Type | Purpose |
|---|---|---|
id | bigint PK | |
transaction_id | uuid | UHI transaction (same across search → init → confirm) |
message_id | uuid | Unique per message |
action | string | search, init, confirm, etc. |
direction | string | Always outbound |
endpoint | text | Full URL called |
payload | jsonb | Full request body |
response | jsonb | Response body |
status_code | int | HTTP status |
status | string | pending, success, failed |
user_id | uuid | Patient'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)
| Column | Type | Purpose |
|---|---|---|
id | bigint PK | |
transaction_id | uuid | UHI transaction |
message_id | uuid | Same as transaction_id |
action | string | on_search, on_init, etc. |
source | string | gateway (broadcast) or eua_p2p (direct P2P) |
source_id | string | EUA subscriber_id that initiated the search (consumer_id) |
source_uri | string | HSPA's base URL |
payload | jsonb | Full request body |
status | string | pending → processed or failed |
processed_data | jsonb | Metadata (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.rbbackend/domains/uhi/controllers/eua/on_init_controller.rbbackend/domains/uhi/controllers/eua/on_confirm_controller.rbbackend/domains/uhi/controllers/eua/on_status_controller.rbbackend/domains/uhi/controllers/eua/on_update_controller.rbbackend/domains/uhi/controllers/eua/on_cancel_controller.rbbackend/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_idlogs are incorrect — they show the responder instead of the initiator - Any logic relying on
source_idto identify the EUA will break - The
/resultsendpoint filters bysource_idwhich 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?.valueuhiApi.tsP2P search: sendsparams.itemsin intentsearch_service.rb: echoesitemsin 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:
- Add
datequery param to URL - Read
dateon mount and use it as initial active date - Update URL when user changes date
- Validate date is within the broadcast search's
date_from→date_torange
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:
- Store broadcast search date range in URL or state
- Validate selected date is within range
- Show message if date is outside range
- Disable date picker options outside range
4. Correctly Implemented ✅
First Search (Broadcast)
| Field | Spec | Implementation | Status |
|---|---|---|---|
context.domain | nic2004:85111 | ✅ Correct | ✅ |
context.country | IND | ✅ Correct | ✅ |
context.city | std:011 | ✅ Correct | ✅ |
context.action | search | ✅ Correct | ✅ |
context.core_version | 0.7.1 | ✅ Correct | ✅ |
context.consumer_id | EUA subscriber_id | ✅ Correct | ✅ |
context.consumer_uri | EUA callback URL | ✅ Correct | ✅ |
context.transaction_id | UUID | ✅ Correct | ✅ |
context.message_id | UUID | ✅ Correct | ✅ |
context.timestamp | ISO 8601 | ✅ Correct (IST, no Z) | ✅ |
message.intent.item.descriptor.code | Consultation | ✅ Correct | ✅ |
message.intent.item.descriptor.name | Consultation | ✅ Correct | ✅ |
message.intent.fulfillment.type | Physical | ✅ Correct | ✅ |
message.intent.fulfillment.start.time.timestamp | ISO 8601 | ✅ Correct | ✅ |
message.intent.fulfillment.end.time.timestamp | ISO 8601 | ✅ Correct | ✅ |
message.intent.category.descriptor.code | Specialty code | ✅ Correct | ✅ |
| ACK response format | { message: { ack: { status: "ACK" } } } | ✅ Correct | ✅ |
Second Search (P2P)
| Field | Spec | Implementation | Status |
|---|---|---|---|
context.provider_id | HSPA subscriber_id | ✅ Correct | ✅ |
context.provider_uri | HSPA base URL | ✅ Correct | ✅ |
context.consumer_id | EUA subscriber_id | ✅ Correct | ✅ |
message.intent.provider.id | Provider ID from first search | ✅ Correct | ✅ |
message.intent.provider.categories | Echoed from first search | ✅ Correct | ✅ |
message.intent.provider.items | Echoed from first search | ✅ Correct | ✅ |
message.intent.provider.fulfillments | Echoed from first search | ✅ Correct | ✅ |
message.intent.fulfillment.agent.id | Doctor HPR ID | ✅ Correct | ✅ |
on_search Response (First & Second)
| Field | Spec | Implementation | Status |
|---|---|---|---|
context.action | on_search | ✅ Correct | ✅ |
context.consumer_id | EUA subscriber_id | ✅ Correct | ✅ |
context.provider_id | HSPA subscriber_id | ✅ Correct | ✅ |
context.provider_uri | HSPA base URL | ✅ Correct | ✅ |
context.transaction_id | Same as request | ✅ Correct | ✅ |
message.catalog.providers[].id | Provider ID | ✅ Correct | ✅ |
message.catalog.providers[].descriptor.name | Facility name | ✅ Correct | ✅ |
message.catalog.providers[].fulfillments[].id | Slot UUID | ✅ Correct | ✅ |
message.catalog.providers[].fulfillments[].type | Physical | ✅ Correct | ✅ |
message.catalog.providers[].fulfillments[].agent | Doctor info | ✅ Correct | ✅ |
message.catalog.providers[].items[].price | Consultation fee | ✅ Correct | ✅ |
message.catalog.providers[].items[].fulfillment_id | Links to slot | ✅ Correct | ✅ |
HSPA Code (Receiving Searches)
Kulcare's HSPA receives searches from ANY EUA including
kulcare.eua,eua-nha,aarogyaSetu.eua, etc.
| Feature | Implementation | Status |
|---|---|---|
| P2P detection | Checks provider_id presence | ✅ Correct |
| P2P routing | Direct to consumer_uri | ✅ Correct |
| Broadcast routing | Via Gateway | ✅ Correct |
source_id in webhook | Uses consumer_id (the EUA that searched) | ✅ Correct |
Real-Time (Ably)
| Feature | Implementation | Status |
|---|---|---|
| Channel convention | private:user:{user_id} | ✅ Correct |
publish_on_search payload | { type, transaction_id, providers } | ✅ Correct |
| Broadcast search routing | user_id from UhiRequest lookup | ✅ Correct |
EUA Code (Receiving from External HSPAs)
Kulcare's EUA endpoints (
/uhi/eua/on_*) receive callbacks from ANY HSPA includingkulcare.hspa,healthSmarttech.hspa,eSushrutLite.hspa,hspa-nha, etc.
| Feature | Implementation | Status |
|---|---|---|
| on_search routing | Logs webhook, publishes to Ably by user_id | ✅ Correct |
| P2P user_id lookup | Via UhiRequest.transaction_id | ✅ Correct |
Other
| Feature | Implementation | Status |
|---|---|---|
transaction_id uniqueness | UUID per search | ✅ Correct |
transaction_id threading | Same across booking lifecycle | ✅ Correct |
| P2P user_id routing | Stored on UhiRequest, looked up via transaction_id | ✅ Correct |
5. Action Items Summary
| # | Severity | Title | Files |
|---|---|---|---|
| 1 | 🔴 | Fix Ably event field total_count → total_providers | useUhiRealtime.ts |
| 2 | 🔴 | Fix source_id to use consumer_id in EUA callbacks | uhi_webhook.rb, all EUA controllers |
| 3 | 🔴 | Add items field to UhiProvider interface | uhi.types.ts |
| 4 | 🟡 | Add date to slot selection URL | UhiSlotSelectionPage.tsx |
| 5 | 🟡 | Change date tabs to dropdown | UhiSlotGrid.tsx |
| 6 | 🟡 | Validate date range from broadcast search | UhiSlotSelectionPage.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