fhir-rust

A Rust monorepo for the HL7® FHIR® standard: the data model, six relational database ports, and an HTTP surface over them.

HL7® and FHIR® are registered trademarks of Health Level Seven International — see Trademarks below.

Family What it is Where
Model FHIR® R2/R3/R4/R4B/R5/R6 as serde-serializable Rust types, generated from the official specification packages, one cargo feature per release fhir/
Persistence core the engine-agnostic half of storage: audit chain, attribution, result types fhir-store/
Databases those resources stored as real relational tables, six SQL engines fhir-postgresql/ + 5 siblings
HTTP surface a FHIR RESTful API server over one of those stores fhir-loco/

They stack in one direction: the model knows nothing about databases, the database ports carry no HTTP or CLI, and the server adds only status codes. spec/index.md is the root of every specification and says which one governs what.

The database ports#

Store FHIR resources in a SQL database as real relational tables — typed columns, child tables, foreign keys, check constraints — not JSON blobs. Get them back losslessly.

Six ports, one specification, one engine.

Port Database Status
fhir-postgresql PostgreSQL 18 Reference — full store, full test suite
fhir-sqlite SQLite 3 Store — native, embeddable, no server
fhir-mysql MySQL 8.4 Store
fhir-mariadb MariaDB 11.4 Store
fhir-mssql SQL Server Store — live-verified against azure-sql-edge, incl. upgrade (F-65)
fhir-oracle Oracle Database Store — live-verified against gvenzl/oracle-free (F-68), upgrade/backfill included (F-15, 2026-08-09); R4.5 open

Those are conformance levels, defined in spec/databases/00-conformance.md. They are the honest version: what has been verified for that port, not what its code contains. The conformance matrix breaks them down requirement by requirement, and it is the document to read before choosing one.

Status: pre-release. Each port's README now describes that port at its own conformance level; until 2026-07-31 all six carried the PostgreSQL reference's measured results with the engine name substituted, including in two ports with no store at all (spec/audit.md F-01, fixed).

The per-port book/ directories were rewritten on 2026-08-03: each names its own engine and attributes every serve/endpoint to fhir-loco (F-56). The conformance matrix is the status document to trust, and spec/audit.md lists one finding still open (F-98).

Also here: the model crate#

fhir/ is the FHIR data model itself — every resource, complex datatype, primitive newtype, and code system as idiomatic Rust, produced by a generator that reads the official specification JSON. It has no database dependency and no I/O, and the database ports do not depend on it: they shred serde_json::Value against a generated relational map rather than against Rust types. The two families share a domain and a discipline, not code.

Releases modelled in code R5 (5.0.0), R4B (4.3.0), R4 (4.0.1), R3 (3.0.2), R2 (DSTU2, 1.0.2), and R6 (6.0.0-ballot3)
Off by default r6 — generated from a draft specification that can change between ballots. r5 is the default; every other release is an opt-in feature
Name reservations, containing no types fhir-r1 (DSTU1) and fhir-r7–fhir-r10
Shape one crate per release behind a cargo feature, ~135k generated lines each
Specification fhir/spec/index.md — 14 sections, ids R1.x–R14.x

Its requirement ids share the R4 prefix with this specification's §4 and mean something entirely different; spec/index.md records the clash and how to cite around it.

Also here: the persistence core and the HTTP surface#

fhir-store/ is the engine-agnostic half of persistence — the tamper-evident audit chain, the attribution and disclosure records, and the value types every port's operations return. It links no driver and opens no socket. It exists because chain.rs alone was 618 lines byte-identical in all six ports, and unwatched by the shared-core gate.

fhir-loco/ is a FHIR RESTful API server — Loco, Axum, Tokio and Hyper — over fhir-sqlite. The ports deliberately ship no server so that a program wanting FHIR storage does not also acquire a web framework; this is where that surface lives, and its job is narrow: translate HTTP to store calls and get the status codes right, so that a resource which was deleted answers 410 Gone and one that never existed answers 404 Not Found.

It has no specification yet, which means none of those externally visible promises can be cited by number or shown to have regressed. That gap is recorded in spec/index.md and, since it is now known to be the service §10 and §12 describe, in spec/audit.md F-58.

Why relational#

