Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

FerroPIX is the Master Patient Index of the FerroHEALTH family, in pure Rust. Every organisation issues its own identifiers for the same person. A patient index holds the cross-reference between those identifiers and answers the question every other component starts with: who is this patient, and which identifier does each system know them by. FerroPIX also mints the pseudonymous identifiers the clinical domain uses, so a clinical record carries a pseudonym and identifying data stays outside it.

FerroPIX speaks the IHE profiles integrators already expect: PIX and PDQ, in their FHIR forms PIXm and PDQm, the PMIR identity feed, and the HL7 v2 PIX and PDQ transactions beside them.

Where the project is

FerroPIX is built in the order of its milestones, and this book says which parts exist today.

  • Built and published: the two libraries of the identity specifications FerroPIX owns, ihe-iti (the IHE identity profiles, a feature each) and nl-generic-functions (the Dutch Generic Functions). They hold the client side of each profile and are on crates.io for any product to use. FerroFED takes both from there.
  • Planned: the server. The PIXm Manager, the PDQm Supplier, the PMIR Registry, the persisted cross-reference, pseudonym minting, the HL7 v2 PIX and PDQ transactions and the container image are the v0.0.3 milestone. Until it lands there is no FerroPIX binary to run, and the pages under Operate say so.

What FerroPIX is lists every capability with its status.

How this book is organised

  • Evaluate: what FerroPIX is, which specification versions it follows, and the licence.
  • Integrate: how to use the published crates today, feature by feature.
  • Operate: configuration, deployment, security and audit, and operations, each with what holds today and what the server adds.
  • Contribute: how the work is organised and how this book stays in step with the code.

This book covers FerroPIX alone. What every FerroHEALTH product shares is in the family book: how the products fit together, the manufacturer, Cadasto B.V., with its security policy, post-market procedure and the Cyber Resilience Act from the manufacturer’s side, and licensing and trademarks.

What FerroPIX is

FerroPIX is a Master Patient Index. It holds the patient identity cross-reference of a group of systems: which identifier each identifier domain (a hospital’s patient number, a national number, a regional index) uses for one person. A system that knows a patient by one identifier asks FerroPIX for the others, or finds a patient by their demographics.

