Skip to content

Commit adfcbab

Browse files
Add aidbox-orchestration-service example (#28)
* Add aidbox-orchestration-service example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update Aidbox env vars, add check-env script, add FHIR Facade feature tag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove check-env.sh script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add tree-shaking to generate-types script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2c2a0ad commit adfcbab

87 files changed

Lines changed: 9703 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# SMART Backend Services credentials
2+
# Test RSA private key (PKCS#8) — matches public key in general-practice-config/init.json
3+
# Use \n for newlines
4+
SMART_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+EfYcGCDTGRh8\natyBcMQP5ehtE3bv06VaQCLk+uDV0+CPjsLxO+5XRiAUOg9ea2Gufw72DLNpUd30\ndo7d50iaqmDfC/ixCy0UXT0UHjHQNfqUizhJY1/AVj4HaxubGkpK/FVeIzpBEG8L\nruDqGCZflPkjDMYVTeb6Gk4ZnnSLIJWZE6ZCwWvFT9fFI+x2sf/JhpyPTsomhPN6\n6qja//MVjzw0HRy4Nb6SOhprGbK55fhKke3oz8hW2Y90nrBEn/HUCdqV9IQvaAeK\nILETxmEZYnni0+9IrefgD4AyIfFR4KCNfqPG7KyxJvC1o7wrd/yuXCyz03+sZjWX\ni6DXM7mxAgMBAAECggEAXuK+l0XgVSIpHCuIy0HNTxZ6UsGt1Yo1+PkdsmwgA/9T\nEre1UBKYKI+EgjR96af3ytH5WRH8Gu7YvCrXpaXJlBTMaW0jiNbIeWsWi82LFqNr\n5e4eelyWt4EWVEO/M04LmqWfxHAXq9WVaiKye4r01TCcs0e0N3x9e4vYQ2fcTHto\n2jybdna/dIuFzgnhk5fGC7nJ17Dnf00/8pJHd6JRoYb/EecuSbB4OjGuPgtF7N9j\nlKto8TwEeIIcKnUpqUxQD5keVluTqDVZt38LS62V7wRnwLzINIlzsTZ7fdgd/CPm\nnjY3Sa8t+j4ULzvWm4goW4kBYJ76G8hXRvA+oKkYAQKBgQD6BylXOswFlDgO6VoP\nQVrujxjalpSp9Vvovo/sSS40UO9hqsNHjuuwPNtLsV1e+GSC14oKfmKt4YooeES5\nmDkzAtlyKUcLtIRJifqQRyBbqf+8ZqtZkXavEp2L8VsKu45rNalHvgwqgCNNVdJ/\n1vnpiGt9z0wrCrQ63fWVOcXnMQKBgQDCnC16ZRgWLKAMc5j6gBAjsMk/SbLk7yM9\n7Ru1YSDFubQ/d8RTNcWDsM1r7OuwsD3zpTHk7b2BFSXtHKgtmV02TPlen9IFA1AV\nUT7rF840ONJSZzUq1/737kSuznQh83WL6BksDfWZRV2iYMuUZNcO0tTX+aUJAgt4\nQwvFY6NagQKBgQDRT018iOxjf0Guugt62euV6pWT6Jtr7MuUfHNgC6NyiI7d5Ga2\ncR892rR7GXBhIPCD2IznXAagKj/OwWBHPvgjjC8dMxEW63gTWD86qVCdbCN7RTgN\nM4l35s2daeAdjAYeGj4soRzuN3dWNpKSExYEOwBBwlixb7SR017UHhlfAQKBgGp1\nWx+Ia/u9X7RQDFCEe8+6ZuzbGSTJeMLokW7QekgPxX2uu9Q1Jx5aOpWennQihVFi\nff/Y2gDiG8QxGAMR0X7h7syHqzEY1ddDgaLDfAbvSobPdLNCQ3VHf4UM5VSpRRVK\n23JRFJhK7OTmBJfh7g9q4Aphw5lA6Btaufa6AeOBAoGAXmF6ImjDJcxTzN+f1hVa\nTBdE+QhdLPN8NHMzKK5xkurqBEc+nKx5RWBiVZ+EhnDO59jQ6Jgimkw4LUCuVNGG\nd/p1+ddTsZ88qby+iFbFnDljo/q29SOgXaySc9Hi723sQgmTiPPPKc5wXe/1mUY0\nrkSM+azcO0Wyc+OAAfX7u6A=\n-----END PRIVATE KEY-----"
5+
SMART_KEY_ID=test-key-001
6+
SMART_CLIENT_ID=orchestration-service
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Environment
5+
.env
6+
.env.local
7+
.env.*.local
8+
9+
# Build
10+
dist/
11+
*.tsbuildinfo
12+
13+
# IDE
14+
.idea/
15+
.vscode/
16+
*.swp
17+
*.swo
18+
*~
19+
20+
# OS
21+
.DS_Store
22+
Thumbs.db
23+
24+
# Logs
25+
*.log
26+
npm-debug.log*
27+
28+
# Bun
29+
bun.lock
30+
31+
# Codegen
32+
.codegen-cache/
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# CLAUDE.md
2+
3+
## Project Overview
4+
5+
FHIR Orchestration Service POC implementing `$getstructuredrecord` (GP Connect). Fetches patient data from 2 FHIR sources (SMART Backend Services + Basic Auth), deduplicates via ConceptMap/$translate, stores with Provenance, and returns merged Bundle.
6+
7+
## FHIR Type Generation
8+
9+
FHIR TypeScript types are generated using `@atomic-ehr/codegen`. The config is at `scripts/generate-types.ts`.
10+
11+
Generated types are output to `src/fhir-types/` — do not edit these files manually.
12+
13+
```ts
14+
import type { Patient, Bundle, Provenance } from "./fhir-types/hl7-fhir-r4-core";
15+
import type { UKCoreAllergyIntolerance } from "./fhir-types/fhir-r4-ukcore-stu2/profiles/UkcoreAllergyIntolerance";
16+
import { UKCorePatientProfile } from "./fhir-types/fhir-r4-ukcore-stu2/profiles";
17+
```
18+
19+
To regenerate types: `bun run generate-types`.
20+
21+
## Commands
22+
23+
| Command | Description |
24+
|---------|-------------|
25+
| `bun run start` | Start orchestration service |
26+
| `bun run typecheck` | TypeScript type check |
27+
| `bun run generate-types` | Regenerate FHIR TypeScript types |
28+
| `docker compose up -d --build` | Start all services |
29+
30+
## Project Structure
31+
32+
- `src/index.ts` — HTTP server (Bun.serve), routing, error handling
33+
- `src/orchestration.ts` — Main orchestration flow (fetch, deduplicate, store)
34+
- `src/deduplication.ts` — Per-resource deduplication logic (Patient merge, AllergyIntolerance code match, Observation time/value match)
35+
- `src/provenance.ts` — Provenance resource creation for audit trail
36+
- `src/fhir-clients.ts` — Auth providers (SMART + Basic), source fetching
37+
- `src/fhir-types/` — Auto-generated FHIR types (do not edit)
38+
- `scripts/generate-types.ts` — Codegen configuration
39+
- `aidbox-config/init.json` — ConceptMap for LOINC->SNOMED CT translation
40+
- `general-practice-config/init.json` — GP test data (SNOMED CT)
41+
- `hospital-config/init.json` — Hospital test data (LOINC)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM oven/bun:1 AS base
2+
WORKDIR /app
3+
4+
# Install dependencies
5+
FROM base AS install
6+
RUN apt-get update && apt-get install -y curl
7+
COPY package.json bun.lock* ./
8+
RUN bun install --frozen-lockfile || bun install
9+
10+
# Production stage
11+
FROM base AS release
12+
RUN apt-get update && apt-get install -y curl
13+
COPY --from=install /app/node_modules node_modules
14+
COPY src ./src
15+
COPY package.json .
16+
COPY tsconfig.json .
17+
18+
USER bun
19+
20+
EXPOSE 3000
21+
22+
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
23+
CMD curl -f http://localhost:3000/health || exit 1
24+
25+
CMD ["bun", "run", "src/index.ts"]
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
---
2+
features: [FHIR Orchestration, FHIR Facade, Deduplication, ConceptMap, Provenance, SMART on FHIR]
3+
languages: [TypeScript]
4+
---
5+
6+
# FHIR Orchestration Service
7+
8+
Implements [`$getstructuredrecord`](https://simplifier.net/guide/gpconnect-data-model/Home/FHIR-Assets/All-assets/OperationDefinitions/OperationDefinition-GPConnect-GetStructuredRecord-Operation-1?version=current) operation that fetches patient data from multiple FHIR sources, deduplicates resources using terminology translation, and returns a merged Bundle with Provenance tracking.
9+
10+
## Problem
11+
12+
Patient data is often spread across multiple healthcare systems that use different terminologies (SNOMED CT, LOINC) and authentication methods (SMART Backend Services, Basic Auth). A client application needs a single, deduplicated view of the patient record.
13+
14+
### Requirements
15+
16+
1. **Multi-source aggregation**: Fetch data from General Practice (SMART on FHIR) and Hospital (Basic Auth) in parallel
17+
2. **Cross-terminology deduplication**: Match resources coded in different systems (LOINC vs SNOMED CT) using [ConceptMap/$translate](https://hl7.org/fhir/R4/conceptmap-operation-translate.html)
18+
3. **Audit trail**: Store source bundles and merged results with [Provenance](https://hl7.org/fhir/R4/provenance.html)
19+
20+
## Architecture
21+
22+
```mermaid
23+
flowchart LR
24+
CA(Client Application):::blue
25+
OL(Orchestration Service):::green
26+
FS[(Aidbox)]:::green
27+
GP[(Aidbox<br/>General Practice<br/>SMART on FHIR)]:::orange
28+
HOSP[(Aidbox<br/>Hospital<br/>Basic Auth)]:::orange
29+
30+
CA -->|"$getstructuredrecord"| OL
31+
OL -->|"Bundle"| CA
32+
OL <-->|"Store + Deduplicate"| FS
33+
OL -->|"GET Bundle"| GP
34+
OL -->|"GET Bundle"| HOSP
35+
36+
classDef blue fill:#e1f5fe,stroke:#01579b
37+
classDef green fill:#e8f5e9,stroke:#2e7d32
38+
classDef orange fill:#fff3e0,stroke:#ef6c00
39+
```
40+
41+
For the POC, all three FHIR servers are Aidbox instances. In production, the external sources would be real GP and Hospital FHIR servers.
42+
43+
## Sequence Diagram
44+
45+
```mermaid
46+
sequenceDiagram
47+
participant Client
48+
participant FS as FHIR Server
49+
participant Orch as Orchestration
50+
participant GP as General Practice
51+
participant Hospital
52+
53+
Client->>Orch: $getstructuredrecord (NHS Number)
54+
55+
par Fetch from sources
56+
Orch->>GP: POST /auth/token (JWT)
57+
GP-->>Orch: access_token
58+
Orch->>GP: GET /fhir/Bundle (Bearer)
59+
GP-->>Orch: Bundle
60+
and
61+
Orch->>Hospital: GET /fhir/Bundle (Basic auth)
62+
Hospital-->>Orch: Bundle
63+
end
64+
65+
Orch->>FS: Store 2 source Bundles
66+
Orch->>FS: ConceptMap/$translate
67+
FS-->>Orch: Translated code
68+
Note over Orch: Deduplicate & Merge
69+
Orch->>FS: Store merged Bundle + Provenance
70+
71+
Orch-->>Client: Merged Bundle
72+
```
73+
74+
### Flow
75+
76+
1. **Client request** - Client calls `$getstructuredrecord` with patient's NHS Number
77+
2. **Parallel fetch** - Orchestration fetches bundles from both sources simultaneously:
78+
- General Practice: SMART Backend Services auth (JWT client assertion -> Bearer token)
79+
- Hospital: Basic authentication
80+
3. **Store for audit** - Source bundles, merged bundle, and Provenance stored in main FHIR Server
81+
4. **Terminology normalization** - `ConceptMap/$translate` converts LOINC codes to SNOMED CT for cross-system matching
82+
5. **Deduplication** - Resources with matching normalized codes are deduplicated
83+
6. **Response** - Merged bundle with deduplicated resources returned to client
84+
85+
## Deduplication Algorithms
86+
87+
| Resource | Key | Algorithm | Why? |
88+
| ---------------------- | ----------------------- | ------------------------ | ---------------------------------------------------------------- |
89+
| **Patient** | NHS Number | Merge | One person = one patient, combine data from sources |
90+
| **AllergyIntolerance** | code (normalized) | Match + select by status | Same allergy in different terminologies -> ConceptMap translation |
91+
| **Observation** | code + time +/-1h + value | Match if all equal | Same measurement, but different values = clinically significant |
92+
| **Encounter** | - | No deduplication | Each visit is unique, even on the same day |
93+
94+
### Patient
95+
96+
```
97+
1. Group by NHS Number
98+
2. Merge: keep most complete name (more given names wins)
99+
3. Merge: take first non-null telecom, address
100+
4. Result: single Patient with merged data
101+
```
102+
103+
### AllergyIntolerance
104+
105+
```
106+
1. Translate LOINC codes to SNOMED CT via ConceptMap/$translate
107+
2. Group by normalized SNOMED code
108+
3. Select canonical: prefer confirmed verificationStatus
109+
```
110+
111+
Example:
112+
113+
```
114+
General Practice: SNOMED 91936005 (confirmed) -+
115+
Hospital: LOINC LA30099-6 (unconfirmed) -+-> translate -> match -> keep GP (confirmed)
116+
```
117+
118+
### Observation
119+
120+
```
121+
1. Group by: code + effectiveDateTime (+/-1h)
122+
2. Compare values:
123+
- Same value -> deduplicate
124+
- Different value -> keep both (clinical significance)
125+
3. Select canonical: prefer has interpretation, has referenceRange
126+
```
127+
128+
Example:
129+
130+
```
131+
GP: HbA1c = 7.2% @ 10:00 -+-> same code, +/-1h, same value -> deduplicate
132+
Hospital: HbA1c = 7.2% @ 10:30 -+
133+
134+
GP: HbA1c = 7.2% @ 10:00 -> keep
135+
Hospital: HbA1c = 6.8% @ 10:30 -> keep (different value = clinically significant)
136+
```
137+
138+
## Quick Start
139+
140+
### 1. Configure environment
141+
142+
```bash
143+
cp .env.example .env
144+
```
145+
146+
The `.env.example` includes a test RSA private key for SMART Backend Services authentication. The matching public key is already configured in `general-practice-config/init.json`.
147+
148+
### 2. Start services
149+
150+
```bash
151+
docker compose up -d --build
152+
```
153+
154+
Wait for all services to become healthy:
155+
156+
```bash
157+
docker compose ps
158+
```
159+
160+
All 4 services should show "healthy" status:
161+
- http://localhost:8080 - Main FHIR server (Aidbox UI)
162+
- http://localhost:8081 - General Practice FHIR server
163+
- http://localhost:8082 - Hospital FHIR server
164+
- http://localhost:3000 - Orchestration service
165+
166+
Each Aidbox instance loads its init bundle on startup:
167+
168+
- **fhir_server**: ConceptMap for LOINC->SNOMED CT translation (`aidbox-config/`)
169+
- **general_practice**: Test patient bundle with SNOMED CT codes (`general-practice-config/`)
170+
- **hospital**: Test patient bundle with LOINC codes (`hospital-config/`)
171+
172+
### 3. Test the orchestration
173+
174+
```bash
175+
curl -X POST http://localhost:3000/fhir/Patient/\$getstructuredrecord \
176+
-H "Content-Type: application/fhir+json" \
177+
-d '{
178+
"resourceType": "Parameters",
179+
"parameter": [{
180+
"name": "patientNHSNumber",
181+
"valueIdentifier": {
182+
"system": "https://fhir.nhs.uk/Id/nhs-number",
183+
"value": "9876543210"
184+
}
185+
}]
186+
}'
187+
```
188+
189+
Expected response: Merged bundle with 1 Patient, 1 AllergyIntolerance (2->1 deduplicated via ConceptMap), 2 Observations (3->2 deduplicated), 2 Encounters.
190+
191+
### 4. Verify Provenance
192+
193+
Provenance resources are stored for audit but not included in response:
194+
195+
```bash
196+
curl -u root:secret http://localhost:8080/fhir/Provenance
197+
```
198+
199+
## Test Data
200+
201+
All resources conform to [UK Core STU2](https://simplifier.net/hl7fhirukcorer4) profiles.
202+
203+
**General Practice (SNOMED CT):**
204+
205+
- [UKCore-Patient](https://simplifier.net/hl7fhirukcorer4/ukcorepatient): NHS 9876543210, Smith John William, address + telecom
206+
- [UKCore-AllergyIntolerance](https://simplifier.net/hl7fhirukcorer4/ukcoreallergyintolerance): SNOMED `91936005` (Allergy to penicillin), confirmed, high criticality
207+
- [UKCore-Observation](https://simplifier.net/hl7fhirukcorer4/ukcoreobservation): HbA1c = 7.2% @ 2024-01-15T10:00 (with interpretation + referenceRange)
208+
- [UKCore-Encounter](https://simplifier.net/hl7fhirukcorer4/ukcoreencounter): ENC-SMART-001
209+
210+
**Hospital (LOINC):**
211+
212+
- [UKCore-Patient](https://simplifier.net/hl7fhirukcorer4/ukcorepatient): NHS 9876543210, Smith John, local ID H12345
213+
- [UKCore-AllergyIntolerance](https://simplifier.net/hl7fhirukcorer4/ukcoreallergyintolerance): LOINC `LA30099-6` (Penicillin allergy), unconfirmed
214+
- [UKCore-Observation](https://simplifier.net/hl7fhirukcorer4/ukcoreobservation): HbA1c = 7.2% @ 2024-01-15T10:30 (duplicate)
215+
- [UKCore-Observation](https://simplifier.net/hl7fhirukcorer4/ukcoreobservation): HbA1c = 6.8% @ 2024-02-20T14:00 (unique)
216+
- [UKCore-Encounter](https://simplifier.net/hl7fhirukcorer4/ukcoreencounter): ENC-BASIC-001
217+
218+
**Result after orchestration:**
219+
220+
| Resource | Source | Result | Reason |
221+
| ------------------ | ------------------ | ------ | ------------------------------------------------------------------------- |
222+
| Patient | 2 (GP + Hospital) | 1 | Merged by NHS Number, kept "John William" (more given names) |
223+
| AllergyIntolerance | 2 (SNOMED + LOINC) | 1 | LOINC->SNOMED translation matched, kept GP's (confirmed > unconfirmed) |
224+
| Observation (Jan) | 2 (GP + Hospital) | 1 | Same code, +/-30min, same value -> duplicate, kept GP's (has interpretation) |
225+
| Observation (Feb) | 1 (Hospital only) | 1 | Unique date, no match |
226+
| Encounter | 2 (GP + Hospital) | 2 | No deduplication (different identifiers) |
227+
228+
## Services
229+
230+
| Service | URL | Auth | Description |
231+
| ------------------ | --------------------- | ---------------------- | ---------------------------- |
232+
| `fhir_server` | http://localhost:8080 | Basic (root:secret) | Main FHIR server (storage) |
233+
| `general_practice` | http://localhost:8081 | SMART Backend Services | General Practice (SNOMED CT) |
234+
| `hospital` | http://localhost:8082 | Basic Auth | Hospital (LOINC) |
235+
| `orchestration` | http://localhost:3000 | - | Orchestration service |
236+
237+
## FHIR Implementation Guides
238+
239+
| Package | Version | Description |
240+
| ------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------------- |
241+
| [hl7.fhir.r4.core](https://hl7.org/fhir/R4/) | 4.0.1 | FHIR R4 base types |
242+
| [fhir.r4.ukcore.stu2](https://simplifier.net/hl7fhirukcorer4) | 2.0.2 | UK Core R4 profiles (UKCorePatient, UKCoreAllergyIntolerance, UKCoreObservation, etc.) |
243+
244+
TypeScript types for both packages are generated into `src/fhir-types/` using `@atomic-ehr/codegen`.
245+
246+
## Local Development
247+
248+
```bash
249+
bun install
250+
bun run src/index.ts
251+
bun run tsc --noEmit
252+
```
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"type": "transaction",
3+
"entry": [
4+
{
5+
"resource": {
6+
"resourceType": "ConceptMap",
7+
"id": "allergy-loinc-to-snomed",
8+
"url": "http://example.org/ConceptMap/allergy-loinc-to-snomed",
9+
"name": "AllergyLoincToSnomed",
10+
"title": "Allergy LOINC to SNOMED CT Mapping",
11+
"status": "active",
12+
"description": "Maps LOINC answer codes to SNOMED CT for NHS deduplication",
13+
"sourceUri": "http://loinc.org",
14+
"targetUri": "http://snomed.info/sct",
15+
"group": [
16+
{
17+
"source": "http://loinc.org",
18+
"target": "http://snomed.info/sct",
19+
"element": [
20+
{
21+
"code": "LA30099-6",
22+
"display": "Penicillin allergy",
23+
"target": [
24+
{
25+
"code": "91936005",
26+
"display": "Allergy to penicillin",
27+
"equivalence": "equivalent"
28+
}
29+
]
30+
}
31+
]
32+
}
33+
]
34+
},
35+
"request": {"method": "POST", "url": "/ConceptMap"}
36+
}
37+
]
38+
}

0 commit comments

Comments
 (0)