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:
| Role | Endpoint prefix | What 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
| Column | Type | Purpose |
|---|---|---|
id | bigint PK | |
transaction_id | uuid | UHI transaction (shared by search + on_search pair) |
message_id | uuid | Unique per message (same as transaction_id for simplicity) |
action | string | search, on_search, on_init, on_confirm, etc. |
direction | string | outbound (from Kulcare to network) or inbound (from network to Kulcare) |
endpoint | text | Full URL the request was sent to |
payload | jsonb | Full request body (context + message) |
response | jsonb | Response body from the HTTP call |
status_code | int | HTTP status code |
status | string | pending, success, failed |
user_id | uuid | For P2P searches only — the patient's user ID for Ably routing |
uhi.webhooks — Inbound callbacks
| Column | Type | Purpose |
|---|---|---|
id | bigint PK | |
transaction_id | uuid | UHI transaction |
message_id | uuid | Same as transaction_id |
action | string | search, on_search, on_init, etc. |
source | string | gateway (broadcast) or eua_p2p (direct P2P) |
source_id | string | Broadcast: subscriber ID (healthSmarttech.hspa). P2P: numeric provider ID (20). ⚠️ Inconsistent — see §6 |
source_uri | string | HSPA's base URL (e.g. https://qa.healthcarehms.com/abdm/api/uhi) |
payload | jsonb | Full request body |
status | string | pending → processed or failed |
processed_data | jsonb | Metadata 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/searchwith 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
SearchServicewith EUA credentials - Calls
broadcast_search(search_params:, transaction_id:) - Returns
{ success: true, transaction_id }to FE
Step 3-4: BE — SearchService#broadcast_search
- 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→ createsuhi.requestsrow - POSTs to
https://uhigatewaysandbox.abdm.gov.in/api/v1/uhi/search - Gateway forwards to all registered HSPAs
Step 5: HSPA processes search
- Each HSPA receives the search and returns
on_searchvia Gateway - For Kulcare's own HSPA:
Hspa::SearchController#createreceives the search - Calls
CatalogService#send_on_searchwhich builds the catalog response - Sends
on_searchback via Gateway
Step 6: BE — Eua::OnSearchController#create
- File:
backend/domains/uhi/controllers/eua/on_search_controller.rb - Receives
POST /uhi/on_searchfrom Gateway - Logs webhook via
UhiLogger.log_inbound→ createsuhi.webhooksrow - Extracts
subscriber_idfrom context (set by original broadcast search) - Calls
RealtimePublisher.publish_on_searchwith 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_completemessage arrives, transitions to complete state UhiSearchCompleteStaterenders 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/p2pwith:{ "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_urifrom params - Calls
service.direct_search(provider_uri:, search_params:, transaction_id:, user_id: current_user.id) - The
user_idis critical — it's stored on theuhi.requestsrow and used to route the on_search callback
Step 3b: BE — SearchService#direct_search
- 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)→ setsuser_idonuhi.requests - POSTs directly to
{provider_uri}/search(bypasses Gateway)
Step 4-5: HealthSmarttech responds
- HealthSmarttech processes the P2P search
- Sends
on_searchdirectly tohttps://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'sprovider_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
/resultsendpoint:# 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
UhiSlotGridgroups 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 type | context.provider_id | Stored as source_id |
|---|---|---|
| Broadcast (via Gateway) | healthSmarttech.hspa | healthSmarttech.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
UhiSlotGridshows ALL slots (no future-only filtering) to avoid timezone ambiguity
Transaction Lifecycle
- Search initiated:
uhi.requestsrow created (action:search, direction:outbound) - Search received at HSPA:
uhi.webhooksrow created (action:search) - on_search sent:
uhi.requestsrow created (action:on_search, direction:outbound) - on_search received at EUA:
uhi.webhooksrow created (action:on_search) - FE polls: GET
/resultsqueriesuhi.webhooksfor 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" button7. Known Issues & Gotchas
- P2P source_id mismatch: P2P webhooks store numeric provider_id as source_id, not the subscriber ID.
/resultsfallback handles this. - Timezone ambiguity: No timezone on timestamps from external HSPAs. Frontend interprets as UTC. Slot grid shows all slots to avoid filtering issues.
- Duplicate searches: EUA reference app sends 2 requests for each search (broadcast + P2P). Kulcare responds 1:1 to each.
- Ably vs polling: Broadcast results arrive via Ably. P2P results are polled every 5 seconds. Both channels populate the same provider/slot data.
- In-clinic type: HealthSmarttech sends
type: "In-clinic"instead of"Physical". Added to types as valid variant. - Image rendering: External HSPAs send placeholder text as catalog
images.toDataUri()returnsnullfor non-image strings.