04-clinicalwave: W2filled10 citations

Clinical Data — Vital Signs

Audiences: doctor, nurse, clinical-buyer, developer

Clinical Data — Vital Signs

Vital-sign capture maps nurse or doctor measurements directly to FHIR R4 Observation resources, creating a longitudinal, interoperable patient biometric record accessible from any connected system.

Business Purpose

Vital signs (blood pressure, heart rate, temperature, SpO2, weight, BMI, blood glucose, etc.) are the most frequently recorded clinical data point in any clinic. Without structured capture they end up in free-text notes — unsearchable, non-comparable, and useless for trend analysis or AI-assisted diagnostics.

Dudoxx HMS stores every vital as a FHIR Observation resource in HAPI FHIR. This gives clinics:

  • Structured, LOINC-coded measurements that are machine-readable and interoperable.
  • Trend queries: fetch all observations of type 8867-4 (heart rate) for a patient sorted by effectiveDateTime.
  • AI readiness: the TUCAN diagnosis engine and medical cards aggregator query vitals directly from FHIR.
  • A LOINC whitelist guard at the service layer ensures only recognized measurement types are written — preventing silent data loss where the UI renders nothing for unrecognized codes.

Audiences

  • Investor: Structured vital capture is a prerequisite for predictive analytics and population health products — the data foundation for future AI upsells.
  • Clinical buyer (doctor/nurse/receptionist): Nurses record vitals at triage; doctors review trends during the visit. PATIENT role can also read their own vitals via the patient portal.
  • Developer/partner: POST /api/v1/vitals to create; GET /api/v1/vitals/patient/:patientId to list. Full LOINC code required in the code field. Non-whitelisted codes return HTTP 400.
  • Internal (ops/support): Vitals live exclusively in HAPI FHIR — not in Prisma. Troubleshoot via FHIR search: GET {HAPI}/fhir/Observation?patient=Patient/{id}&_sort=-date.

Architecture

VitalsControllerVitalsServiceIFhirClient (FHIR_CLIENT token) → HAPI FHIR port 18080.

The service is intentionally thin: no Prisma involvement, no caching layer. Every vital is a pure FHIR Observation write/read. Patient UUID resolution (Prisma User UUID → FHIR Patient ID) goes through PatientIdResolverService.

ControllerServiceCollaboratorResponsibility
VitalsController (ddx-api/src/clinical-data/vitals/)VitalsServicePatientIdResolverServiceUUID → FHIR Patient ID
IFhirClientwrite/read Observation resources
LOINC_DISPLAY_NAMEScode → human display name map

LOINC Whitelist enforced at service layer (12 codes):

LOINCDisplay
85354-9Blood pressure panel
8480-6Systolic BP
8462-4Diastolic BP
8867-4Heart rate
8310-5Body temperature
9279-1Respiratory rate
2708-6O2 saturation (arterial)
59408-5SpO2 (pulse oximetry)
29463-7Body weight
8302-2Body height
39156-5BMI
2339-0Blood glucose

Any code outside this set is rejected with HTTP 400 before the FHIR write is attempted.

See coding_context/ddx-hms-context.md §Clinical Data for the broader data module map.

Tech Stack & Choices

LayerTechnologyNotes
APINestJS 11, @Controller('vitals')ResourceMeta decorator provides /meta introspection endpoint
Auth@RequireRole(DOCTOR, NURSE, CLINIC_ADMIN, SUPER_ADMIN, PATIENT)PATIENT can read own vitals; only clinical staff can write
FHIR storageHAPI FHIR R4 ObservationNo Prisma model — FHIR is sole SoT
FHIR clientFHIR_CLIENT DI token (FhirClientService)Injected via @Inject(FHIR_CLIENT) per canonical pattern
LOINC codessrc/platform/common/constants/loinc-codes.tsStatic map; display name auto-resolved if not provided in DTO
Patient IDPatientIdResolverServiceTranslates Prisma User UUID to FHIR Patient ID
ResponseVitalResponseDtoFlattens FHIR Observation to clinical-friendly shape

