Lab Data Pipeline
End-to-end lab flow: LabRequest to 8 LAB_* events to orchestrator handlers to Supabase read models to realtime widget.
End-to-end flow from backend lab orders (MongoDB) through the encounter-orchestrator (Deno edge function) into Supabase read models, consumed by the LabResultsWidget via realtime subscriptions.
Data Flow Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ WRITE PATH (Backend — MongoDB) │
│ │
│ Clinician orders lab ──▶ LabRequest Service (NestJS/Moleculer) │
│ OR │ │
│ Roche LIS auto-result ────▶ │ services/medication/.../labRequest/ │
│ │ │
│ ▼ │
│ MongoDB write │
│ + Moleculer event emit │
│ (LAB_ORDER_CREATED, LAB_RESULT_FILED, etc.) │
└─────────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ EVENT BUS │
│ │
│ Moleculer → NATS → webhookDispatcher → POST hospital_events (Supabase) │
│ │
│ Legacy event type Normalized (manifest) type │
│ ───────────────── ────────────────────────── │
│ LAB_ORDER_CREATED → manifest.lab.order_created │
│ LAB_SPECIMEN_COLLECTED → manifest.lab.specimen_collected │
│ LAB_SPECIMEN_RECEIVED → manifest.lab.specimen_received │
│ LAB_SPECIMEN_ANALYZING → manifest.lab.specimen_analyzing │
│ LAB_RESULTED → manifest.lab.resulted │
│ LAB_VERIFIED → manifest.lab.verified │
│ LAB_SPECIMEN_REJECTED → manifest.lab.specimen_rejected │
│ LAB_RESULT_FILED → manifest.lab.result_filed │
└─────────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ENCOUNTER ORCHESTRATOR (Deno Edge Function) │
│ infrastructure/medbase/functions/encounter-orchestrator/index.ts │
│ │
│ Triggered by: INSERT on hospital_events (Postgres trigger) │
│ │
│ Two lab handlers: │
│ │
│ ┌─ handleLabSpecimenPipeline() ────────────────────────────────────────┐ │
│ │ Routes: manifest.lab.order_created .. specimen_rejected │ │
│ │ Writes: │ │
│ │ • encounter_journey_cache.clinical_context.specimen_pipeline │ │
│ │ • department_queues (lab worklist rows) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ handleLabResultFiled() ─────────────────────────────────────────────┐ │
│ │ Routes: manifest.lab.result_filed │ │
│ │ Writes: │ │
│ │ • encounter_journey_cache.clinical_context.latest_labs │ │
│ │ • observations (unified FHIR R4 Observation) │ │
│ │ • acknowledgement_requests (critical value escalation) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ observation trigger → lab_results_ts (TimescaleDB hypertable) │
└─────────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ READ PATH (Supabase → Frontend) │
│ │
│ useLaboratoryResultsSupabase hook │
│ • Query: department_queues WHERE patient_id & dept_type='lab' │
│ • Query: hospital_events WHERE patient_id & event_type IN LAB_* │
│ • Realtime: postgres_changes on both tables │
│ • Projects: LabPanelResult[] (specimen timeline + analytes + flags) │
│ │
│ LabResultsWidget │
│ • Pending / Completed sections │
│ • Specimen stage dots (7 stages) │
│ • Critical value warnings │
│ • Live/offline indicator (Supabase realtime status) │
│ │
│ Widget Rail (macOS-Dock sidebar) │
│ • Lab icon with badge (critical/pending count) │
│ • 320px flyout panel │
│ • "Open full tab" promotes to DynamicContentRenderer slot │
└─────────────────────────────────────────────────────────────────────────────┘
Layer 1 — Backend Lab Services (Write Path)
LabRequest Service
| Property | Value |
|---|---|
| Location | services/medication/src/api/medication/modules/labRequest/ |
| Data store | MongoDB (primary write) |
| Transport | Moleculer via NATS |
| Controller | labRequest.controller.mixin.ts |
The LabRequest service handles the full lifecycle of lab orders:
- Order creation — clinician requests a lab panel → MongoDB document created
- Specimen collection — nurse collects sample → status update + audit log
- Result ingestion — manual entry or Roche LIS auto-import → results stored on order document
- Result filing —
updateLabRequestStatus()orupdateLabReportForLisRoche()emits events
Event Emission
When lab results are filed (status = resulted or completed), the service calls
EncounterJourneyService.emitLabResultFiled():
Payload shape:
{
labRequestId: string,
results: [{
testCode: string, // HGB, WBC, ALT, K, etc.
testName: string,
value: number, // parsed from labResult string
unit: string,
annotation?: string
}],
reportedAt: string // ISO timestamp
}
Both updateLabRequestStatus and updateLabReportForLisRoche follow the same
emit path. Each also creates a LabRequestAuditLog entry with
diffString(oldData, newData).
Specimen Pipeline Events
Specimen lifecycle events are emitted at each transition. The backend emits the
legacy LAB_* event type; the webhookDispatcher mixin inserts a row into
hospital_events (Supabase) which triggers the encounter-orchestrator.
Layer 2 — Event Contract
File: infrastructure/medbase/functions/_shared/event-contract.ts
8 Lab Event Types
| Legacy Event | Normalized (manifest) | Meaning |
|---|---|---|
LAB_ORDER_CREATED |
manifest.lab.order_created |
Clinician places lab order |
LAB_SPECIMEN_COLLECTED |
manifest.lab.specimen_collected |
Nurse draws specimen |
LAB_SPECIMEN_RECEIVED |
manifest.lab.specimen_received |
Lab department receives tube |
LAB_SPECIMEN_ANALYZING |
manifest.lab.specimen_analyzing |
Analyzer processing sample |
LAB_RESULTED |
manifest.lab.resulted |
Raw results available |
LAB_VERIFIED |
manifest.lab.verified |
Medical technologist signs off |
LAB_SPECIMEN_REJECTED |
manifest.lab.specimen_rejected |
Sample rejected (hemolyzed, etc.) |
LAB_RESULT_FILED |
manifest.lab.result_filed |
Final results with analyte values |
The LEGACY_TO_NORMALIZED map in event-contract.ts provides bidirectional
lookup. All events arrive at the orchestrator already normalized.
Event Payload Contract
Every lab event in hospital_events carries:
{
"event_type": "LAB_SPECIMEN_COLLECTED", // legacy type (column)
"patient_id": "...",
"encounter_id": "...",
"payload": {
"ticket_id": "lab-order-xxx", // links to department_queues
"specimen_id": "spec-xxx", // physical tube/container
"panel_name_th": "ตรวจเลือด CBC",
"panel_name_en": "Complete Blood Count",
"specimen_type_th": "เลือด",
"specimen_type_en": "Blood",
"actor": "nurse-001",
"occurred_at": "2026-05-15T08:30:00Z",
// result_filed also includes:
"results": [{ "testCode": "HGB", "value": 12.5, "unit": "g/dL", ... }],
"alertTier": 0 // 0=normal, 3=critical
}
}
Layer 3 — Encounter Orchestrator (Projection)
File: infrastructure/medbase/functions/encounter-orchestrator/index.ts
The orchestrator is a Deno edge function triggered by a Postgres trigger on
hospital_events INSERT. It routes events by normalized type to handler
functions.
handleLabSpecimenPipeline()
Routes: All manifest.lab.* events except result_filed
Stage mapping:
| Event | Stage | Queue Status |
|---|---|---|
manifest.lab.order_created |
ordered |
WAITING (creates row) |
manifest.lab.specimen_collected |
collected |
IN_PROGRESS |
manifest.lab.specimen_received |
received |
IN_PROGRESS |
manifest.lab.specimen_analyzing |
in-analysis |
IN_PROGRESS |
manifest.lab.resulted |
resulted |
IN_PROGRESS |
manifest.lab.verified |
verified |
COMPLETED |
manifest.lab.specimen_rejected |
rejected |
CANCELLED |
Writes to encounter_journey_cache:
// clinical_context.specimen_pipeline[specimenId]
{
"currentStage": "received",
"orderedAt": "2026-05-15T07:00:00Z",
"orderedBy": "dr-smith",
"collectedAt": "2026-05-15T07:15:00Z",
"collectedBy": "nurse-jane",
"receivedAt": "2026-05-15T07:30:00Z",
"receivedBy": "mt-tech-01",
// rejected specimens also get:
"rejectionReason": "Hemolyzed sample"
}
Writes to department_queues:
- On
ordered: INSERT new row withdept_type='LABORATORY',status='WAITING',ticket_idfrom payload - On subsequent stages: UPDATE existing row’s
status+ patchmetadataJSONB withspecimen_[stage]_attimestamps
handleLabResultFiled()
Routes: manifest.lab.result_filed
Writes to encounter_journey_cache:
// clinical_context.latest_labs[testCode]
{
"HGB": { "value": 12.5, "unit": "g/dL", "refLow": 12.0, "refHigh": 17.5, "critical": false },
"K": { "value": 6.7, "unit": "mmol/L", "refLow": 3.5, "refHigh": 5.0, "critical": true }
}
Writes to observations (via projectLabResultFinalized):
Each analyte becomes a row in the unified observations table with
source_kind='lab', category='biomarker', LOINC code mapping, and
interpretation flag.
Writes to acknowledgement_requests (critical values only):
When alertTier === 3, creates an acknowledgement request with:
- 30-minute deadline
- 2-tier escalation: 15 min →
charge_nurse, 30 min →attending_physician - Links back to the lab result for one-click review
Observation → lab_results_ts Trigger
A Postgres trigger on observations INSERT (where category = 'biomarker')
copies the row into lab_results_ts, a TimescaleDB hypertable optimized for
time-series queries (trend charts, analytics dashboards).
Retention policy: 2yr full, 5yr hourly aggregates, 10yr daily aggregates. Compression starts after 30 days.
Layer 4 — Supabase Read Model Tables
department_queues (Lab Worklist)
One row per lab order. Serves both the lab department worklist and per-patient widget views.
-- Key columns for lab rows
id UUID PRIMARY KEY
tenant_id TEXT
encounter_id TEXT
patient_id TEXT
dept_type TEXT -- 'LABORATORY'
ticket_id TEXT -- lab order reference
status TEXT -- WAITING | IN_PROGRESS | COMPLETED | CANCELLED
priority INTEGER
metadata JSONB -- specimen stage, panel name, analytes, timestamps
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
metadata JSONB for lab rows:
{
"panel_name_th": "CBC",
"panel_name_en": "Complete Blood Count",
"specimen_type_th": "เลือด",
"specimen_type_en": "Blood",
"specimen_ordered_at": "...",
"specimen_collected_at": "...",
"specimen_received_at": "...",
"specimen_analyzing_at": "...",
"specimen_resulted_at": "...",
"specimen_verified_at": "...",
"analytes": [
{ "code": "HGB", "value": 12.5, "unit": "g/dL", "flag": "normal" },
{ "code": "K", "value": 6.7, "unit": "mmol/L", "flag": "critical-high" }
],
"has_critical": true
}
// NOTE: critical_acknowledged is NOT stored here — it is derived at
// query time from the acknowledgement_requests bounded context.
// See "Acknowledgement as a Separate Bounded Context" below.
hospital_events (Timeline)
Append-only event log. Each lab lifecycle transition creates a row.
id UUID PRIMARY KEY
event_type TEXT -- LAB_ORDER_CREATED, LAB_SPECIMEN_COLLECTED, ...
patient_id TEXT
encounter_id TEXT
payload JSONB -- ticket_id, specimen_id, actor, results, etc.
occurred_at TIMESTAMPTZ
created_at TIMESTAMPTZ
observations (Unified FHIR R4)
One row per analyte result. Used for cross-encounter trending and FHIR export.
id UUID PRIMARY KEY
patient_id TEXT
encounter_id TEXT
category TEXT -- 'biomarker' for lab results
code_system TEXT -- 'http://loinc.org'
code TEXT -- LOINC code
code_display TEXT -- human-readable name
value_numeric DOUBLE PRECISION
unit TEXT
reference_low DOUBLE PRECISION
reference_high DOUBLE PRECISION
interpretation TEXT -- normal, low, high, critical-low, critical-high
effective_at TIMESTAMPTZ
source_kind TEXT -- 'lab'
source_id TEXT -- lab order reference
status TEXT -- final, amended, corrected
metadata JSONB
lab_results_ts (TimescaleDB Hypertable)
Time-series copy of lab observations for analytics and trend charts.
Auto-populated by Postgres trigger on observations INSERT where
category = 'biomarker'.
time TIMESTAMPTZ -- effective_at
patient_id TEXT
encounter_id TEXT
order_id TEXT
test_code TEXT
test_name TEXT
value_numeric DOUBLE PRECISION
value_text TEXT
unit TEXT
reference_low DOUBLE PRECISION
reference_high DOUBLE PRECISION
encounter_journey_cache (Patient Manifest)
JSONB document per encounter. Lab data lives in clinical_context:
{
"clinical_context": {
"specimen_pipeline": {
"spec-001": {
"currentStage": "verified",
"orderedAt": "...", "orderedBy": "...",
"collectedAt": "...", "collectedBy": "...",
"receivedAt": "...", "receivedBy": "...",
"analyzingAt": "...",
"resultedAt": "...",
"verifiedAt": "...", "verifiedBy": "..."
}
},
"latest_labs": {
"HGB": { "value": 12.5, "unit": "g/dL", "refLow": 12.0, "refHigh": 17.5, "critical": false },
"WBC": { "value": 8.2, "unit": "x10^3/uL", "refLow": 4.0, "refHigh": 11.0, "critical": false },
"K": { "value": 6.7, "unit": "mmol/L", "refLow": 3.5, "refHigh": 5.0, "critical": true }
}
}
}
Layer 5 — Frontend Consumption
useLaboratoryResultsSupabase Hook
File: web/packages/miniapps/laboratory-results/useLaboratoryResultsSupabase.ts
Two modes:
| Mode | When | Scoping |
|---|---|---|
| Encounter-scoped | encounterId provided |
Only labs for this admission/visit |
| Patient-wide | No encounterId |
Recent labs across encounters, bounded by withinDays (default 90) |
Encounter-scoped mode is designed for long-stay IPD patients (months-long admissions) — it prevents pulling thousands of historical rows. Patient-wide mode suits OPD workflows and the Widget Rail badge.
Options:
interface UseLaboratoryResultsOptions {
encounterId?: string | null; // scope to single encounter
withinDays?: number; // default 90 for patient-wide mode
limit?: number; // default 200 queue rows
}
Two initial queries:
department_queues— filtered bypatient_id+dept_type='lab'- optional
encounter_idorcreated_at >= cutoff, capped bylimit
- optional
hospital_events— filtered bypatient_id+ 7 LAB_* event types- same encounter/time scoping, capped at
limit * 10
- same encounter/time scoping, capped at
Realtime subscriptions (debounced incremental):
Channel: lab-results-{patientId}[-{encounterId}]
postgres_changes on department_queues
filter: patient_id=eq.{patientId}
event: * (INSERT, UPDATE, DELETE)
→ client-side filter: dept_type='lab', optional encounter match
→ debounced reload (300ms coalesce window)
postgres_changes on hospital_events
filter: patient_id=eq.{patientId}
event: INSERT
→ client-side filter: LAB_* event types, optional encounter match
→ debounced reload (300ms coalesce window)
Debouncing coalesces burst updates (e.g. a batch of specimen-stage events from LIS auto-import) into a single re-fetch instead of N sequential reloads.
Projection logic:
- Each
department_queuesrow becomes aLabPanelResult - Panel names, specimen type, analytes extracted from
metadataJSONB hospital_eventsmatched to queue rows viapayload.ticket_idorpayload.orderId- Events sorted by
occurred_at→ buildSpecimenTracking.timeline[] - Latest event determines
currentStage - Critical flags derived from analyte flags in metadata
Returns:
{
panels: LabPanelResult[] | null, // null = not yet loaded
loading: boolean,
error: Error | null,
realtimeConnected: boolean
}
LabResultsWidget
File: web/packages/miniapps/laboratory-results/LabResultsWidget.tsx
Compact widget rendered inside the Widget Rail flyout (320px wide):
- Summary chips — pending count (orange), completed count (green), critical count (red)
- Live indicator — CloudDone/CloudOff icon showing Supabase realtime status
- Panel list — “In Progress” and “Completed” sections
- Each row: colored stage dot, Thai+English name, order number, stage chip
- Warning icon for unacknowledged critical values
- Footer — “Open full lab results” button to promote to DynamicContentRenderer tab
- States — loading spinner, Supabase error display, empty state message
Props:
interface LabResultsWidgetProps {
encounterId: string;
patientId: string;
locale?: string;
onClose: () => void;
onOpenTab?: () => void;
panels?: LabPanelResult[]; // pre-fetched bypass
disableLive?: boolean; // force sample data
}
Type System
File: web/packages/miniapps/laboratory-results/types.ts
| Type | Purpose |
|---|---|
SpecimenStage |
7 stages: ordered → collected → received → in-analysis → resulted → verified / rejected |
LabFlag |
Result interpretation: normal, low, high, critical-low, critical-high, abnormal |
LabPanelCategory |
13 categories: hematology, chemistry, liver, renal, coagulation, etc. |
LabValueKind |
Value representation: numeric, positive-negative, text, titer |
LabAnalyte |
Individual test within a panel (code, value, unit, reference, flag, trend) |
LabPanelResult |
Complete panel: analytes + specimen tracking + status + bilingual names |
SpecimenTracking |
Physical tube: currentStage + timeline entries + container ID + ETA |
LabReferenceRange |
Numeric bounds + critical bounds + qualitative text |
LabTrendPoint |
Historical value for sparkline charts |
Demo Seed Data
File: infrastructure/medbase/migrations/081_laboratory_demo_seed.sql
Seeds 8 lab orders for demo patient pt-demo-lab-001 (HN 00012345):
| Order | Panel | Stage | Notable |
|---|---|---|---|
| 1 | CBC | verified | Normal results |
| 2 | Electrolytes | verified | K=6.7 critical-high |
| 3 | Liver Function | resulted | Pending verification |
| 4 | Renal Function | in-analysis | Analyzer processing |
| 5 | Coagulation | received | Lab received tube |
| 6 | Lipid Panel | collected | Nurse drew specimen |
| 7 | Thyroid Function | ordered | Just placed |
| 8 | Urinalysis | rejected | Contaminated sample |
All orders include full bilingual metadata, analyte values where applicable,
and corresponding hospital_events timeline entries.
Critical Value Escalation Flow
Lab result with alertTier=3
│
▼
handleLabResultFiled()
│
├─▶ encounter_journey_cache.latest_labs[code].critical = true
│
├─▶ observations row with interpretation = 'critical-high' or 'critical-low'
│
└─▶ acknowledgement_requests INSERT
├── deadline: now() + 30 min
├── escalation[0]: 15 min → charge_nurse
└── escalation[1]: 30 min → attending_physician
│
▼
AcknowledgementInbox FAB (global, mounted in App.tsx)
+ push notification via messaging service
The LabResultsWidget shows a red WarningAmberIcon on any panel row where
hasCritical && !criticalAcknowledged. The full lab miniapp (promoted via
“Open full tab”) provides the acknowledge action.
Acknowledgement as a Separate Bounded Context (DDD)
The acknowledgement_requests table is a shared kernel — owned by the ack
domain, consumed by every feature that needs “has this been seen?” semantics.
The lab pipeline does not denormalize ack state into department_queues
metadata. Instead, the widget queries the ack domain directly:
useLaboratoryResultsSupabase.load()
│
├─▶ department_queues (lab panels, specimens, statuses)
├─▶ hospital_events (specimen timeline events)
└─▶ acknowledgement_requests
WHERE subject_order_type = 'lab'
AND patient_ref = $patientId
AND status = 'acknowledged'
AND active = true
→ Set<subject_order_id> (ackedOrderIds)
toPanel(queue, events, ackedOrderIds)
→ criticalAcknowledged = ackedOrderIds.has(ticket_id) || ackedOrderIds.has(encounter_id)
A third realtime subscription on acknowledgement_requests (filtered by
patient_ref and subject_order_type='lab') triggers a debounced reload so
the badge clears within ~300ms of an ack response — no trigger, no
denormalization, single source of truth.
Sequence Diagram — Happy Path
Clinician LabRequest Service MongoDB Supabase Orchestrator Frontend
│ │ │ │ │ │
│─── order lab ──────▶│ │ │ │ │
│ │── write ────────▶│ │ │ │
│ │── emit LAB_ORDER_CREATED ──▶│ hospital_events │ │
│ │ │ │── trigger ──────────▶│ │
│ │ │ │ │── INSERT dept_queues │
│ │ │ │ │── PATCH ejc specimen │
│ │ │ │◀─────────────────────│ │
│ │ │ │── realtime ─────────────────────────────────▶│
│ │ │ │ │ │── re-render
│ │ │ │ │ │
┊ (nurse collects, lab receives, analyzer runs — same pattern per stage) ┊ │
│ │ │ │ │ │
│ │── results ──────▶│ │ │ │
│ │── emit LAB_RESULT_FILED ───▶│ hospital_events │ │
│ │ │ │── trigger ──────────▶│ │
│ │ │ │ │── PATCH ejc labs │
│ │ │ │ │── INSERT observations│
│ │ │ │ │── (if critical) ack │
│ │ │ │◀─────────────────────│ │
│ │ │ │── realtime ─────────────────────────────────▶│
│ │ │ │ │ │── badge + alert
Backend Event Emitters
The labRequest.controller.mixin.ts emits specimen-stage events at each
lifecycle transition via EncounterJourneyService.emitLabSpecimenEvent():
| LabRequestStatus | Emitted Event | Orchestrator Handler |
|---|---|---|
(created via createCache) |
LAB_ORDER_CREATED |
handleLabSpecimenPipeline |
collected |
LAB_SPECIMEN_COLLECTED |
handleLabSpecimenPipeline |
sent-specimens |
LAB_SPECIMEN_COLLECTED |
handleLabSpecimenPipeline |
accepted-specimens |
LAB_SPECIMEN_RECEIVED |
handleLabSpecimenPipeline |
resulted |
LAB_RESULTED + LAB_RESULT_FILED |
both handlers |
completed |
LAB_VERIFIED + LAB_RESULT_FILED |
both handlers |
(via rejectLabRequestStatus) |
LAB_SPECIMEN_REJECTED |
handleLabSpecimenPipeline |
All events are inserted into hospital_events (Supabase), which triggers the
encounter-orchestrator via pg_net.http_post (migration 002_orchestrator_trigger_hardening).
File Inventory
| File | Layer | Purpose |
|---|---|---|
services/medication/.../labRequest/labRequest.controller.mixin.ts |
Backend | LabRequest CRUD, result filing, specimen-stage + LAB_RESULT_FILED emission |
services/medication/.../encounterJourney/encounterJourney.service.ts |
Backend | emitLabSpecimenEvent() + emitLabResultFiled() — inserts into hospital_events |
infrastructure/medbase/functions/_shared/event-contract.ts |
Event bus | Legacy ↔ normalized event type mappings |
infrastructure/medbase/functions/encounter-orchestrator/index.ts |
Orchestrator | handleLabSpecimenPipeline, handleLabResultFiled |
infrastructure/medbase/migrations/20260512_observation_unification.sql |
Schema | observations table + lab→hypertable trigger |
infrastructure/medbase/migrations/021_timescale_vitals.sql |
Schema | lab_results_ts TimescaleDB hypertable |
infrastructure/medbase/migrations/081_laboratory_demo_seed.sql |
Seed | 8 demo lab orders with full analytes |
web/packages/miniapps/laboratory-results/types.ts |
Frontend | LabPanelResult, SpecimenStage, LabAnalyte, etc. |
web/packages/miniapps/laboratory-results/useLaboratoryResultsSupabase.ts |
Frontend | Supabase queries + realtime subscription hook |
web/packages/miniapps/laboratory-results/LabResultsWidget.tsx |
Frontend | Compact widget for Widget Rail flyout |
web/packages/miniapps/laboratory-results/index.ts |
Frontend | Barrel + widgetSurface export |
web/packages/ui-kit/src/components/widget-rail/WidgetRail.tsx |
Frontend | Dock + flyout chrome (hosts the widget) |
web/sandbox/targets/WidgetRailTarget.tsx |
Dev | Sandbox demo with mock patient profile |
Extending the Pipeline
Adding a new analyte/panel type
No code changes needed. The pipeline is panel-agnostic — any lab order that emits the standard 8 event types will flow through automatically. The panel name, specimen type, and analyte codes are carried in event payloads, not hardcoded.
Adding trend charts
Query lab_results_ts for historical values:
SELECT time, value_numeric, unit
FROM lab_results_ts
WHERE patient_id = $1 AND test_code = $2
ORDER BY time DESC
LIMIT 20;
The LabAnalyte.trend field already supports LabTrendPoint[] — populate it
from the hypertable query and the widget will render sparklines.
Adding new result sources (HL7v2 ORU, FHIR write)
Any source that can emit LAB_RESULT_FILED / LAB_SPECIMEN_* events into
hospital_events will be picked up by the orchestrator. Current sources:
- Manual entry via LabRequest service
- Roche LIS auto-import (
updateLabReportForLisRoche) - HL7v2 ORU mapper (
services/interoperability/.../oru-to-medos.mapper.ts) - FHIR write API (
services/public-api/.../fhir-write.controller.ts)
Connecting to a new LIS
See docs/architecture/ris-adapter-design.md for the bidirectional adapter
pattern. Lab instruments follow the same ORM/ORU flow — the adapter translates
LIS-specific HL7v2 segments into the standard event payloads.