FHIR Vitals Pipeline
Streaming ingestion for HL7 FHIR R4 Observation vitals, landing ward telemetry in TimescaleDB for monitoring.
At a glance
- HL7 FHIR R4 Observation parsing and validation
- Kafka-compatible streaming via Redpanda
- Time-partitioned storage in TimescaleDB
- FastAPI read endpoints for ward-level views
- Back-pressure handling for bursty vitals traffic
From the repository
README
Bedside → Broker → Brain → Board.
A production-grade event-streaming platform for hospital wards: ingests HL7 FHIR R4 Observation telemetry, detects sustained clinical anomalies with a sliding-window rules engine, persists time-series vitals in a TimescaleDB hypertable, and fans out CRITICAL alerts to clinician dashboards over WebSocket — with zero-loss Kafka delivery guarantees.
📌 Why this exists
Ward monitors emit a firehose of vital-sign readings. Clinicians don't need data — they need the three numbers that mean "come to bed 7, now." This pipeline turns raw device chatter into exactly that:
- 🩺 FHIR R4 ingestion — every payload is validated as a real
Observationresource (LOINC-coded, clinically plausible ranges). Garbage never enters the stream: rejected with a FHIROperationOutcome422. - ⚡ Zero-loss streaming — Kafka
acks=all, partition-by-patient_idordering, 3-attempt exponential retry, plus an in-memory local broker for offline dev (USE_LOCAL_BROKER=true). - 🧠 Sustained-anomaly detection — a 120 s sliding window per patient/metric fires rules only on contiguous breaches (Tachycardia, Bradycardia, Critical Desaturation), with 60 s anti-flapping cooldowns that are bypassed when severity escalates. Alert fatigue is a bug, not a feature.
- 📡 Push, not poll — a dedicated consumer group (
healthcare-ws-alert-dispatcher) pumps alerts into a WebSocket hub; dashboards subscribe per ward, per patient, or hospital-wide. - 🗄️ Audit-first persistence — vitals land in a time-chunked hypertable via a 500 ms / 1 000-row batch writer; every alert is an auditable row with an acknowledgment workflow.
🌐 System Architecture

🔍 Interactive version: open
docs/architecture.htmlin any browser — pan/zoom, dark/light themes, PATH & LENS focus mode. Source JSON (validated, showcase profile):docs/archify/architecture.candidate.json.
Component breakdown
| Component | Responsibility | Source |
|---|---|---|
| Ingestion Gateway | FastAPI app on :8000. Routes, FHIR validation, Kafka producing, health checks. | cmd/ingestion/main.py |
| Clinical Validator | Pydantic model for FHIR R4 Observation; enforces LOINC codes + clinically plausible ranges, emits OperationOutcome on failure. | internal/domain/fhir.py |
| Redpanda (Kafka) | Durable event backbone. Topics: patient.vitals.raw, patient.alerts.critical. | docker-compose.yml, internal/stream/producer.py |
| Stream Processor | Consumes raw vitals, feeds the window buffer, evaluates rules, writes batches + alert audits, produces alerts. | cmd/processor/main.py |
| Sliding Window | In-memory contiguous-breach tracker, 120 s per patient/metric. | internal/engine/sliding_window.py |
| Rules Engine | Tachycardia / Bradycardia / Desaturation detection + anti-flapping suppression. | internal/engine/rules.py |
| Batch Writer | Flushing vitals to TimescaleDB every 500 ms or 1 000 rows, final flush on shutdown. | internal/repository/batch_writer.py |
| TimescaleDB | vitals_observations hypertable (1-day chunks), clinical_alerts audit log, patients registry. | migrations/001_init_healthcare_schema.sql |
| Alert Dispatcher | Kafka→WebSocket fan-out hub embedded in the gateway process; ward/patient/global subscriptions. | internal/transport/websocket/hub.py |
| Ward Dashboards | Nurse-station clients; receive pushes, acknowledge via REST. | internal/transport/websocket/routes.py |
Trust & failure boundaries
- Edge → Gateway is untrusted input. Only fully validated FHIR resources are ever produced to Kafka —
invalid payloads terminate as
422 OperationOutcomeand never touch the stream. - Gateway → Broker is the durability seam.
acks=all+ retry means a202 ACCEPTEDimplies the record is committed in Kafka, not just accepted into process memory. - Broker + DB are one private network (Docker Compose). Application services hold the only connections; nothing else is exposed except the gateway's HTTP/WS port.
- Processor is stateless-able: Kafka consumer-group offsets are the replay mechanism; the in-memory window is rebuilt from the stream on restart.
🔄 Core Workflow: one observation's journey

