Clinical Data — Vital Signs
Audiences: doctor, nurse, clinical-buyer, developer
Vital-sign capture maps nurse or doctor measurements directly to FHIR R4
Observationresources, 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 byeffectiveDateTime. - 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/vitalsto create;GET /api/v1/vitals/patient/:patientIdto list. Full LOINC code required in thecodefield. 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
VitalsController → VitalsService → IFhirClient (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.
| Controller | Service | Collaborator | Responsibility |
|---|---|---|---|
VitalsController (ddx-api/src/clinical-data/vitals/) | VitalsService | PatientIdResolverService | UUID → FHIR Patient ID |
| ” | ” | IFhirClient | write/read Observation resources |
| ” | ” | LOINC_DISPLAY_NAMES | code → human display name map |
LOINC Whitelist enforced at service layer (12 codes):
| LOINC | Display |
|---|---|
| 85354-9 | Blood pressure panel |
| 8480-6 | Systolic BP |
| 8462-4 | Diastolic BP |
| 8867-4 | Heart rate |
| 8310-5 | Body temperature |
| 9279-1 | Respiratory rate |
| 2708-6 | O2 saturation (arterial) |
| 59408-5 | SpO2 (pulse oximetry) |
| 29463-7 | Body weight |
| 8302-2 | Body height |
| 39156-5 | BMI |
| 2339-0 | Blood 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
| Layer | Technology | Notes |
|---|---|---|
| API | NestJS 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 storage | HAPI FHIR R4 Observation | No Prisma model — FHIR is sole SoT |
| FHIR client | FHIR_CLIENT DI token (FhirClientService) | Injected via @Inject(FHIR_CLIENT) per canonical pattern |
| LOINC codes | src/platform/common/constants/loinc-codes.ts | Static map; display name auto-resolved if not provided in DTO |
| Patient ID | PatientIdResolverService | Translates Prisma User UUID to FHIR Patient ID |
| Response | VitalResponseDto | Flattens 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:
- Nurse submits
POST /api/v1/vitalswith{ patientId, code: "8480-6", value: "120", unit: "mmHg" }. VitalsController.create()(vitals.controller.ts:62) extractsclinicIdfrom@ClinicId()(X-Clinic-ID header).VitalsService.create()validatescodeagainstVITAL_LOINC_WHITELIST(vitals.service.ts:23); rejects unknown codes.PatientIdResolverService.resolveFhirPatientId(dto.patientId)translates UUID to FHIR Patient ID.- Service builds FHIR
Observationwithcode.coding[0] = { system: "http://loinc.org", code: "8480-6", display: "Systolic blood pressure" },subject.reference = "Patient/{fhirId}",valueQuantity. fhirClient.create<FhirObservationResource>('Observation', resource, organizationId)posts to HAPI FHIR port 18080.- 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 introspectionddx-api/src/clinical-data/vitals/vitals.controller.ts:62—POST /vitals— requires DOCTOR/NURSE/CLINIC_ADMIN/SUPER_ADMINddx-api/src/clinical-data/vitals/vitals.controller.ts:73—GET /vitals/patient/:patientId— PATIENT role can read own vitalsddx-api/src/clinical-data/vitals/vitals.service.ts:1—VitalsService— FHIR-only service, no Prisma involvementddx-api/src/clinical-data/vitals/vitals.service.ts:23—VITAL_LOINC_WHITELIST— 12-code set enforced before any FHIR writeddx-api/src/clinical-data/vitals/vitals.service.ts:38—isLoincCode()— type guard; triggers HTTP 400 on unknown codesddx-api/src/clinical-data/vitals/vitals.service.ts:59—VitalsServiceclass — injectsFHIR_CLIENT+PatientIdResolverService
Operational Notes
- LOINC whitelist is UI-driven: The whitelist in
vitals.service.tsmirrors 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/metadatabefore diagnosing missing vitals. - FHIR patient must exist:
PatientIdResolverServicethrowsNotFoundExceptionif the patient has no FHIR Patient record. Run the seeder orPOST /patients/:id/ensure-fhirfirst. - Blood pressure panel vs components: LOINC
85354-9(panel),8480-6(systolic), and8462-4(diastolic) are all whitelisted — send panel OR components. - Tenant scoping:
clinicIdfrom@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.
Related Topics
- Clinical Visits — Vitals are typically recorded at visit open
- Clinical Data — Conditions — Conditions may be triggered by abnormal vitals
- Medical Cards — Aggregates latest vitals for patient summary display
- Diagnosis Engine — Reads vitals as input to AI diagnostic rules