A web tool for diffing FHIR profiles element-by-element. Install any FHIR Implementation Guide from get-ig.org through the UI, pick two (or more) profiles, and see exactly where they agree, narrow each other, or conflict.
Built for cross-jurisdiction work — comparing a national base profile (e.g. FI Base Patient) against base FHIR R4 Patient, or two countries' Patient profiles against each other — but works with anything you can install.
bun install
bun --hot src/server.tsOpen http://localhost:4000. You'll see an empty IG list — click a suggestion (hl7.fhir.r4.core, hl7.fhir.fi.base, etc.) or type any package name and hit Install. Then pick two profiles and Compare.
- Bun 1.3+ for the server, bundler, and package manager.
- Claude Code CLI — optional, only required if you want to use the Explain with Claude button on diff rows. The server shells out to your local
claudelogin via the Claude Agent SDK; no API key needed, but the CLI must be installed and logged in. Without it, everything else works and the Explain button just returns an error.
Packages are pulled from fs.get-ig.org/pkgs/ via @atomic-ehr/fhir-canonical-manager. The on-disk cache lives at ./fhir-cache/ (gitignored).
When you install a package, every StructureDefinition in it is indexed into data/catalog.db (SQLite) with its url, type, title, kind, derivation, and package coordinates — that's what powers the picker's type filter and search.
A raw StructureDefinition is hard to diff: it's a flat list of ElementDefinitions with cryptic id/path strings and inherited fields scattered around. @atomic-ehr/fhirschema converts it into a tree shaped like the resource itself.
That tree is flattened into Map<dotted.path, leaf>, then the diff engine walks the union of paths across all selected profiles and classifies each row:
conflict— real contradiction (disjoint reference targets, mismatched fixed/pattern values, different required-binding value sets)narrow— one profile narrows another in a compatible way (tighter cardinality, narrower reference set)only-in— element appears in only some profiles
Each diff row has an Explain with Claude button. It POSTs the row's payload to /api/explain, which streams a focused explanation back via SSE using the Claude Agent SDK. No API key needed — it uses your local claude login. All tools are disabled; Claude only sees the prompt.
- Bun — runtime, package manager, HTTP server, bundler
- React 19.2 + Tailwind v4 via
bun-plugin-tailwind - @health-samurai/react-components — UI primitives
- @atomic-ehr/fhir-canonical-manager — package install + canonical URL resolution
- @atomic-ehr/fhirschema — SD → flat schema
bun:sqlite— local profile catalog
src/
server.ts # Bun.serve: HTML route + /api/*
index.html # HTML entry
frontend.tsx # React root
index.css # Tailwind v4 + react-components tokens
components/
App.tsx # layout shell
PackagesPanel.tsx # install / list / remove IGs
ProfilePicker.tsx # search + type filter + selection
DiffView.tsx # rendered diff rows + Explain
igs/
manager.ts # fhir-canonical-manager singleton
loadPackage.ts # install + index into SQLite
listPackages.ts
removePackage.ts
profiles/
diff.ts # N-way diff over flattened FHIRSchemas
flatten.ts # FHIRSchema tree → Map<path, leaf>
toSchema.ts # StructureDefinition → FHIRSchema
listProfiles.ts # picker catalog queries
loadByUrl.ts # resolve canonical URL via FCM
explain/diff.ts # Claude SSE streamer
db/open.ts # SQLite (data/catalog.db)
Caches that you can safely rm -rf:
fhir-cache/— installed FHIR packages (managed by FCM)data/catalog.db— SQLite picker catalog
Both are gitignored; deleting either just means the next install/load rebuilds it.
MIT.