🔍 Interactive version:
docs/sequence.html· source:docs/archify/sequence.candidate.json
- Monitor → Gateway —
POST /api/v1/fhir/Observation(single, alias route, or/batcharray). - Validate — Pydantic checks
resourceType, LOINC code against the six monitored vitals, and clinically plausible ranges (e.g. HR ∈ [0, 350] bpm). Failure ⇒ immediate422 OperationOutcome. - Produce — the converted internal record is sent to
patient.vitals.rawwithacks=all, keyed bypatient_id(per-patient ordering), 3-attempt exponential backoff before surfacing an error. - Acknowledge to device —
202 ACCEPTED { ingested_count, patient_id }. - Consume & store — the Stream Processor pulls batches (≤ 100), and the batch writer flushes inserts to the hypertable every 500 ms or 1 000 rows.
- Evaluate — each record updates its patient's 120 s sliding window; a rule fires only when a threshold breach is contiguous and sustained (see table below).
- Alert — on trigger: audit row →
clinical_alerts, and a message onpatient.alerts.critical, subject to a 60 s anti-flapping cooldown per (patient, rule) — escalated severity breaks through the cooldown. - Push — the dispatcher consumer group receives the alert and broadcasts it to all subscribed ward/patient/ global WebSocket clients.
- Close the loop — a nurse acknowledges:
POST /api/v1/alerts/{alert_id}/ackupdates the audit trail; unacknowledged alerts remain queryable viaGET /api/v1/alerts/unacknowledgedso nothing is lost across reconnects.
Clinical rules
| Rule | Metric (LOINC) | Condition | Severity |
|---|---|---|---|
| Severe Tachycardia | Heart rate 8867-4 | > 130 bpm sustained ≥ 30 s | CRITICAL |
| Severe Bradycardia | Heart rate 8867-4 | < 40 bpm sustained ≥ 15 s | CRITICAL |
| Critical Desaturation | SpO₂ 2708-6 | < 88 % sustained ≥ 20 s | CRITICAL |
Supported vitals: heart rate, SpO₂, systolic/diastolic BP, body temperature, respiratory rate
(internal/domain/fhir.py — LOINCCode, LOINC_RANGES).
🧰 Tech Stack
| Layer | Technology | Role |
|---|---|---|
| Language / Runtime | Python ≥ 3.11, asyncio | All services, single venv |
| HTTP + WS Framework | FastAPI 0.115 · Uvicorn | Ingestion API, WebSocket hub, OpenAPI docs |
| Domain / Validation | Pydantic v2 | FHIR R4 models, clinical-range validators |
| Streaming | Redpanda (Kafka wire API) · aiokafka | Decoupling, replay, alert transport |
| Database | TimescaleDB (PostgreSQL 16) · asyncpg | Hypertable time-series + alert audit log |
| Infra | Docker Compose | One-command broker + database |
| Testing | pytest · pytest-asyncio | Unit + stream-integration + WebSocket tests |
| Tooling | Makefile · migrate script · ICU patient simulator | Dev loop automation |
📁 Project Structure
.
├── cmd/ # Service entrypoints (process boundaries)
│ ├── ingestion/ # FastAPI gateway + WebSocket dispatcher
│ └── processor/ # Stream processor (rules engine loop)
├── internal/ # Shared application core
│ ├── domain/ # FHIR R4, Observation, ClinicalAlert models
│ ├── engine/ # Sliding window + clinical rule evaluation
│ ├── repository/ # asyncpg pool, batch writer, alert repository
│ ├── stream/ # Resilient Kafka producer/consumer + local broker
│ └── transport/ # REST routes + WebSocket hub/routes
├── migrations/ # Versioned SQL (hypertable, indexes, audit log)
├── simulator/ # ICU patient-bed telemetry generator
├── scripts/ # Migration runner
├── tests/ # pytest suite (5 files, asyncio mode)
├── docs/ # Diagrams & documentation
│ ├── architecture.html # Interactive Archify architecture
│ ├── sequence.html # Interactive Archify sequence
│ ├── archify/ # Validated diagram source JSON (regenerable)
│ └── assets/ # Static PNG exports for this README
├── docker-compose.yml # Redpanda + TimescaleDB
├── Makefile # up / down / migrate / test / simulate / clean
└── pyproject.toml # Project metadata + pytest config
🚀 Quick Start
Prerequisites: Python ≥ 3.11 · Docker Engine · (optional) make
1. Clone & install
git clone https://github.com/<your-org>/healthcare-fhir-vitals-pipeline.git
cd healthcare-fhir-vitals-pipeline
python -m venv .venv
.venv\Scripts\Activate.ps1 # Windows PowerShell
# source .venv/bin/activate # macOS / Linux
pip install -r requirements.txt
2. Start infrastructure & schema
docker compose up -d # Redpanda :9092 + TimescaleDB :5432
python scripts/migrate.py # hypertable + audit log + indexes
Environment overrides (all have sane defaults matching docker-compose.yml; export them in your shell):
# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/healthcare_vitals
# KAFKA_BOOTSTRAP_SERVERS=localhost:9092
# PORT=8000
# KAFKA_MOCK_MODE=false # true → run without Kafka/DB (dev, tests)
# USE_LOCAL_BROKER=false # true → in-process broker shim
3. Run the services (two terminals)
# Terminal 1 — ingestion API + WebSocket dispatcher
python -m cmd.ingestion.main
# Terminal 2 — stream processor + clinical rules engine
python -m cmd.processor.main
4. Verify & generate load
curl http://localhost:8000/healthz
# {"status":"healthy","service":"telemetry-and-alert-dispatcher","active_ws_clients":0}
python simulator/patient_simulator.py # streams ICU beds, induces anomalies
Watch alerts land live:
wscat -c ws://localhost:8000/ws/v1/alerts # hospital-wide
wscat -c ws://localhost:8000/ws/v1/wards/ICU-1/alerts
Or ingest a single observation manually:
curl -X POST http://localhost:8000/api/v1/fhir/Observation \
-H "Content-Type: application/json" -d '{
"resourceType": "Observation",
"status": "final",
"code": { "coding": [ { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" } ] },
"subject": { "reference": "Patient/example-001" },
"effectiveDateTime": "2026-01-01T12:00:00Z",
"valueQuantity": { "value": 142, "unit": "bpm" }
}'
💡 Makefile shortcuts (paths assume the Windows
.venv/Scriptslayout):make up·make down·make migrate·make test·make simulate·make clean
⚡ Zero-infra demo (no Docker, no Kafka, no database)
The whole pipeline also runs in a single process using an in-memory broker shim and mock repositories — ideal for a first look or a recruiter sanity check:
python simulate.py --beds 20 --rate 1.0 --duration 60
This spins up the gateway, stream processor, 20 simulated ICU beds (beds 3/7/12 induce anomalies) and a live WebSocket alert console — end to end.
📡 API & Event Reference
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/fhir/Observation · /api/v1/telemetry/observations | Ingest one FHIR R4 Observation → 202 / 422 OperationOutcome |
POST | /api/v1/telemetry/observations/batch | Ingest an array of Observations |
GET | /api/v1/alerts/unacknowledged?patient_id= | Active unacknowledged alerts |
POST | /api/v1/alerts/{alert_id}/ack | Acknowledge alert ({ "acknowledged_by": "..." }) |
GET | /healthz | Liveness + active WebSocket client count |
WS | /ws/v1/wards/{ward_id}/alerts | Subscribe to a ward's alert stream |
WS | /ws/v1/patients/{patient_id}/alerts | Subscribe to one patient/bed |
WS | /ws/v1/alerts | Subscribe to all alerts, hospital-wide |
| Kafka topic | Producer | Consumer | Guarantee |
|---|---|---|---|
patient.vitals.raw | Ingestion Gateway | Stream Processor | acks=all, keyed by patient_id |
patient.alerts.critical | Stream Processor | Alert Dispatcher | consumer group healthcare-ws-alert-dispatcher |
🧪 Testing
pytest -v tests/ # or: make test
Covers: FHIR validation (test_fhir_validation.py), ingestion API (test_ingestion_api.py),
sliding-window engine (test_sliding_window.py), alert dispatch + WebSocket delivery
(test_alerts_and_websocket.py), and an end-to-end stream integration test (test_stream_integration.py)
running on asyncio mode via pyproject.toml.
🗺️ Roadmap
- Kubernetes manifests + HA broker/DB topology
- Idempotent upserts + replay-position watermarking
- Grafana dashboard over the hypertable (continuous aggregates)
- Configurable rule packs per unit (NICU vs. general ward)
- OAuth2 / SMART-on-FHIR authorization for ward subscriptions
🤝 Contributing
Fork → branch (feat/…, fix/…) → keep pytest -v tests/ green → open a PR describing the clinical or
streaming impact. Diagram sources live in docs/archify/*.json — regenerate interactive HTML with
Archify: archify finalize architecture docs/archify/architecture.candidate.json docs/architecture.html.
📄 License
Not yet declared — add a LICENSE file before public release.
Built for the moment a monitor says what a nurse must hear first. ⭐ Star it if you'd trust it on your ward.