JSON storage makes writing FHIR easy and querying it painful. Normalized storage inverts that trade, and for a production clinical system the trade is right:

  • Integrity the database enforces — enum columns backed by FHIR value sets, CHECK constraints on choice elements, typed dates and decimals, reference columns that can be joined.
  • SQL that reads like the domain — SELECT family FROM r5.patient_name, no ->>'…' path spelunking, and the query planner sees real column statistics.
  • Search that is just SQL — FHIR search parameters compile to indexed predicates on ordinary columns.

The cost is thousands of generated tables per FHIR version — 7,355 for R5. That is fine for a database and impossible for a human, so everything is generated and every name is recorded in the relational map.

How it works#

FHIR spec packages          ──gen──▶  relational map + DDL  ──▶  assets/*.json.gz
(StructureDefinitions,                                            (committed)
 SearchParameters)
                                          │
  FHIR resource  ──shred──▶  rows  ───────┼───────  database
  FHIR resource  ◀─reconstruct──  rows  ──┘

A build-time generator reads each FHIR version's StructureDefinitions and SearchParameters and emits two artifacts per version: the DDL and a compact relational map. At runtime one generic engine walks any resource against the map to shred it into rows, and walks the map in reverse to reconstruct the identical resource. There is no per-resource handwritten code and no generated Rust — three versions × ~150 resource types × deep nesting would explode compile times for zero runtime benefit.

Round-trip fidelity is a tested invariant, decimal precision and partial dates included. Search parameters compile against the same map into SQL.

What differs between ports is two files. map/src/ddl.rs decides which SQL the generator emits; the store crate decides driver, transactions, and placeholder syntax. Everything else — shred, reconstruct, fold, canonical JSON, the whole generator — is byte-identical across all six.

Quick start#

The ports are libraries: no CLI, and no server crate in any port (C0.17, C0.18). The REST server is a separate crate — fhir-loco, on Loco.rs, Axum, Tokio and Hyper — which mounts a FHIR API over a store.

SQLite needs no server, so it is the shortest path to a working store:

[dependencies]
fhir-sqlite-map   = "0.6.1"
fhir-sqlite-store = "0.6.1"   # add `features = ["r3", "r4"]` for other versions
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde_json = "1"
use std::sync::Arc;
use fhir_sqlite_map::model::RelMap;
use fhir_sqlite_store::{Audit, sqlite::SqliteStore};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // The map is compiled in — feature `r5`, on by default. No file to read,
    // and no FHIR specification packages needed.
    let map = Arc::new(RelMap::bundled("r5")?);

    let store = SqliteStore::open("clinic.sqlite", map).await?;
    store.init("r5-baseline").await?;        // ~7,400 tables, one transaction

    let patient = serde_json::json!({
        "resourceType": "Patient",
        "id": "example",
        "name": [{ "family": "Ærø", "given": ["Anna"] }],
        "birthDate": "1974-12"               // a partial date, preserved verbatim
    });

    store.put(&patient, &Audit::cli()).await?;

    let back = store.get("Patient", "example").await?.unwrap();
    assert_eq!(back, patient);               // losslessly, including "1974-12"

    // Accent- and case-insensitive by construction: "aero" finds "Ærø".
    let hits = store.search("Patient", &[("name".into(), "aero".into())], 50, 0).await?;
    println!("{hits:?}");
    Ok(())
}

Then query it as what it is — a relational schema:

SELECT n.family, count(o.id) AS observations
  FROM patient p
  JOIN patient_name n ON n.rid = p.id AND n.ords = '{1}'
  LEFT JOIN observation o
    ON o.subject_ref_type = 'Patient' AND o.subject_ref_id = p.id
 GROUP BY n.family
 ORDER BY observations DESC;

Every child table addresses its rows with rid (the resource id) and ords (the 1-based index path through repeating elements), so arbitrarily nested — even recursive — structure stays joinable.

Full walkthroughs: doc/.

Which port#

If you want Use
Production today, with the full test suite behind it fhir-postgresql
Embedded, no server, a single file fhir-sqlite
An existing MySQL or MariaDB estate fhir-mysql, fhir-mariadb
SQL Server or Oracle supported, Store level — fhir-mssql, fhir-oracle; see the matrix for remaining gaps (no conditional_create_audited/put_audited/transact_audited on mssql; no working R4.5 mechanism and no search_page concurrency coverage on Oracle)

doc/choosing-an-engine.md has the long version, including what each engine costs you in its type bindings.

What these libraries guarantee, and what they do not#

Because the data is protected health information, the boundary is stated rather than implied (§12):

Guaranteed here Your deployment must provide
Attribution on every write Authentication — unless you run fhir-loco, which verifies a PASETO v4.public token on every request and has no unauthenticated mode
A disclosure record on every read Authorization, scopes, compartments, consent
Tamper-evident history (SHA-256 + SHA3-256, optional HMAC) TLS termination
Append-only history enforced in the database Rate limiting per identity
Erasure that leaves a verifiable tombstone Terminology validation
No PHI in logs at default level Everything else a certified system needs

These are components, not certified systems. They cannot make a deployment compliant; they are built so as not to be the reason one cannot be. §13 maps each obligation to a numbered requirement and to the test that evidences it.

Documentation#

index.md every entry point in the repository, in one page
doc/index.md documentation hub — tutorials, examples, comparisons
spec/index.md the root of every specification — which one governs what
spec/databases/index.md the normative core, shared by all six ports
spec/databases/conformance-matrix.md what each port actually satisfies
spec/databases/audit.md every known divergence, with evidence
spec/publishing.md how these crates reach crates.io — a documented laptop step, by decision, since all 34 already have
spec/trusted-publishing/index.md why that decision means no Trusted Publishing here yet, checked against each forge's actual support
fhir/spec/index.md the model crate's specification
AGENTS.md contributing — human or agent
fhir-<engine>/book/ that port's user guide
fhir-<engine>/plan.md that port's design decisions

AI tooling#

Machine-readable guidance, alongside the human-oriented docs above.

llms.txt / llms.json a curated map of the repository's most important content, under 40k bytes each (spec)
fhir-skill/ agent skill for end users — FHIR® concepts, terminology, examples grounded in this repo
fhir-rust-maintainer-skill/ agent skill for maintainers — checklists and commands for shared-core, spec, and verification work
spec/agent-skills/index.md the requirement behind the two skill folders above

The docs site publishes its own copy at fhir-rust.github.io/llms.txt — generated, not copied, so every link resolves on that domain rather than against this repository's paths.

Project documents#

INSTALL.md requirements, which port to pick, and how to add it to a project
PHI.md what this does and does not do with patient data, in plain language
SECURITY.md how to report a vulnerability, what is in scope, what is open
CONTRIBUTING.md ways to help — data, expertise, review, code, money — then the five rules and the green gate
RFC.md the open questions this project most wants answered, and what feedback helps
CODE_OF_CONDUCT.md Contributor Covenant 2.1, plus one addition specific to this project
COMPARISONS.md how this relates to HAPI, Firely, Medplum, Aidbox, SQL on FHIR, and the other Rust FHIR crates
BENCHMARKS.md what has been measured, on what, and what has not
CHANGELOG.md what changed, repository-wide
NEWS.md project news, how to follow, and press contacts
MAINTAINERS.md who maintains this, and what happens if they cannot
GOVERNANCE.md how decisions get made, what binds them, and how to disagree
AI_STATEMENT.md how AI tooling is used here, and the failure mode it has already caused
CITATION.cff how to cite this work
LICENSE.md the five-way licence choice, with SPDX
help/outreach/index.md who this is for, what may be claimed about it, and what is missing

Specification-driven#

Behaviour is decided in spec/ before it is written in Rust, and every requirement keeps a permanent number so a test, a commit, and an auditor's workpaper can all point at the same thing years later.

The database specification is one copy, at the root, in spec/databases/. Until this revision it was six copies, one per port, identical apart from the product name — and they had begun to drift exactly as you would expect. Each port now states only where it departs, in a numbered dialect annex, so a departure has to be written down to exist.

The model crate keeps its own specification in fhir/spec/, because it is a different artifact with a different release cadence and a different conformance question. Neither family's requirements bind the other; spec/index.md states that precedence rule and the one prefix collision between them.

License#

MIT OR Apache-2.0 OR BSD-3-Clause OR GPL-2.0-only OR GPL-3.0-only — declared identically by all 34 crates in the repository, across all four families. OR means you choose; no obligation from one option carries into another.

Full statement: LICENSE.md. Versions already on crates.io keep the terms they were published under, since a published version is immutable.

Trademarks#

HL7®, and FHIR® are the registered trademarks of Health Level Seven International and their use of these trademarks does not constitute an endorsement by HL7.

Edit this page on GitHub — the repository is the source of truth; this site renders it.