HAPI FHIR R4 Server — Clinical Data Store
Audiences: developer, clinical-buyer, internal
HAPI FHIR 8.10.0-1 is the authoritative clinical data store for Dudoxx HMS — every Patient, Encounter, Observation, DiagnosticReport, and DocumentReference is persisted in HAPI, partitioned by clinic, and accessed through a typed NestJS client that routes all requests through the correct tenant partition.
Business Purpose
Clinical data is legally regulated healthcare information. In Germany (and across EU healthcare systems), clinical records must be stored in a format that supports interoperability (HL7 FHIR R4), can be shared with insurers and referring practitioners, and is audit-safe. HAPI FHIR 8.10.0-1 satisfies all three requirements out of the box.
Dudoxx HMS uses HAPI as the canonical store for all structured clinical data — not as a proxy or export target, but as the live system of record. Prisma (ddx_api_main) stores operational metadata (scheduling, user accounts, waiting room state), but clinical content (diagnoses, lab results, notes, prescriptions) lives in HAPI.
This design means:
- Any FHIR-capable external system (insurance gateway, lab, specialist network) can read Dudoxx patient data without transformation.
- Clinical data is never locked in a proprietary schema — the FHIR resource structure is the public API.
- Partition isolation (one partition per clinic) gives strong multi-tenancy guarantees required by DSGVO.
Audiences
- Investor: FHIR compliance is a hard requirement for integrations with German public health systems (KBV, GKV, ePA). HAPI R4 certification removes a major regulatory blocker for market entry and differentiates Dudoxx from non-interoperable EMR alternatives.
- Clinical buyer (doctor/nurse/receptionist): Clinical staff never interact with HAPI directly. The HAPI server powers the patient chart, lab results, clinical notes, and visit history that appear in the doctor portal — with sub-second response times for individual reads and under 500ms for FHIR search queries.
- Developer/partner: All clinical reads and writes go through the
IFhirClientinterface (tokenFHIR_CLIENT) injected into NestJS services. Direct HTTP calls to the HAPI base URL are not permitted from application code — they bypass partition routing. UsefhirClient.read(),fhirClient.search(),fhirClient.create(),fhirClient.update(),fhirClient.delete()instead. - Internal (ops/support): On dev (dudoxxmac) HAPI runs as a native
mvn/Spring Boot JVM (overlay+vendor build, NOT Docker); on acc-dev it is a native host Java process. It uses a dedicatedddx_fhir_corePostgreSQL database. Tenant onboarding registers a new tenant viaTenantAdminController(POST /admin/tenants/register), andTenantRegistryServicereconciles slug→partition from theorganizationstable on a 60 s cache refresh.
Architecture
ddx-api (NestJS 11) — FhirInfrastructureModule (fhir.module.ts) owns two services:
| Service (file) | Method | Notes |
|---|---|---|
FhirClientService (fhir-client.service.ts) | read<R>(type, id, slug) | — |
FhirClientService | search<R>(type, params, slug) | — |
FhirClientService | create<R>(type, body, slug) | — |
FhirClientService | update<R>(type, id, body, slug) | — |
FhirClientService | delete(type, id, slug) | — |
FhirClientService | transaction(bundle, slug) | — |
FhirClientService | executeBatch(bundle, slug) | — |
FhirClientService | everything(patientId, slug) | $everything operation |
FhirClientService | patch(type, id, ops, slug) | JSON Patch |
FhirPartitionService (fhir-partition.service.ts) | resolveClinicSlug(clinicId) → slug | UUID → slug, TTL cache |
FhirPartitionService | assignPartition(clinicId) → { 'X-Clinic-ID': slug } | — |
ddx-fhir (HAPI FHIR R4 8.10.0-1, overlay layout):
| Overlay file | Role |
|---|---|
overlay/.../r4/interceptor/ClinicPartitionInterceptor.java | reads X-Clinic-ID header |
overlay/.../r4/tenant/TenantRegistryService.java | slug → partition int map (60 s cache) |
overlay/.../r4/tenant/TenantAdminController.java | POST /admin/tenants/{refresh,register} |
overlay/src/main/resources/init-tenants.sql | seeds organizations + partition_sequence |
Every FhirClientService method sends the clinic slug verbatim as the X-Clinic-ID header (the slug IS the tenant key on the wire — no UUID/partition-int leaks to the HTTP layer). HAPI's ClinicPartitionInterceptor resolves that slug to an integer partition via TenantRegistryService and scopes all reads/writes to the matching partition, providing tenant isolation at the JPA layer.
See coding_context/ddx-hms-context.md §Platform for the full module map.
Tech Stack & Choices
| Layer | Technology | Rationale |
|---|---|---|
| FHIR server | HAPI FHIR R4 8.10.0-1 (Java 21 / Spring Boot 3.5.x) | Mature, HL7-certified; partition support; Spring Boot ops endpoints |
| FHIR version | R4 (not R4B/R5) | KBV ePA uses R4; broadest lab/insurance compatibility in DACH region |
| Storage backend | PostgreSQL 18-alpine (dedicated database ddx_fhir_core) | Separate container/datasource from ddx_api_main; HAPI stores resources as JSON blobs in the JPA HFJ_RESOURCE table |
| Partitioning | HAPI built-in partitions (integer ID per clinic) | Hard tenant isolation without per-tenant schema or JVM — one HAPI process for all clinics |
| NestJS client | FhirClientService — typed wrapper over HAPI REST | Enforces partition routing; surfaces FHIR 404/400 as NestJS NotFoundException/BadRequestException |
| Interface | IFhirClient — DI token FHIR_CLIENT | Enables mock injection in tests; decouples service layer from HAPI implementation detail |
| Batch/transaction | executeBatch() + transaction() methods | Atomic multi-resource writes for seeder and migration use cases |
Key design decision: Application code NEVER constructs HAPI URLs directly. The FhirClientService owns all URL assembly, partition header injection, and error mapping. Bypassing the client (direct axios/fetch to the HAPI base URL) breaks tenant isolation — this is enforced by linting rules.
Data Flow
Clinical data write (e.g., create lab result)
POST /api/v1/lab-results→LabResultsController.create()(lab-results.controller.ts:88) with clinic slug fromX-Clinic-IDheader.LabResultsService.create()builds aFhirDiagnosticReportResourcepayload and callsfhirClient.create<FhirDiagnosticReportResource>('DiagnosticReport', resource, clinicSlug).FhirClientService.create()(fhir-client.service.ts:60) delegates toGenericFhirService, which sets theX-Clinic-ID: <slug>header (generic-fhir.service.ts:509) and POSTs to$FHIR_BASE/fhir/DiagnosticReport(base fromHAPI_FHIR_BASE_URL).- HAPI
ClinicPartitionInterceptorvalidates the partition header, routes the write to the correct tenant partition, and returns the created resource with a server-assigned numeric ID. FhirClientServicemaps HAPI HTTP errors (404, 400, 422) to NestJS exceptions and returns the typed resource.
Tenant onboarding
- Super Admin creates a new organization via the Super Admin portal (
POST /super-admin/tenantsin NestJS — writesorganizations.fhirPartitionIdallocated frompartition_sequence). - NestJS calls
POST /admin/tenants/refresh(or/admin/tenants/register) on HAPI'sTenantAdminControllerto reload the slug→partition cache. TenantRegistryServicereads theorganizationstable and caches the slug→partition-ID map in memory.init-tenants.sqlseeds theorganizations/partition_sequence/global_configtables idempotently; the 60 s@Scheduledrefresh reconciles any organizations added while the cache was stale.
$everything operation
FhirClientService.everything(patientId, clinicSlug) (fhir-client.service.ts:150) issues a FHIR GET /Patient/{id}/$everything call, returning all resources linked to a patient (Encounters, Observations, Conditions, MedicationRequests, etc.) as a FHIR Bundle. Used by the patient export and medico-legal summary flows.
Implicated Code
ddx-api/src/platform/fhir/fhir.module.ts:41—FhirInfrastructureModule— binds theFHIR_CLIENTtoken toFhirClientService, exports it (+ supporting services)ddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:33—FhirClientService— typed HAPI REST client; delegates CRUD/search/transaction toGenericFhirService, implements $everything/patch/ping directlyddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:60—create()— delegates toGenericFhirService.create(X-Clinic-ID header)ddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:85—search()— FHIR search with query params + slug routingddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:102—executeBatch()— batch/transaction bundle execution (used by seeder)ddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:150—everything()—$everythingFHIR operation for full patient record exportddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:203—patch()— FHIR JSON Patch (RFC 6902,application/json-patch+json)ddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:289—transaction()— FHIR transaction bundle (all-or-nothing multi-resource write)ddx-api/src/platform/infrastructure/fhir/fhir-client.service.ts:375—ping()— health check against HAPI/metadataendpointddx-api/src/platform/core/interfaces/services/fhir-client.interface.ts:80—IFhirClientinterface (FHIR_CLIENTsymbol at:18) — DI contract; enables test mocksddx-api/src/platform/fhir/services/generic-fhir.service.ts:509—buildHeaders()setsX-Clinic-ID: <slug>on every requestddx-api/src/platform/fhir/services/fhir-partition.service.ts:95—resolveClinicSlug()— UUID → slug resolution (TTL cache);assignPartition():112→{ 'X-Clinic-ID': slug }ddx-fhir/overlay/src/main/resources/init-tenants.sql:38— seedsorganizations+partition_sequence(partition 0 = DEFAULT/system reserved by HAPI, ≥1 = clinics)
Operational Notes
- Direct HTTP to HAPI is forbidden: Any code that calls
$FHIR_BASE/fhir/...directly bypasses partition routing and writes to the default (partition 0) tenant. This is a silent data-leak bug. All access must go throughFHIR_CLIENTtoken. - HAPI health check:
GET $FHIR_BASE/fhir/metadatareturns a CapabilityStatement.FhirClientService.ping()(fhir-client.service.ts:375) wraps this for use in health checks. Base URL isHAPI_FHIR_BASE_URL— resolve LIVE (dev 6xxx / acc-dev 17xxx), never hardcode. - Partition 0 is the system partition: Resources written to partition 0 are visible to all tenants. Never write patient data to partition 0. The
ClinicPartitionInterceptorblocks writes without an explicitX-Clinic-IDheader to prevent accidental cross-tenant writes. - FHIR numeric IDs vs UUIDs: HAPI assigns numeric IDs (e.g.,
"4910") to resources. Domain services use UUIDs as the stable API surface and resolve to FHIR numeric IDs viafhir_resource_linkrows orPatientIdResolverService. Passing a UUID to a raw HAPI endpoint returns 404. - Search performance: HAPI stores indexes in
HFJ_SPIDX_*tables. Unindexed search params trigger full-table scans. LOINC-coded Observations andDocumentReference.categoryare indexed. If new search params are needed, addSearchParamDefinitionannotations to the HAPI starter config. - Version: HAPI FHIR 8.10.0-1 (Java 21 / Spring Boot 3.5.x), overlay+vendor+git-subtree layout (
ddx-fhir/pom.xmlis a thin aggregator →vendor/hapi-fhir-jpaserver-starter; DDX customizations underoverlay/). Upstream bumps viaddx-fhir/scripts/upstream-pull.sh <tag>. Breaking changes in the HAPI JPA layer require migration ofHFJ_*tables — test in dev before updating.
Related Topics
- FHIR Partitions and Multi-Tenancy — How clinic slugs map to HAPI partition IDs; ClinicPartitionInterceptor deep-dive
- FHIR ResourceFacade — Declarative Prisma↔FHIR↔MinIO storage primitive used by domain modules
- Prisma 7 Schemas — Prisma operational data alongside HAPI clinical data
- Lab Results and Diagnostic Reports — DiagnosticReport resources stored in HAPI
- Clinical Notes — Visit Notes with Audio Transcription — DocumentReference resources stored in HAPI
- Clinical Visits — FHIR Encounter as the anchor resource; visit-to-FHIR-ID resolution