Design choice: Storing vitals exclusively in FHIR (no Prisma mirror) keeps data in one canonical store. The tradeoff is that complex relational queries (e.g., "all patients with SpO2 < 95% in the last 24h") must go through FHIR search — acceptable for current clinic scale, revisited if population-health analytics are added.

Data Flow

Nurse records blood pressure

Business outcome: Nurse enters 120/80 mmHg; the value appears immediately in the patient's vitals timeline and is accessible by the AI diagnosis engine.

Technical mechanism:

  1. Nurse submits POST /api/v1/vitals with { patientId, code: "8480-6", value: "120", unit: "mmHg" }.
  2. VitalsController.create() (vitals.controller.ts:62) extracts clinicId from @ClinicId() (X-Clinic-ID header).
  3. VitalsService.create() validates code against VITAL_LOINC_WHITELIST (vitals.service.ts:23); rejects unknown codes.
  4. PatientIdResolverService.resolveFhirPatientId(dto.patientId) translates UUID to FHIR Patient ID.
  5. Service builds FHIR Observation with code.coding[0] = { system: "http://loinc.org", code: "8480-6", display: "Systolic blood pressure" }, subject.reference = "Patient/{fhirId}", valueQuantity.
  6. fhirClient.create<FhirObservationResource>('Observation', resource, organizationId) posts to HAPI FHIR port 18080.
  7. Returns VitalResponseDto (flat shape: id, type, value, unit, effectiveDateTime).

Doctor views vitals trend

GET /api/v1/vitals/patient/:patientId (vitals.controller.ts:73) lists all Observations for the patient via FHIR search, sorted by effectiveDateTime descending. Returns VitalResponseDto[].

Implicated Code

  • ddx-api/src/clinical-data/vitals/vitals.controller.ts:34@ResourceMeta — declares filterable fields (type, effectiveDateTime) for API introspection
  • ddx-api/src/clinical-data/vitals/vitals.controller.ts:62POST /vitals — requires DOCTOR/NURSE/CLINIC_ADMIN/SUPER_ADMIN
  • ddx-api/src/clinical-data/vitals/vitals.controller.ts:73GET /vitals/patient/:patientId — PATIENT role can read own vitals
  • ddx-api/src/clinical-data/vitals/vitals.service.ts:1VitalsService — FHIR-only service, no Prisma involvement
  • ddx-api/src/clinical-data/vitals/vitals.service.ts:23VITAL_LOINC_WHITELIST — 12-code set enforced before any FHIR write
  • ddx-api/src/clinical-data/vitals/vitals.service.ts:38isLoincCode() — type guard; triggers HTTP 400 on unknown codes
  • ddx-api/src/clinical-data/vitals/vitals.service.ts:59VitalsService class — injects FHIR_CLIENT + PatientIdResolverService

Operational Notes

  • LOINC whitelist is UI-driven: The whitelist in vitals.service.ts mirrors what the front-end vitals summary panel renders. Adding a new vital type requires updating both the whitelist constant and the UI component.
  • No Prisma fallback: If HAPI FHIR is unreachable, vital reads/writes fail immediately — no cached fallback. Verify HAPI FHIR health at GET http://localhost:18080/fhir/metadata before diagnosing missing vitals.
  • FHIR patient must exist: PatientIdResolverService throws NotFoundException if the patient has no FHIR Patient record. Run the seeder or POST /patients/:id/ensure-fhir first.
  • Blood pressure panel vs components: LOINC 85354-9 (panel), 8480-6 (systolic), and 8462-4 (diastolic) are all whitelisted — send panel OR components.
  • Tenant scoping: clinicId from @ClinicId() is passed to all FHIR calls as the partition key. Missing X-Clinic-ID header causes FHIR calls to hit the default partition, potentially returning wrong-patient data in multi-clinic setups.