Two siblings are planned to resolve patients through the FerroPIX server once it runs: FerroEHR, as the provider of its EHR index (FerroHEALTH/FerroEHR#3380), and FerroFED, whose identity binding uses a PIX Manager fake from its own tests until then (FerroHEALTH/FerroFED#367). FerroFED already takes both published crates from crates.io.

A patient index keeps identifying data outside the clinical record, which is where the openEHR Platform Service Model puts the EHR Index. FerroPIX adds pseudonyms: one per patient and pseudonymisation domain, so a clinical store names a patient by a value that identifies no one on its own.

What is built and what is planned

The table gives the status of each capability. “Client, in ihe-iti” means the published crate can call another system that offers the transaction; the server side, where FerroPIX answers the transaction itself, is the v0.0.3 milestone.

CapabilityTransactionsStatus
PIXm, the identifier cross-reference queryITI-83 by GET, or by POST of a Parameters bodyClient, in ihe-iti (pixm). The PIXm Manager is planned.
PIXm, the FHIR identity feedITI-104Planned, with the PIXm Manager.
PDQm, the demographics query and matchITI-78, ITI-119Client, in ihe-iti (pdqm). The PDQm Supplier is planned.
PMIR, the master identity feed and subscriptionITI-93, ITI-94Subscriber and feed Consumer, in ihe-iti (pmir). The PMIR Registry is planned.
mCSD, the care services directoryITI-90, ITI-91Client and replica, in ihe-iti (mcsd).
XCPD, cross-community patient discoveryITI-55Initiating Gateway, in ihe-iti (xcpd).
IUA, access tokens on each requestITI-72The authorizer seam of the ihe-iti clients.
The persisted cross-referenceidentifier domains, one source per domain, linking and mergePlanned.
Pseudonym mintinga UUID per patient and pseudonymisation domainPlanned.
HL7 v2 PIX and PDQITI-8, ITI-9, ITI-21, ITI-22, and ITI-44 to ITI-47 as the ITI Technical Framework texts decidePlanned, on FerroBRIDGE’s HL7 v2 parser and MLLP codec.
Audit of the Manager sideBALP AuditEvent through ihe-atnaPlanned. The clients’ audit records are built (balp, atna).
Dutch Generic Functions: localization (NVI)the NVI Localization Service searchClient, in nl-generic-functions (nvi).
Dutch Generic Functions: consent (Mitz)the closed authorization questionClient, in nl-generic-functions (mitz).
Dutch Generic Functions: addressing (LRZa)the URA of an organisationIn nl-generic-functions (lrza).
Dutch Generic Functions: authentication (Nuts)the Nuts RFC021 access token requestClient, in nl-generic-functions (nuts-auth).
BSN and the pseudonymised BSNthe identifier systems the functions shareThe naming systems and a redacting pseudonym type, in nl-generic-functions.
The server’s container imagePlanned, pinned by digest, with a compose quickstart beside PostgreSQL.
The operator console and its imagethe data steward’s tool: search, the patient record, the review queue, merge and unmerge, the audit trailPlanned, in the milestone after the server, on the FerroHEALTH component library.

The BSNk polymorphic pseudonyms are in the scope of nl-generic-functions and are not built: the crate does not call the BSNk pseudonym service today.

The server, as planned

The v0.0.3 milestone names what the server will do. Until it lands, none of this runs, and this book describes it only as planned:

  • The PIXm Manager: ITI-83 by GET and by POST with a Parameters body, FerroHEALTH clients calling it by POST so a BSN never appears in a URL, and no URL query in a request log (FerroHEALTH/FerroPIX#52), with several targetSystem domains in one query, and the ITI-104 identity feed.
  • The PDQm Supplier: ITI-78 and the ITI-119 match.
  • The PMIR Registry: ITI-93 and ITI-94.
  • The cross-reference: identifier domains, one Patient Identity Source per domain, linking and merge with their full history, persisted in PostgreSQL.
  • Pseudonyms: a UUID per patient and pseudonymisation domain, for FerroEHR’s subject namespaces.
  • HL7 v2: the PIX and PDQ transactions on the HL7 v2 parsing FerroBRIDGE publishes, so the family has one parser.
  • Audit: the Manager side’s BALP records through ihe-atna, the audit crate of FerroAUDIT.
  • The server’s container image, published and pinned by digest, with a compose quickstart beside PostgreSQL; the operator console and its own image follow in the milestone after it.

What FerroPIX is not

These are the limits of its scope, for the crates today and for the server as the v0.0.3 milestone plans it.

  • It holds no clinical data. Compositions, observations and documents stay in the clinical repository; FerroPIX holds identifiers, the demographics a match needs, and the links between them.
  • It is no audit repository. The audit trail is the ihe-atna crate, and the records go to the ATNA repository you run.
  • It is no authorization server. Access tokens come from the issuer you run. The crates take a token through an authorizer you supply, and the server takes its authorization from ferrohealth-smart, the family’s SMART and OAuth 2.0 crate.

Conformance and specification pins

The specifications are the authority for every transaction, parameter, status and acknowledgment FerroPIX handles: the IHE ITI Technical Framework (Volumes 1 and 2), the IHE PIXm, PDQm and PMIR implementation guides, HL7 FHIR R4, and the HL7 v2 PIX and PDQ transactions. Where none of them governs a decision, the code says so and names the decision as FerroPIX’s own design.

The specification versions

These are the versions the crates implement. The table is read from the repository’s pin matrix, docs/VERSIONS.md, when the book is built, so it cannot disagree with it.

ItemVersionTransactions
IHE PIXmihe.iti.pixm 3.1.0ITI-83 Mobile Patient Identifier Cross-reference Query, ITI-104 Patient Identity Feed FHIR
IHE PDQmihe.iti.pdqm 3.2.0ITI-78 Mobile Patient Demographics Query, ITI-119 Patient Demographics Match
IHE PMIRihe.iti.pmir 1.6.0ITI-93 Mobile Patient Identity Feed, ITI-94 Subscribe to Patient Updates
IHE mCSDihe.iti.mcsd 4.0.0ITI-90, ITI-91 (a feature of ihe-iti)
IHE XCPDITI Technical Framework Vol 2 Rev 20.1 (2024-12-12), no FHIR packageITI-55 (a feature of ihe-iti)
IHE IUAITI Technical Framework Supplement Rev 2.5 (2026-06-18)ITI-71, ITI-72 (a feature of ihe-iti)
Netherlands Generic Functionsfhir.nl.gf 0.3.0the functions of nl-generic-functions
IHE ITI Technical FrameworkVolumes 1 and 2, the revision the move pinsPIX and PDQ: ITI-8, ITI-9, ITI-10, ITI-21 and ITI-22, and ITI-44 to ITI-47 as the TF-2 texts decide, with the actors and options of each
HL7 FHIRR4, 4.0.1the resources and the RESTful API PIXm and PDQm are profiled on
HL7 v2the version each PIX or PDQ transaction names in ITI TF-2the HL7 v2 PIX and PDQ transactions, where in scope

The toolchain

ItemPinRepeated in
Rust toolchain1.99.1rust-toolchain.toml
Minimum supported Rust version1.98Cargo.toml [workspace.package] rust-version
Edition2024Cargo.toml [workspace.package] edition, rustfmt.toml

The minimum supported Rust version is one minor release below the toolchain the repository builds with, the FerroHEALTH family’s rule.

How conformance is held

  • Every citation is checked against the vendored text. The specification texts the crates read are vendored under docs/specs/ at pinned versions, each with a PROVENANCE.md, and the ihe-citations check in CI holds every IHE section the tree cites to the vendored text of that profile and version.
  • Each answer is held to its profile. The clients refuse an answer that departs from the transaction: a PIXm $ihe-pix answer is held to its OperationDefinition, a PDQm search to the Query Patient Resource Response Message profile, an ITI-93 feed message to the PMIR profiles. The crate pages say what each client checks.
  • An unknown patient is never an outage. A 404 reads as “patient unknown” only when its OperationOutcome carries a not-found issue, as PIXm and PDQm define it; every other failure is a typed error.
  • Each feature builds and tests on its own. CI lints and tests every feature of both crates alone and with its audit feature, as well as all of them together.

There is no conformance statement for the server yet: it is part of the v0.0.3 milestone, which is planned.

The published texts

Licensing

FerroPIX, its two published crates and this book are source-available under the Business Source License 1.1. The parameters that apply (the Licensor, the Licensed Work, the Additional Use Grant and the Change Date) are in the repository’s LICENSE:

  • free to read, build, modify and redistribute;
  • free for every non-production use and for non-commercial production use;
  • a commercial licence for any other production use;
  • Apache License 2.0 four years after each version is published.

Every FerroHEALTH product shares these terms, and the family book’s Licensing and trademarks page explains them once for all of them, with what counts as a hosted service and the trademarks.

A commercial licence is arranged with Cadasto B.V., the Licensor: info@cadasto.com or https://www.cadasto.com/contact/. Technical questions go to the maintainer named in MAINTAINERS.md.

The crates

ihe-iti and nl-generic-functions each ship the licence in their package. A product that depends on them takes them under the same terms.

Vendored material

The specification texts under docs/specs/ keep their upstream terms, each recorded in the PROVENANCE.md beside the text. A text whose licence keeps it out of a public repository is fetched at build time and never committed.

Contributions

A contribution is licensed under the same terms, and grants the Licensor the relicensing right in CONTRIBUTING.md. The pull request’s checkbox records it. There is no separate agreement to sign.

Getting started

Today you use FerroPIX through its published crates: you call a PIXm Manager, a PDQm Supplier, a PMIR Registry, an mCSD directory or an XCPD gateway that someone else runs, from your own Rust code. The FerroPIX server is planned in the v0.0.3 milestone; its quickstart joins this page when it lands.

Add the crate

Each profile is a feature, and no feature is on by default, so you compile only the profiles you use. The FerroHEALTH products pin family crates by exact version:

[dependencies]
ihe-iti = { version = "=0.0.44", default-features = false, features = ["pixm"] }
ihe-atna = { version = "=0.2.4", default-features = false }

ihe-atna is the family’s audit crate. The clients name whom an exchange is made for with its ihe_atna::user::OnBehalfOf, so you depend on the same version ihe-iti does. The versions above are the ones on main when this book was built; crates.io lists the published releases.

The example below also uses reqwest, secrecy, url and tokio:

cargo add reqwest secrecy url
cargo add tokio --features macros,rt-multi-thread

Ask a PIXm Manager for a patient’s other identifiers

This is the example of the ihe_iti::pixm module documentation, which CI compiles with every change, with one line added: the client posts its parameters, as every FerroHEALTH client does, so the identifier is in no URL (why). It asks a PIX Manager which identifier one other domain holds for the patient a source identifier names (ITI-83):

use std::time::Duration;

use ihe_iti::pixm::{Invocation, PixmClient};
use ihe_iti::pixm::identifier::{CrossReference, SourceIdentifier, TargetSystem};
use ihe_atna::user::OnBehalfOf;
use secrecy::{ExposeSecret, SecretString};
use url::Url;

async fn run() -> Result<(), Box<dyn std::error::Error>> {
    let http = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .build()?;
    let client = PixmClient::new(Url::parse("https://pix.example.org/fhir/")?, http)?
        .invoked_by(Invocation::Post);
    let source = SourceIdentifier::new(
        "urn:oid:1.3.6.1.4.1.21367.13.20.1000",
        SecretString::from("IHERED-994"),
    )?;
    let targets = [TargetSystem::new("urn:oid:1.3.6.1.4.1.21367.13.20.3000")?];
    let on_behalf = OnBehalfOf::System;
    match client
        .cross_reference(&source, &targets, &on_behalf, Duration::from_secs(2))
        .await?
    {
        CrossReference::Matched(found) => {
            for identifier in found.identifiers() {
                let _value = identifier.value().expose_secret();
            }
        }
        CrossReference::SourceNotFound => {}
        _ => {}
    }
    Ok(())
}

Three things in it hold for every client of the crate:

  • Build the HTTP client with redirect::Policy::none(). The request carries the patient identifier, and a redirect would send it to a host you did not choose.
  • Identifiers travel as secrecy::SecretString. The types that carry them redact their value in Debug and have no Display, and no error carries a value, a URL or the server’s free text, so a log line cannot leak one by accident. You read a value with expose_secret() where you mean to.
  • An unknown patient is an answer, an outage is an error. CrossReference::SourceNotFound is the profile’s “patient unknown”; a timeout, a refusal or a 404 without a not-found issue is a typed PixmError.

Next

  • The published crates: every feature of ihe-iti and nl-generic-functions, what each client checks, and the API reference on docs.rs.
  • Security and audit: access tokens on each request, and the audit record of each exchange.

The published crates

FerroPIX publishes the libraries of the identity specifications it owns, so every FerroHEALTH product, and anyone else, can use them. Each crate carries the name of the specification it implements and holds one feature per profile or function, with no feature on by default.

CrateWhat it implementsVersion on mainAPI reference
ihe-itiThe IHE ITI identity profiles: PIXm, PDQm, PMIR, mCSD and XCPD, with the IUA authorizer and each profile’s audit record0.0.44docs.rs/ihe-iti
nl-generic-functionsThe Netherlands Generic Functions: NVI localization, Mitz consent, LRZa addressing and Nuts authentication0.0.26docs.rs/nl-generic-functions

Both crates depend on no application: they are the specifications’ transactions as Rust, for any caller. The FerroPIX server, when it lands, adds the server sides of the profiles as features of ihe-iti.

Versions

Each crate keeps its own version line, apart from the FerroPIX product version (0.0.2 on main). A change to what a crate ships moves that crate’s version in the same pull request, and a version is never reused. Not every version is published, so the published sequence has gaps; crates.io lists what is published: ihe-iti, nl-generic-functions. The changes of each version are in the changelog.

The crates are published from this repository through crates.io Trusted Publishing, so a release carries no long-lived token.

ihe-iti

The IHE IT Infrastructure (ITI) identity profiles in Rust: one crate for the technical framework, with a feature per profile. The API reference is on docs.rs/ihe-iti; this page says what each feature does and what it checks. The version on main is 0.0.44.

Features

FeatureProfileTransactionsActor
pixmPatient Identifier Cross-reference for Mobile, PIXmITI-83Patient Identifier Cross-reference Consumer
pdqmPatient Demographics Query for Mobile, PDQmITI-78, ITI-119Patient Demographics Consumer
pmirPatient Master Identity Registry, PMIRITI-93, ITI-94Patient Identity Subscriber and Consumer
mcsdMobile Care Services Discovery, mCSDITI-90, ITI-91Query Client and Update Client
xcpdCross-Community Patient Discovery, XCPDITI-55Initiating Gateway
balpThe BALP AuditEvent of each FHIR profile aboveITI-83, ITI-78, ITI-119, ITI-90, ITI-91, ITI-93, ITI-94the audit record of the client side
atnaThe DICOM audit message of xcpd, with no FHIR modelITI-55the audit record of the Initiating Gateway

Only xcpd carries the SOAP 1.2, HL7 v3 and SAML XUA stack; a build without it compiles none of it. The FHIR features decode FHIR R4 through fhir-types, the model FerroBRIDGE generates from the HL7 packages.

PIXm (pixm)

ihe_iti::pixm::PixmClient asks a PIX Manager for the identifiers other domains hold for a patient (ITI-83, PIXm 3.1.0): GET [base]/Patient/$ihe-pix with one sourceIdentifier and any number of targetSystem domains. Built with .invoked_by(Invocation::Post), it posts the same parameters in a Parameters body to [base]/Patient/$ihe-pix instead (FHIR R4 Operations §3.2.0.1), so the source identifier is in no request URL.

Lookups by a patient identifier use POST

ITI-83 prescribes the GET, which puts the patient identifier, a BSN for a Dutch patient, in the query string. FerroHEALTH therefore settles it this way:

  • The FerroPIX PIX Manager, planned, is to accept $ihe-pix by GET (ITI-83) and by POST with a Parameters body (FHIR R4 Operations §3.2.0.1), and to keep no URL query in a request log (FerroHEALTH/FerroPIX#52).
  • FerroHEALTH clients call it by POST (ihe_iti::pixm::Invocation::Post), so a BSN never appears in a URL. Against another Manager, post only when it accepts the POST form.
  • A proxy in front of a Manager must not log query strings, since a client that uses the GET puts the identifier there.

Every answer is held to the $ihe-pix OperationDefinition: the two out parameters, an assigning authority on each identifier, identifiers only from the domains asked about, and never the source identifier itself. A 404 is “patient unknown” only when its OperationOutcome carries a not-found issue.

PDQm (pdqm)

ihe_iti::pdqm::PdqmClient is the Patient Demographics Consumer:

  • ITI-78, the demographics query: a POST [base]/Patient/_search with the criteria of a PatientQuery (every Patient search parameter ITI-78 names), read into a page of matching Patients with the total, each match’s score and grade, and the next link, which next_page follows on the Supplier’s own origin only.
  • ITI-119, the demographics match: match_patient posts a MatchInput to [base]/Patient/$match and reads the matched Patients, most likely first, each with its score between 0 and 1 and its match-grade.

The criteria travel in the request body, never the URL, so a PDQm search needs no choice of invocation: like a PIXm lookup from a FerroHEALTH client, it keeps every identifier and demographic out of the URL. The planned PDQm Supplier of FerroPIX answers the same POST (FerroHEALTH/FerroPIX#52 covers its request log). An answer that departs from the profile, such as a match entry without its score or grade, is refused.

PMIR (pmir)

The Patient Identity Subscriber and Consumer sides of PMIR 1.6.0:

  • ITI-94, Subscribe to Patient Updates: ihe_iti::pmir::PmirSubscriber creates a Subscription at the Patient Identity Registry, reads its status and deletes it.
  • ITI-93, Mobile Patient Identity Feed: ihe_iti::pmir::feed::Feed::read reads a message the Registry sends to the subscription’s channel into typed creates, updates, deletes and merges, and acknowledgement writes the response. The feed keeps the resource ids and the identifiers and drops every demographic as it reads.

The specification leaves the authentication of the Registry to your channel to the two parties, so your code authenticates each message before it reads it.

mCSD (mcsd)

  • ihe_iti::mcsd::directory::Directory reads the Organization and Endpoint resources of one directory Bundle and resolves the references between them as FHIR R4 does. A reference it cannot resolve is reported, never guessed.
  • ihe_iti::mcsd::client::McsdClient asks a directory over HTTP: find is ITI-90 and updates is ITI-91. Every walk over the pages draws on one budget of time, pages, bytes and entries, and running out of any of them is an error, never a shorter answer.
  • ihe_iti::mcsd::replica::Replica keeps a copy of a directory in step with ITI-90 and ITI-91.

XCPD (xcpd)

ihe_iti::xcpd::XcpdClient is the Initiating Gateway of ITI-55 (ITI TF-2 §3.55, Revision 20.1): a PRPA_IN201305UV02 in a SOAP 1.2 envelope, posted to one Responding Gateway, asking by the shared patient identifier alone. The answer reads into a Discovery for Cases 1 to 4 of the transaction, each match with the community that holds it, or a typed error. A Responding Gateway is https only; the reqwest::Client you pass in carries the mutual TLS of the ATNA Secure Node.

The crate signs nothing. You supply a SAML 2.0 assertion your identity provider signed, and the client sends it unchanged in a WS-Security header.

Whom an exchange is made for

PixmClient::cross_reference, the PdqmClient searches and matches, and XcpdClient::discover each take an ihe_atna::user::OnBehalfOf: the User an OAuth 2.0 access token names, or System for an exchange the client’s own system makes. An audited exchange’s record names that user. The mCSD and PMIR exchanges are always the system’s own.

Access tokens and audit

Each client can ask an authorizer you supply for the headers of every request (IUA ITI-72), and with balp or atna records the audit record its profile defines. Security and audit describes both.

nl-generic-functions

The Netherlands Generic Functions for health data exchange (the Nuts nl-generic-functions-ig, package fhir.nl.gf 0.3.0) in Rust: one crate for the implementation guide, with a feature per function. Identification and localization are a patient index’s own functions in the Netherlands, which is why the crate lives in FerroPIX. The API reference is on docs.rs/nl-generic-functions. The version on main is 0.0.26.

Features

FeatureGeneric FunctionServiceWhat the crate holds
nviGF-Localizationthe national index (NVI)the Localization Service search: which care providers (by URA) hold data for a patient (by pseudonymised BSN)
mitzGF-ConsentMitzthe closed authorization question: may a data holder make a patient’s data of each category available to a data user, answered Permit or Deny
lrzaGF-Addressingthe national address book (LRZa)the URA of an NL-GF Organization
nuts-authGF-Authenticationthe Nuts profilethe access token request of Nuts RFC021, with a DPoP-bound token, and the holder’s did:web DID document
authorizerGF-Authenticationthe trait a data user implements to give each request its own headers (the IG’s GFI-005), with no FHIR model; nvi turns it on

Identifiers: BSN and the pseudonymised BSN

The identification module, built with nvi, lrza or mitz, holds the identifier systems the functions share. A patient is named by a pseudonymised BSN, never by the BSN itself, and a care provider by its URA. A pseudonymised BSN is personal data like the BSN it stands for: its value is a secrecy::SecretString, its Debug is redacted and it has no Display.

The BSNk polymorphic pseudonyms are not built: the crate does not call the BSNk pseudonym service.

NVI (nvi)

The NVI client asks the Localization Service which care providers hold data for a patient. It never puts the pseudonym in an error, a Debug rendering or anything but the request to the service, follows a next link only on its own search endpoint, and hands an authorizer the request URL without its query, so the pseudonym never reaches it.

Mitz (mitz)

The Mitz client follows the VZVZ Implementatiehandleiding Open en gesloten autorisatievraag 3.8.2: a SOAP 1.2 request carrying one XACML 3.0 XACMLAuthzDecisionQuery, over mutual TLS. Only Permit and Deny are decisions; Indeterminate, NotApplicable, a fault, a timeout and an answer that does not hold to the question are typed errors. The BSN travels only in the request to Mitz, and the client follows no redirect.

Nuts (nuts-auth)

The Nuts client requests an access token with a Verifiable Presentation of the holder’s credentials as the grant (Nuts RFC021) and a DPoP-bound token. It sends no credential before the authorization server’s Presentation Definition says they answer it, and never puts a credential, the presentation, a key, a proof or the token in an error or a Debug rendering. The authorization server’s issuer is an https URL, or plain http on a loopback host for a test, under the RFC 8414 rules of ferrohealth-smart.

Configuration

There is no FerroPIX server to configure yet. The server, with its configuration file and environment variables, is planned in the v0.0.3 milestone, and its reference replaces this page’s first section when it lands.

What you configure today is the crates, in your own code: which features you compile, and how you build each client.

Features

Every profile and function is a feature, and none is on by default.

CrateFeaturesPages
ihe-itipixm, pdqm, pmir, mcsd, xcpd, and the audit records balp (with a FHIR profile) and atna (with xcpd)ihe-iti
nl-generic-functionsnvi, mitz, lrza, nuts-auth, and authorizernl-generic-functions

Building a client

The settings below hold for every HTTP client of both crates. The API reference on docs.rs has each constructor and builder method.

SettingWhereWhat to give it
The server’s base URLthe constructor, as a url::UrlThe FHIR base of the Manager, Supplier, Registry or directory. Its userinfo, if any, is shown as *** in Debug.
The HTTP clientthe constructor, as a reqwest::ClientBuilt with redirect::Policy::none(), and with the mutual TLS identity where the profile asks for one (XCPD, Mitz).
A timeout per exchangeeach call, as a DurationThe bound of the whole exchange; with an audit feature it bounds the audit record as well.
Access tokens.with_authorizer(…)An authorizer that makes the headers of each request (IUA ITI-72); see Security and audit.
The audit recorder.audited(…), with balp or atnaThe recorder that routes each record to your audit repository; see Security and audit.
The PIXm invocation.invoked_by(Invocation::Post)POST, as every FerroHEALTH client calls a Manager, so the identifier stays out of the URL; leave the GET default only for a Manager that refuses the POST form (Lookups use POST).
The mCSD budgetbudget::BudgetThe deadline and the caps on pages, bytes and entries of one walk; by default 200 pages, 64 MiB and 50,000 entries.

Deployment

FerroPIX publishes no binary and no container image yet. The server, on PostgreSQL, its container image pinned by digest and a compose quickstart that runs it beside PostgreSQL are planned in the v0.0.3 milestone. A second image, the operator console, and a Helm chart are planned for the milestone after it.

What deploys today is the two crates, inside your own application: they build with the Rust toolchain your application uses, at or above the minimum supported Rust version in Conformance and specification pins, and need nothing at run time beyond the servers they call.

What the server is planned to run beside

As the milestone plans it, a FerroPIX deployment sits beside:

  • the clinical systems that ask it, FerroEHR among them, which is planned to resolve its ehr_id through PIXm and PDQm (FerroHEALTH/FerroEHR#3380);
  • a PostgreSQL 18 database that holds the cross-reference;
  • an authorization server whose access tokens it reads;
  • an ATNA audit repository that receives its BALP records;
  • the operator console, its own image, which reads the server’s operator API with a signed-in steward’s token.

This page gains the image, its ports, its verification and its configuration when the server lands.

Security and audit

A patient index handles the most identifying data in a health system, so both crates are built to keep it out of anywhere it was not sent. This page says what they guarantee today. The server’s own access control and audit are planned in the v0.0.3 milestone.

No identifying data where it was not sent

  • Every identifier, every demographic criterion, a pseudonymised BSN, a SAML assertion and a token travels as a secrecy::SecretString or in a type that redacts its content in Debug and has no Display.
  • No error carries a value, a request URL or body, or the free text the other server wrote, so an error you log cannot leak one.
  • A client’s Debug shows its URLs with the userinfo replaced by *** and leaves out the reqwest::Client, whose default headers may hold a credential.
  • Build each reqwest::Client you pass in with redirect::Policy::none(): the request carries the identifier, and a redirect would send it on.
  • Lookups by a patient identifier use POST. A PDQm query sends its criteria in the request body. FerroHEALTH clients call a PIXm Manager by POST with a Parameters body (ihe_iti::pixm::Invocation::Post, FHIR R4 Operations §3.2.0.1), so a BSN never appears in a URL. FerroPIX’s planned PIX Manager is to accept $ihe-pix by GET (ITI-83) and by POST, and to keep no URL query in a request log (FerroHEALTH/FerroPIX#52). A proxy in front of any Manager must not log query strings, because a client that uses the GET puts the identifier there.
  • The PMIR feed drops every demographic as it reads an ITI-93 message.

Access tokens

The PIXm, PDQm, mCSD and PMIR clients send the credential their reqwest::Client carries in its default headers or, built .with_authorizer(…), ask an ihe_iti::authorizer::Authorizer for the headers of every request: an OAuth 2.0 access token incorporated as IUA ITI-72 §3.72.4.2 asks. After a 401 the request is sent once more with new headers (§3.72.4.3), never more than twice in all. The authorizer is handed the request URL without its query, so no identifier or search criterion reaches it, and a request it cannot authenticate is not sent. nl-generic-functions has the same seam for the Generic Functions, behind its authorizer feature.

Audit

The audit trail is ihe-atna, the family’s audit crate from FerroAUDIT: ATNA ITI-20 with its syslog sender, spool and forwarder, and the BALP FHIR feed. ihe-iti writes the record each profile defines for its own transactions on it:

  • balp: every PIXm, PDQm, mCSD and PMIR client takes an ihe_atna::balp::AuditRecorder through .audited(…) and records the BALP AuditEvent of each transaction.
  • atna: XcpdClient::audited hands the ITI-55 Initiating Gateway audit message of every exchange to a recorder, written as the DICOM audit message in ihe-atna’s ATNA model.

The client waits for the recorder before it returns the answer, the exchange’s timeout bounds the record too, and a record the recorder refuses fails the exchange as an audit failure, so no answer is used without its audit. An audited exchange’s record names the user it was made for.

Reporting a vulnerability

Report a vulnerability privately through GitHub’s private vulnerability reporting, never in a public issue. The supported versions and the response times are in SECURITY.md, and the manufacturer’s vulnerability handling, disclosure and advisories are in the family book’s security page.

Operations

The server’s health checks, metrics, logs and upgrade path are planned with the server in the v0.0.3 milestone. This page covers what operating the crates involves today.

Releases and support

  • Only the newest release is supported. When a release is published, every earlier one stops receiving fixes: there are no maintenance branches and no backports. Security fixes keep being published for at least five years after each release, in the newest release (SECURITY.md).
  • A FerroPIX release is cut from its milestone. The release workflow refuses a tag whose milestone still holds an open issue, and closes the milestone once every leg has published. The milestones are the roadmap.
  • The crates are published between releases. A crate’s version moves with any change to what it ships, so a fix reaches crates.io without waiting for a product release.

Upgrading a crate

Read the crate’s entries in the changelog for the versions you skip. A change that breaks the API is marked API break with what to change. The family pins its crates by exact version, so ihe-iti moves together with the ihe-atna version it names.

Reporting a defect

Open an issue when a transaction answers differently from what its specification names, or identifying data reaches a log line. Never paste patient data or a real identifier. The family’s post-market procedure says how complaints and incidents are handled.

Contributing

The tracker is the worklist: every change answers an issue of FerroHEALTH/FerroPIX, and each milestone is a release. Read CONTRIBUTING.md before a first change; this page is the short form.

Build and test

cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo nextest run --workspace --all-features --locked
cargo test --doc --workspace --all-features --locked
cargo deny check

A change to a workflow or a shell script also runs the linters CI runs (zizmor, actionlint, shellcheck --severity=style) and the --self-test of every script it touches. To build this book and the landing page as the site serves them, install the documentation toolchain in the pin matrix and run:

scripts/site/assemble.sh _site
scripts/checks/site-links.sh

What a change carries

  • The specification it follows. The IHE ITI Technical Framework, the PIXm, PDQm and PMIR guides, HL7 FHIR R4 and HL7 v2 decide every transaction. A change cites the section, and where none governs a choice it says so.
  • A changelog entry under [Unreleased] in CHANGELOG.md for every change a user can see.
  • The book page it changes, in the same pull request, or the no-book label when it alters nothing the book says (Keeping this book current).
  • A crate version bump when it changes what a published crate ships.
  • Signed commits on a conventional branch (feat/, fix/, docs/ and the rest), merged through the merge queue. Commit messages describe only the change.

Licensing of contributions

A contribution is licensed under the Business Source License 1.1 with the rest of the repository, and grants the Licensor the relicensing right in CONTRIBUTING.md. The pull request template’s checkbox records it.

Keeping this book current

This book describes only what the code does, so it changes with the code.

  • A change a user can see updates its book page in the same pull request, as it adds its changelog entry: a crate feature, a client’s behaviour, a specification pin, and, once the server lands, its configuration, its deployment files and its endpoints.
  • Work that is not built yet links its issue or milestone on the tracker, and a page never describes it as present.
  • Versions are rendered, never typed. The crate and product versions on these pages and the specification and toolchain tables come from the manifests and the pin matrix when the site is assembled, so they cannot go stale.
  • Every internal link and anchor resolves. CI assembles the site the way GitHub Pages serves it and checks every link offline on each pull request.

Four checks in CI hold this, each inside the required conclusion:

CheckWhat it refuses
scripts/checks/book-guard.shA pull request that changes a crate’s source or features, the server or a deployment file without touching the book. The no-book label lets through a change that alters nothing the book says, such as a refactor.
scripts/checks/docs-claims.shA published crate without its page, a feature without its row or a row without its feature, a typed crate version, and a section that names unbuilt work without linking the tracker.
scripts/checks/site-links.shA broken internal link or anchor anywhere in the assembled site or README, and an mdbook-lint warning.
scripts/checks/writing-style.shAn em dash, a word from the banned list, or AI attribution in the published prose.

What a check can catch is limited: a link check passes on a page whose every claim is false. The rest is review, which is why the same-pull-request rule carries the weight.

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • docs/architecture.md, the design of record for the server and the operator console: the process and its configuration, the PostgreSQL cross-reference store with its history and transaction boundaries, the matching in three tiers with the steward in the middle, the PIXm, PDQm and PMIR faces and the HL7 v2 listener, pseudonyms, authorization and audit, the two images released at SLSA Build Level 3, the CI lanes, and the console’s information architecture, screens, wireframes and FerroUI component inventory. CLAUDE.md and the book name the console, the PostgreSQL store and the two images as planned, by milestone.
  • The documentation site at https://ferropix.eu/: a landing page in the FerroHEALTH design with the FerroPIX brand, and the book under /docs/ (what FerroPIX is and what is built or planned, getting started, the published crates with every feature, configuration, deployment, security and audit, operations, the specification pins, contributing and this changelog), built and link-checked on every pull request and deployed from main by the Docs workflow. The brand gains a favicon set and a mark for a dark ground (#45, #46, #47, #49).
  • The guards that keep the book in step with the code, in CI’s required conclusion: book-guard.sh refuses a change to a crate’s source or features, the server or a deployment file that does not update the book (the no-book label is the escape), docs-claims.sh holds each crate’s Features table to its Cargo.toml, refuses a typed crate version and a planned item without its tracker link, and writing-style.sh refuses an em dash, a banned word or AI attribution in the published prose. Each has a --self-test (#48).

Changed

  • The repository builds with Rust 1.99.1 in place of 1.98.1, and the MSRV stays at 1.98: the FerroHEALTH family builds with the latest stable toolchain and declares the release before it as its MSRV. No published crate changes.
  • ihe-iti 0.0.40 splits the audit feature in two: balp, the BALP AuditEvent of the PIXm, PDQm, mCSD and PMIR transactions on ihe-atna/balp, and atna, the ITI-55 audit message of xcpd on ihe-atna/atna, which compiles no FHIR model. A product that enabled audit enables balp, atna or both. Published between releases (#32).
  • ihe-iti 0.0.41 takes ihe-atna = "=0.2.1" and drops its own copies of the record bound, the URL redaction and the FHIR base join: they are ihe-atna’s balp::recording, redact and fhir_base (FerroHEALTH/FerroAUDIT#8, #9, #61), taken through ihe-atna’s recording and redact features, so a profile without balp or atna takes no audit stack. API breaks: ihe_iti::recording is gone (use ihe_atna::balp::recording, whose Late is made with Late::new()); the EventKind constants of the audit modules are functions, since ihe-atna 0.2’s EventKind holds a Vec (pixm::audit::query_consumer(), pdqm::audit::query_consumer() and match_consumer(), mcsd::audit::query() and updates(), pmir::audit::feed_kind(), subscription_create(), subscription_read(), subscription_delete() and subscription_search()). A product that implements ihe_atna::balp::AuditRecorder for ihe-iti’s clients moves to ihe-atna = "=0.2.1" with it (#35).
  • nl-generic-functions 0.0.26 takes ferrohealth-smart = "=0.4.0", so a product resolves one copy of it. The three checks the Nuts grant adds to the RFC 8414 issuer rule (the exact canonical issuer text, every endpoint on the issuer’s origin, and a refusal of JSON that repeats a name) stay private in nuts_auth, as 0.4.0 does not carry them (#36).
  • ihe-iti 0.0.42 takes ihe-atna = "=0.2.2", which changes only the declared MSRV, raising it to 1.98; a product that takes ihe-iti moves to ihe-atna =0.2.2 with it (#39).
  • ihe-iti 0.0.43 takes ihe-atna = "=0.2.3", the only supported release; 0.0.42 was not published (#41).
  • ihe-iti 0.0.44 takes ihe-atna = "=0.2.4", the only supported release (#43).

[0.0.2] - 2026-10-20

The move from FerroFED (FerroHEALTH/FerroFED#367): ihe-iti 0.0.39 and nl-generic-functions 0.0.25 are published from FerroPIX, each copied with its history and with the corpora it reads re-vendored at FerroFED’s pins. FerroFED pins both by exact version from crates.io.

Added

  • ihe-iti 0.0.39, moved from FerroFED with its history: the IHE PIXm, PDQm, PMIR, mCSD and XCPD clients and the IUA authorizer, a feature each. The atna and balp features are gone: the audit trail is the ihe-atna crate of FerroAUDIT. The audit record each profile defines for its own transactions stays here behind the new audit feature, on ihe-atna =0.1.0. A product changes ihe_iti::user to ihe_atna::user, ihe_iti::balp and ihe_iti::atna to ihe_atna::balp and ihe_atna::atna, and the features balp and atna to audit (#25).

  • nl-generic-functions 0.0.25, moved from FerroFED with its history: NVI localization, the Mitz closed authorization question, LRZa addressing and the Nuts access token request, a feature each. nuts-auth no longer depends on oauth-server-metadata, which is retired: it takes the RFC 8414 issuer rule and metadata location from ferrohealth-smart =0.3.1. A plain http issuer is now accepted on a loopback host only (#26).

  • The IHE citation guard, scripts/checks/ihe-citations.sh, and the corpora its audit citations read at FerroFED’s pins: the BALP package and narrative pages and the ITI-20 page with the RESTful ATNA supplement (#25).

  • The corpora ihe-iti and nl-generic-functions read, vendored under docs/specs/ by FerroFED’s scripts at FerroFED’s pins and byte-identical to FerroFED’s copies: the IHE PIXm, PDQm, PMIR and mCSD packages and narrative pages, the ITI Technical Framework pages, the IUA supplement, the Swiss pins of PIXm, PDQm and IUA, the Netherlands Generic Functions IG source, the Mitz documents (cache only), the Nuts RFCs, the IETF OAuth RFCs, the W3C DID and VC texts and DIF Presentation Exchange, each with its scripts/vendor/*.sh, its PROVENANCE.md and its row in docs/VERSIONS.md (#24).

Changed

  • The minimum supported Rust version is 1.98, the version ihe-atna requires (#25).
  • Two version lines, the model FerroBRIDGE uses: the product version is the workspace version and the release tag, and each published crate keeps its own version, which never adopts the product version. The crate-version guard holds both, and refuses a change to a crate’s packaged content that does not bump it unless the pull request carries no-crate-bump. The release lane names each crate’s files by the crate’s own version (#23).

[0.0.1] - 2026-10-20

The repository setup: the working discipline, the tracker helpers and the guards, CI and the release lane, the workspace skeleton and the GitHub settings. No crate is published in this release; the first crates arrive with the move from FerroFED in 0.0.2.

Added

  • The tracker helpers under scripts/gh/ (fields.sh, rel.sh, labels.sh, watch-prs.sh, changelog-merge.sh, and project.sh, which says FerroPIX has no roadmap board) and the guards under scripts/checks/ (comment style, the pin matrix and licence headers, file length, TODO pointers, the crate version, the changelog and the contribution licence), each with a --self-test (#11).
  • CI with the single required conclusion check, CodeQL, OpenSSF Scorecard, Dependabot, the release lane that refuses a tag whose milestone is not empty and closes it after the release, the crates.io lane through Trusted Publishing, and the pull request and issue templates (#12).
  • docs/VERSIONS.md, the pin matrix the guards read (#11).
  • The working discipline: CLAUDE.md with the scope the owner settled on 2026-10-20, the rules, the spec-researcher and implementer agents, the spec-lookup, next-task, phase-done and phase-status skills, the hooks (the destructive-command hook with its self-test) and the memory (#13).
  • The specification versions of every identity profile that moves from FerroFED in docs/VERSIONS.md (#13).
  • SUPPORT.md, GOVERNANCE.md and CODE_OF_CONDUCT.md, and a README and CONTRIBUTING.md that name what FerroPIX is, how to build and test it and the specifications that govern it (#14).
  • The owner’s rule for moving the vendored corpora with the crates, in .claude/rules/vendored-inputs.md and the memory (#20).
  • The Cargo workspace skeleton: the root manifest with the family lint set and the release profile, the pinned toolchain, and the cargo-deny, clippy and rustfmt configuration. The workspace has no member until the move from FerroFED; CI reads the manifest under the pinned toolchain meanwhile (#15).

Changed

  • SECURITY.md supports only the newest release, with the Cyber Resilience Act commitment to publish security fixes for at least five years after each release (#14).