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) andnl-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.
| Capability | Transactions | Status |
|---|---|---|
| PIXm, the identifier cross-reference query | ITI-83 by GET, or by POST of a Parameters body | Client, in ihe-iti (pixm). The PIXm Manager is planned. |
| PIXm, the FHIR identity feed | ITI-104 | Planned, with the PIXm Manager. |
| PDQm, the demographics query and match | ITI-78, ITI-119 | Client, in ihe-iti (pdqm). The PDQm Supplier is planned. |
| PMIR, the master identity feed and subscription | ITI-93, ITI-94 | Subscriber and feed Consumer, in ihe-iti (pmir). The PMIR Registry is planned. |
| mCSD, the care services directory | ITI-90, ITI-91 | Client and replica, in ihe-iti (mcsd). |
| XCPD, cross-community patient discovery | ITI-55 | Initiating Gateway, in ihe-iti (xcpd). |
| IUA, access tokens on each request | ITI-72 | The authorizer seam of the ihe-iti clients. |
| The persisted cross-reference | identifier domains, one source per domain, linking and merge | Planned. |
| Pseudonym minting | a UUID per patient and pseudonymisation domain | Planned. |
| HL7 v2 PIX and PDQ | ITI-8, ITI-9, ITI-21, ITI-22, and ITI-44 to ITI-47 as the ITI Technical Framework texts decide | Planned, on FerroBRIDGE’s HL7 v2 parser and MLLP codec. |
| Audit of the Manager side | BALP AuditEvent through ihe-atna | Planned. The clients’ audit records are built (balp, atna). |
| Dutch Generic Functions: localization (NVI) | the NVI Localization Service search | Client, in nl-generic-functions (nvi). |
| Dutch Generic Functions: consent (Mitz) | the closed authorization question | Client, in nl-generic-functions (mitz). |
| Dutch Generic Functions: addressing (LRZa) | the URA of an organisation | In nl-generic-functions (lrza). |
| Dutch Generic Functions: authentication (Nuts) | the Nuts RFC021 access token request | Client, in nl-generic-functions (nuts-auth). |
| BSN and the pseudonymised BSN | the identifier systems the functions share | The naming systems and a redacting pseudonym type, in nl-generic-functions. |
| The server’s container image | Planned, pinned by digest, with a compose quickstart beside PostgreSQL. | |
| The operator console and its image | the data steward’s tool: search, the patient record, the review queue, merge and unmerge, the audit trail | Planned, 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
GETand byPOSTwith aParametersbody, FerroHEALTH clients calling it byPOSTso a BSN never appears in a URL, and no URL query in a request log (FerroHEALTH/FerroPIX#52), with severaltargetSystemdomains 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-atnacrate, 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.
| Item | Version | Transactions |
|---|---|---|
| IHE PIXm | ihe.iti.pixm 3.1.0 | ITI-83 Mobile Patient Identifier Cross-reference Query, ITI-104 Patient Identity Feed FHIR |
| IHE PDQm | ihe.iti.pdqm 3.2.0 | ITI-78 Mobile Patient Demographics Query, ITI-119 Patient Demographics Match |
| IHE PMIR | ihe.iti.pmir 1.6.0 | ITI-93 Mobile Patient Identity Feed, ITI-94 Subscribe to Patient Updates |
| IHE mCSD | ihe.iti.mcsd 4.0.0 | ITI-90, ITI-91 (a feature of ihe-iti) |
| IHE XCPD | ITI Technical Framework Vol 2 Rev 20.1 (2024-12-12), no FHIR package | ITI-55 (a feature of ihe-iti) |
| IHE IUA | ITI Technical Framework Supplement Rev 2.5 (2026-06-18) | ITI-71, ITI-72 (a feature of ihe-iti) |
| Netherlands Generic Functions | fhir.nl.gf 0.3.0 | the functions of nl-generic-functions |
| IHE ITI Technical Framework | Volumes 1 and 2, the revision the move pins | PIX 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 FHIR | R4, 4.0.1 | the resources and the RESTful API PIXm and PDQm are profiled on |
| HL7 v2 | the version each PIX or PDQ transaction names in ITI TF-2 | the HL7 v2 PIX and PDQ transactions, where in scope |
The toolchain
| Item | Pin | Repeated in |
|---|---|---|
| Rust toolchain | 1.99.1 | rust-toolchain.toml |
| Minimum supported Rust version | 1.98 | Cargo.toml [workspace.package] rust-version |
| Edition | 2024 | Cargo.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 aPROVENANCE.md, and theihe-citationscheck 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-pixanswer is held to itsOperationDefinition, 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
404reads as “patient unknown” only when itsOperationOutcomecarries anot-foundissue, 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
- IHE ITI Technical Framework: https://profiles.ihe.net/ITI/TF/index.html
- IHE PIXm: https://profiles.ihe.net/ITI/PIXm/
- IHE PDQm: https://profiles.ihe.net/ITI/PDQm/
- IHE PMIR: https://profiles.ihe.net/ITI/PMIR/
- IHE mCSD: https://profiles.ihe.net/ITI/mCSD/
- HL7 FHIR R4: https://hl7.org/fhir/R4/
- The Netherlands Generic Functions implementation guide: https://build.fhir.org/ig/nuts-foundation/nl-generic-functions-ig/
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 inDebugand have noDisplay, 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 withexpose_secret()where you mean to. - An unknown patient is an answer, an outage is an error.
CrossReference::SourceNotFoundis the profile’s “patient unknown”; a timeout, a refusal or a404without anot-foundissue is a typedPixmError.
Next
- The published crates: every feature of
ihe-itiandnl-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.
| Crate | What it implements | Version on main | API reference |
|---|---|---|---|
ihe-iti | The IHE ITI identity profiles: PIXm, PDQm, PMIR, mCSD and XCPD, with the IUA authorizer and each profile’s audit record | 0.0.44 | docs.rs/ihe-iti |
nl-generic-functions | The Netherlands Generic Functions: NVI localization, Mitz consent, LRZa addressing and Nuts authentication | 0.0.26 | docs.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
| Feature | Profile | Transactions | Actor |
|---|---|---|---|
pixm | Patient Identifier Cross-reference for Mobile, PIXm | ITI-83 | Patient Identifier Cross-reference Consumer |
pdqm | Patient Demographics Query for Mobile, PDQm | ITI-78, ITI-119 | Patient Demographics Consumer |
pmir | Patient Master Identity Registry, PMIR | ITI-93, ITI-94 | Patient Identity Subscriber and Consumer |
mcsd | Mobile Care Services Discovery, mCSD | ITI-90, ITI-91 | Query Client and Update Client |
xcpd | Cross-Community Patient Discovery, XCPD | ITI-55 | Initiating Gateway |
balp | The BALP AuditEvent of each FHIR profile above | ITI-83, ITI-78, ITI-119, ITI-90, ITI-91, ITI-93, ITI-94 | the audit record of the client side |
atna | The DICOM audit message of xcpd, with no FHIR model | ITI-55 | the 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-pixbyGET(ITI-83) and byPOSTwith aParametersbody (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 thePOSTform. - A proxy in front of a Manager must not log query strings, since a
client that uses the
GETputs 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/_searchwith the criteria of aPatientQuery(every Patient search parameter ITI-78 names), read into a page of matching Patients with thetotal, each match’s score and grade, and thenextlink, whichnext_pagefollows on the Supplier’s own origin only. - ITI-119, the demographics match:
match_patientposts aMatchInputto[base]/Patient/$matchand reads the matched Patients, most likely first, each with its score between 0 and 1 and itsmatch-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::PmirSubscribercreates aSubscriptionat the Patient Identity Registry, reads its status and deletes it. - ITI-93, Mobile Patient Identity Feed:
ihe_iti::pmir::feed::Feed::readreads a message the Registry sends to the subscription’s channel into typed creates, updates, deletes and merges, andacknowledgementwrites 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::Directoryreads theOrganizationandEndpointresources 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::McsdClientasks a directory over HTTP:findis ITI-90 andupdatesis 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::Replicakeeps 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
| Feature | Generic Function | Service | What the crate holds |
|---|---|---|---|
nvi | GF-Localization | the national index (NVI) | the Localization Service search: which care providers (by URA) hold data for a patient (by pseudonymised BSN) |
mitz | GF-Consent | Mitz | the closed authorization question: may a data holder make a patient’s data of each category available to a data user, answered Permit or Deny |
lrza | GF-Addressing | the national address book (LRZa) | the URA of an NL-GF Organization |
nuts-auth | GF-Authentication | the Nuts profile | the access token request of Nuts RFC021, with a DPoP-bound token, and the holder’s did:web DID document |
authorizer | GF-Authentication | the 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.
| Crate | Features | Pages |
|---|---|---|
ihe-iti | pixm, pdqm, pmir, mcsd, xcpd, and the audit records balp (with a FHIR profile) and atna (with xcpd) | ihe-iti |
nl-generic-functions | nvi, mitz, lrza, nuts-auth, and authorizer | nl-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.
| Setting | Where | What to give it |
|---|---|---|
| The server’s base URL | the constructor, as a url::Url | The FHIR base of the Manager, Supplier, Registry or directory. Its userinfo, if any, is shown as *** in Debug. |
| The HTTP client | the constructor, as a reqwest::Client | Built with redirect::Policy::none(), and with the mutual TLS identity where the profile asks for one (XCPD, Mitz). |
| A timeout per exchange | each call, as a Duration | The 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 atna | The 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 budget | budget::Budget | The 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_idthrough 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::SecretStringor in a type that redacts its content inDebugand has noDisplay. - 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
Debugshows its URLs with the userinfo replaced by***and leaves out thereqwest::Client, whose default headers may hold a credential. - Build each
reqwest::Clientyou pass in withredirect::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 byPOSTwith aParametersbody (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-pixbyGET(ITI-83) and byPOST, 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 theGETputs 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 anihe_atna::balp::AuditRecorderthrough.audited(…)and records the BALPAuditEventof each transaction.atna:XcpdClient::auditedhands the ITI-55 Initiating Gateway audit message of every exchange to a recorder, written as the DICOM audit message inihe-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]inCHANGELOG.mdfor every change a user can see. - The book page it changes, in the same pull request, or the
no-booklabel 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:
| Check | What it refuses |
|---|---|
scripts/checks/book-guard.sh | A 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.sh | A 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.sh | A broken internal link or anchor anywhere in the assembled site or README, and an mdbook-lint warning. |
scripts/checks/writing-style.sh | An 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.mdand 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 frommainby 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.shrefuses a change to a crate’s source or features, the server or a deployment file that does not update the book (theno-booklabel is the escape),docs-claims.shholds each crate’s Features table to itsCargo.toml, refuses a typed crate version and a planned item without its tracker link, andwriting-style.shrefuses 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-iti0.0.40 splits theauditfeature in two:balp, the BALPAuditEventof the PIXm, PDQm, mCSD and PMIR transactions onihe-atna/balp, andatna, the ITI-55 audit message ofxcpdonihe-atna/atna, which compiles no FHIR model. A product that enabledauditenablesbalp,atnaor both. Published between releases (#32).ihe-iti0.0.41 takesihe-atna = "=0.2.1"and drops its own copies of the record bound, the URL redaction and the FHIR base join: they areihe-atna’sbalp::recording,redactandfhir_base(FerroHEALTH/FerroAUDIT#8, #9, #61), taken throughihe-atna’srecordingandredactfeatures, so a profile withoutbalporatnatakes no audit stack. API breaks:ihe_iti::recordingis gone (useihe_atna::balp::recording, whoseLateis made withLate::new()); theEventKindconstants of the audit modules are functions, sinceihe-atna0.2’sEventKindholds aVec(pixm::audit::query_consumer(),pdqm::audit::query_consumer()andmatch_consumer(),mcsd::audit::query()andupdates(),pmir::audit::feed_kind(),subscription_create(),subscription_read(),subscription_delete()andsubscription_search()). A product that implementsihe_atna::balp::AuditRecorderforihe-iti’s clients moves toihe-atna = "=0.2.1"with it (#35).nl-generic-functions0.0.26 takesferrohealth-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 innuts_auth, as 0.4.0 does not carry them (#36).ihe-iti0.0.42 takesihe-atna = "=0.2.2", which changes only the declared MSRV, raising it to 1.98; a product that takesihe-itimoves toihe-atna =0.2.2with it (#39).ihe-iti0.0.43 takesihe-atna = "=0.2.3", the only supported release; 0.0.42 was not published (#41).ihe-iti0.0.44 takesihe-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-iti0.0.39, moved from FerroFED with its history: the IHE PIXm, PDQm, PMIR, mCSD and XCPD clients and the IUA authorizer, a feature each. Theatnaandbalpfeatures are gone: the audit trail is theihe-atnacrate of FerroAUDIT. The audit record each profile defines for its own transactions stays here behind the newauditfeature, onihe-atna=0.1.0. A product changesihe_iti::usertoihe_atna::user,ihe_iti::balpandihe_iti::atnatoihe_atna::balpandihe_atna::atna, and the featuresbalpandatnatoaudit(#25). -
nl-generic-functions0.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-authno longer depends onoauth-server-metadata, which is retired: it takes the RFC 8414 issuer rule and metadata location fromferrohealth-smart=0.3.1. A plainhttpissuer 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-itiandnl-generic-functionsread, vendored underdocs/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 itsscripts/vendor/*.sh, itsPROVENANCE.mdand its row indocs/VERSIONS.md(#24).
Changed
- The minimum supported Rust version is 1.98, the version
ihe-atnarequires (#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, andproject.sh, which says FerroPIX has no roadmap board) and the guards underscripts/checks/(comment style, the pin matrix and licence headers, file length,TODOpointers, the crate version, the changelog and the contribution licence), each with a--self-test(#11). - CI with the single required
conclusioncheck, 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.mdwith the scope the owner settled on 2026-10-20, the rules, thespec-researcherandimplementeragents, thespec-lookup,next-task,phase-doneandphase-statusskills, 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.mdandCODE_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.mdand 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.mdsupports only the newest release, with the Cyber Resilience Act commitment to publish security fixes for at least five years after each release (#14).