Identity resolution
A patient query names its patient by an identifier and that identifier’s
issuing namespace. The gateway resolves the pair, outside the query, to the
local ehr_id each member holds for the patient, and sends each member a
query scoped to that ehr_id alone (§5.2, N3, N7). This page covers the
cross-reference the gateway asks and how you configure it. Which members are
asked at all is Localization, and which may not be is
Consent.
See how it works: where the patient identifier stops.
Where the identifier comes from
The gateway reads the patient on either carrier §5.4.3 names (N33, CP-38):
- the subject predicate of §7,
e/ehr_status/subject/external_ref/id/value = '…', with the namespace ine/ehr_status/subject/external_ref/namespace; - an
ENTRY-level subject,…/subject/identifiers/id = '…', with the namespace in…/subject/identifiers/issueror…/subject/identifiers/type.
GET {base}/v1/ehr?subject_id=…&subject_namespace=… names the patient in
its query string instead
(Reading an EHR by subject).
A query that names no namespace resolves in federation.default_namespace
when you set one, and is refused 400 (no-namespace) when you do not.
§5.2 requires a namespace and names no default, so the setting is
FerroFED’s own:
[federation]
default_namespace = "urn:oid:2.999.1"
A PIX Manager: [pixm]
The identity binding FerroFED ships is IHE PIXm, the Patient Identifier
Cross-reference Manager’s ITI-83 query (Annex A.1). Each registry member is
resolved by one Manager, in its ehr_id domain there: the assigning
authority whose identifiers are that member’s ehr_ids.
[[pixm.manager]]
url = "https://pix.example.org/fhir/"
[pixm.manager.members]
"hospital-a" = "urn:oid:2.999.10" # a registry node id = its ehr_id domain
"clinic-b" = "urn:oid:2.999.20"
[pixm.manager.credentials]
bearer_token_file = "/run/secrets/pix-token"
[pixm.namespaces]
"2.999.1" = "urn:oid:2.999.1" # a client namespace = the PIX assigning authority
- Name one
[[pixm.manager]]per Manager. Every member of the registry is resolved by exactly one: a member no Manager names, a member two Managers name, and a name the registry does not hold each refuse the configuration. - Each domain, and each value of
[pixm.namespaces], is an absolute URI. urlis the Manager’s FHIR base,https, with no query or fragment. The Manager is asked for patient identifiers, sohttpis refused namingpixm.manager[N].url, with or without credentials, unless the profile isdevelopment(What must travel encrypted). A user name or password in it refuses the configuration, namingpixm.manager[N].urland never quoting it; the credentials go in[pixm.manager.credentials], with the keys of an endpoint’s credentials:bearer_token, oruserandpassword, each with its_filesibling, or anoauth2grant (A PIX Manager behind OAuth 2.0). Leave the section out when the transport, such as mutual TLS or a private network, authenticates the gateway.[pixm.namespaces]maps the namespace a client writes to the assigning authority the Manager knows it by. A namespace that is itself an absolute URI needs no entry.methodis"get", the default, or"post", which keeps the patient identifier out of the request URL (GETorPOST).
For each query, the gateway asks each Manager once, with one targetSystem
per member that Manager resolves, and reads the identifier the answer holds
in a member’s domain as that member’s ehr_id. The patient identifier goes
to the Manager and nowhere else; no node, log line or error body carries it
(§5.4.1, N33).
| The Manager answers, for one member | The member is | The query |
|---|---|---|
exactly one identifier in the domain, which reads as an ehr_id | sent its node query | goes on |
| no identifier in the domain, or the patient is unknown | not-resolved | goes on; complete is false (N6) |
two different identifiers, one that is no ehr_id, an error, no answer within the budget, or a namespace with no mapping | not-resolved, with the reason in error | fails 424 under all-or-nothing |
A member the Manager could not answer for may hold the patient, so the
gateway fails the query by default rather than answer without it (§11.1;
§11.3 covers only an answered lookup, so this rule is FerroFED’s own). With
openEHR-federation-completeness: partial, the members that did resolve
answer. Resolution runs inside the request’s overall budget
(Timeouts).
Two identical identifiers in one domain, the same system and the same
value, are one identifier: ITI-83 returns one targetIdentifier per
business identifier of the patient (PIXm 3.1.0 §2:3.83.4.2.2.1), and
resolution yields a set of {node, local ehr_id} (§5.2). A Manager that
lists the same identifier from a master record and a local record resolves
the member. Two different values in one domain are never guessed between.
A PIX Manager behind OAuth 2.0
A Manager that issues expiring tokens, as an IHE IUA Authorization Server
does, is asked with a token the gateway obtains itself by the OAuth 2.0
client-credentials grant (RFC 6749 §4.4; IUA ITI-71 §3.71.4.1.2.1). The
same oauth2 table serves [pdqm.credentials], [pmir.credentials] and
[registry.mcsd.credentials].
[pixm.manager.credentials.oauth2]
grant = "client_credentials"
token_endpoint = "https://mpi.example.org/auth/oauth2_token"
client_id = "ferrofed-pix-consumer"
client_auth = "client_secret_basic" # or client_secret_post, or private_key_jwt
client_secret_file = "/run/secrets/pix-consumer-secret"
scope = "*" # the scopes the Manager's server defines
# resource = "https://mpi.example.org/fhir/" # RFC 8707, when the server takes one
# audience = "https://mpi.example.org"
client_authsays how the gateway authenticates at the token endpoint:client_secret_basic, the client id and secret in the HTTP Basic scheme, as ITI-71 prescribes;client_secret_post, the two in the request body, for a server that reads them there (RFC 6749 §2.3.1); orprivate_key_jwt, a client assertion the[signing]key signs (RFC 7523 §2.2), with no secret.client_secretorclient_secret_fileholds the secret. It is required with the two secret methods and refused besideprivate_key_jwt.scopeis required, each value an RFC 6749 §3.3 scope token.token_endpointishttpsoutside the development profile: the client secret or assertion travels to it.grant = "token_exchange",dpop_key_file,tls_client_certificate_bound_access_tokensand the TLS methods ofclient_authbelong to a node’s grant (Onward credentials) and are refused here.config checknames every key it refuses, and never a secret.
The gateway caches the token until 30 seconds before the expires_in the
token response states, and incorporates it in each ITI-83 request as
Authorization: Bearer (ITI-72 §3.72.4.2). A token with no stated lifetime
serves the one request it was obtained for. When the Manager answers 401,
the gateway drops the token, obtains a new one, and sends the request once
more; it never retries with the refused token (§3.72.4.3). A token endpoint
that refuses or does not answer within the per-node timeout leaves the
members of that Manager not-resolved with the reason in error, and the
query fails 424 under all-or-nothing; nothing is sent to the Manager
without a token.
[pixm] as the localizer
Under federation.node_selection = "localized" with no [xcpd], the
[pixm] resolver is also the localizer, the “demographic-registration” kind
of §14.2. The candidates are the members whose domain holds an identifier
for the patient at its Manager. Every other member is not-localized and is
never asked. The resolution of the same query reuses that ITI-83 answer, so
each query still asks each Manager once.
A member whose domain holds two identifiers, or one that is no ehr_id, is a
candidate, because the Manager holds the patient there; its resolution then
reports why it could not be asked. A Manager that fails, answers in a form
the gateway cannot read, or does not answer within
[federation.localization] timeout_ms fails the localization closed
(§14.1): every member is not-localized with the error, and no member is
asked unless on_failure = "ask-all". OPTIONS {base}/ declares
localization.mode as "pixm". With [xcpd] set, XCPD localizes and
[pixm] only resolves.
Keeping the PIX Manager current
The gateway reads each member’s ehr_id from the Manager and never writes
to it. The Manager answers only from what its identity feeds delivered, so
for every patient it must hold:
- the identifier clients name the patient by, in the system
[pixm.namespaces]maps the client’s namespace to (or the namespace itself when it is an absolute URI); - for each member that holds an EHR for the patient, that EHR’s
ehr_idas an identifier whosesystemis the member’sehr_iddomain, the value[pixm.manager.members]maps the member to; - the demographics the Manager links records by. The PIXm Patient profile
requires a
name(PIXm 3.1.0, StructureDefinitionIHE.PIXm.Patient).
Annex A puts it the same way: the ITI-104 identity feed “seeds the cross-reference with, for example, a local id or an openEHR id” (Annex A §A.1). How the Manager links records from different domains to one patient is its own business: PIXm “does not specify the rules and algorithm applied by the Patient Identifier Cross-reference Manager” (PIXm 3.1.0 §1:41.4.1).
Who feeds a member’s domain
Each ehr_id domain has one feeding system. “The Patient Identifier
Cross-reference Manager shall only recognize a single Patient Identity
Source per domain” (PIXm 3.1.0 §2:3.104.4.1.3). Make that Source the member
itself, or the integration engine at the member that creates its EHRs or
sees them created. Whether a CDR product can send a feed message on its own
depends on the product. FerroFED makes no claim for any CDR.
Register each member’s domain at the Manager before you add the member to
the registry. A Manager that does not know a domain the gateway asks for
refuses the whole call (403, PIXm 3.1.0 §2:3.83.4.2.2.4), and every member of
that Manager is then unavailable for the query.
ITI-104: the Patient Identity Feed FHIR
ITI-104 is PIXm’s own feed (PIXm 3.1.0 §2:3.104). The Source sends it when
it adds a patient, revises one, merges two of its own records, and, with
the Remove Patient Option, when it removes one (§2:3.104.4.1.1,
§2:3.104.4.2.1, §2:3.104.4.3.1). For a member, adding a patient is creating an EHR. The
message is a conditional update keyed on the identifier in the Source’s
own domain (§2:3.104.4.1.2), here the ehr_id:
PUT https://pix.example.org/fhir/Patient?identifier=urn:oid:2.999.10|aaaaaaaa-aaaa-4aaa-8aaa-000000000001
Content-Type: application/fhir+json
{
"resourceType": "Patient",
"identifier": [
{ "system": "urn:oid:2.999.10", "value": "aaaaaaaa-aaaa-4aaa-8aaa-000000000001" },
{ "system": "urn:oid:2.999.1", "value": "ffd-test-0001" }
],
"name": [ { "family": "Synthetic", "given": [ "Example" ] } ]
}
The Manager’s CapabilityStatement requires conditional update for ITI-104
and conditional delete with the Remove Patient Option (PIXm 3.1.0,
CapabilityStatement IHE.PIXm.Manager). A removed EHR is that conditional
delete on the same identifier (§2:3.104.4.3.2).
ITI-93: the PMIR Mobile Patient Identity Feed
When the Manager is a PMIR Patient Identity Registry, the Source sends the
Mobile Patient Identity Feed instead (PMIR 1.6.0 §2:3.93), on each create,
update, merge or delete (§2:3.93.4.1.1). It is an HTTP POST of a message
Bundle: a MessageHeader whose event is urn:ihe:iti:pmir:2019:patient-feed,
whose focus is a history Bundle that holds the Patient (§2:3.93.4.1.2). The
Patient carries the same identifiers as the ITI-104 example. A Registry
answers ITI-83 as the Patient Identifier Cross-reference Manager (PMIR 1.6.0
§1:49.1.1.1), and its CapabilityStatement declares $ihe-pix on Patient
(CapabilityStatement IHE.PMIR.PatientIdentityRegistry).
PMIR keeps a single master identity per patient (PMIR 1.6.0 §1:49), so each
member’s ehr_id is one more identifier on it. This feed runs from each
member to the Registry. It is a different flow from [pmir]
below, where the gateway subscribes to hear of identity changes.
A bulk load for the EHRs that already exist
Neither profile defines a bulk transaction, so each EHR that existed before the feed started reaches the Manager as one feed message. At each member, as that member’s operator, read the pairs from the member’s own CDR:
SELECT e/ehr_id/value,
e/ehr_status/subject/external_ref/id/value,
e/ehr_status/subject/external_ref/namespace
FROM EHR e
Then send one ITI-104 or ITI-93 message per row, as the member’s Source, and switch the Source to the live feed before the load ends, so an EHR created during the load is not missed. Run the query at the member, never through the gateway: the member is the Source of its domain, and the pairs are the member’s to publish. A member whose EHRs carry no subject has no pairs to read, and its feed has to come from the system that knows which patient each EHR belongs to.
Why the gateway does not feed the Manager
FerroFED could read each member’s EHRs and post the pairs itself. It does not (no specification governs this: our own design):
- The Manager recognizes one Source per domain (PIXm 3.1.0 §2:3.104.4.1.3), and the member creates, merges and removes its own EHRs. A copy the gateway posts in batches is a second Source that lags the first.
- FerroFED holds no clinical data and stores no patient identifier. A feed would make it read and send every patient’s identifier in bulk, which no federation request needs: a query consumes one identifier, once, at resolution (§5.4.1, N33).
- Feeding a Manager is the identity source’s function. A helper that does it belongs beside the MPI, never on the gateway’s request path.
A PIX Manager verified with FerroFED: SanteMPI
SanteMPI, SanteSuite’s master patient index (Apache License 2.0), answers
ITI-83 and takes the ITI-93 feed. The end-to-end lane runs
santesuite/santedb-mpi:2.5.12, pinned by the digest in the example below,
against two FerroEHR members: each member’s feed application registers
the patient with its own ehr_id over ITI-93, the gateway resolves the
patient over ITI-83 GET, both members answer the federated query, and a
patient no member fed is not-resolved at both.
Set it up as follows. The image settings are the ones the lane uses, and every domain, address and secret is an example.
-
Run the image with SanteSuite’s documented SanteMPI features and a PostgreSQL database (SanteSuite: Using Docker Containers):
services: santempi: image: santesuite/santedb-mpi:2.5.12@sha256:608484de046a932ec2f92e9991a32507fc8ec89d53d7639cbab886a63dbf6207 environment: SDB_FEATURE: "LOG;DATA_POLICY;AUDIT_REPO;ADO;PUBSUB_ADO;RAMCACHE;SEC;SWAGGER;OPENID;FHIR;HDSI;AMI;BIS;MDM;MATCHING;IHE_PIXM;IHE_PDQM;IHE_PMIR" SDB_MATCHING_MODE: WEIGHTED SDB_MDM_RESOURCE: Patient=org.santedb.matching.patient.default SDB_MDM_AUTO_MERGE: "false" SDB_DB_MAIN: "server=db;port=5432; database=santedb; user id=santedb; password=…; pooling=true;" SDB_DB_AUDIT: "server=db;port=5432; database=auditdb; user id=santedb; password=…; pooling=true;" SDB_DB_MAIN_PROVIDER: Npgsql SDB_DB_AUDIT_PROVIDER: Npgsql SDB_OPENID_INSECURE_CLIENT_AUTH: "true"Its FHIR base is
{origin}/fhir/and its token endpoint{origin}/auth/oauth2_token. -
Create one security application per member’s feed, and one for the gateway, through SanteMPI’s administration interface, each granted the client-credentials policies
1.3.6.1.4.1.33349.3.1.5.9.2.1.0.0.1and1.3.6.1.4.1.33349.3.1.5.9.2.1.0.0.1.0, and setSDB_OPENID_INSECURE_CLIENT_AUTH: "true"so an application can obtain a token with no device credential. SanteMPI denies the client-credentials grant otherwise. Grant the two policies by hand: SanteDB 2.x loads its OAuth flow policies from000-OAuthPolicies.datasetafter its initialisation script has granted the built-in applications every policy that existed then, so no application holds them out of the box. -
Register the identity domains: the patient namespace, open to every source, and one per member, with
urlset to the member’s domain URI and the member’s feed application as its assigning application, so that application is the domain’s one authoritative Source (SanteSuite: Patient Identity Feed, Blocks Inappropriate Assigner). SanteMPI writes the domain’surlas the identifiersystemin its answers, so it must equal the value in[pixm.manager.members]. -
Feed each member’s EHRs as its feed application, over ITI-93 to
POST {origin}/fhir/Bundle, live and as the bulk load above. -
Point the gateway at it:
[[pixm.manager]] url = "https://mpi.example.org/fhir/" method = "get" [pixm.manager.members] "hospital-a" = "urn:oid:2.999.10" "clinic-b" = "urn:oid:2.999.20" [pixm.manager.credentials.oauth2] grant = "client_credentials" token_endpoint = "https://mpi.example.org/auth/oauth2_token" client_id = "FFD_PIX_CONSUMER" client_auth = "client_secret_post" # SanteMPI reads the client from the body client_secret_file = "/run/secrets/mpi-consumer-secret" scope = "*"
Before you rely on it, weigh these:
- The image. 2.5.12, published on 2023-07-04, is the newest stable
image SanteSuite publishes. It is built for
linux/amd64only and runs on Mono as root. The 3.0 line is still an alpha. Put it through your own security review. - The token. SanteMPI issues bearer tokens by the client-credentials
grant, and they expire. The gateway runs that grant itself and refreshes
the token before it expires
(A PIX Manager behind OAuth 2.0).
SanteMPI reads the client id and secret from the request body, so
client_authisclient_secret_post. - What was verified. Resolution by
GET, which ismethod’s default, and the feed over ITI-93. ITI-83 byPOSTand the ITI-104 feed were not verified against SanteMPI. - Every domain registered. SanteMPI refuses a call naming a domain it
does not know with
403, as PIXm requires, so the members of that Manager are unavailable and the query fails424.
Products checked that do not answer as the gateway needs
| Product | What it is | Why it is not a Manager for FerroFED |
|---|---|---|
| HAPI FHIR JPA server | a FHIR server with a master data management module | its MDM operations do not include $ihe-pix (HAPI FHIR: MDM Operations) |
| OpenCR (IntraHealth client registry) | a client registry on a FHIR server | its $ihe-pix is served on GET alone, compares a single targetSystem, so a call for two members’ domains finds none, and writes targetId without a Reference value (server/lib/routes/fhir.js) |
| IHE Gazelle Patient Manager | a simulator that plays IHE actors, the PIXm Manager among them, for testing | a test tool, not a production MPI (Gazelle Patient Manager) |
| IPF | a Java integration library | a component to build a Manager with, not a server (IPF: ITI-83) |
| OpenHIM | an interoperability layer | it relies on a client registry behind it and holds no cross-reference of its own (OpenHIM: About) |
When the hospital MPI does not speak PIXm
The cross-reference role need not sit in one place. “Whether the node, its organization’s MPI, or a regional service fills that role is immaterial to the federation” (N34, §5.5). When the MPI you have does not answer ITI-83:
- It speaks PIX or PIXV3. A PIXm Manager “can be grouped with either PIX or PIXV3 Patient Identifier Cross-reference Consumer and Source to proxy” ITI-83 and ITI-104 (PIXm 3.1.0 §1:41.6.1). A PIXm front grouped with the MPI in that way serves the gateway. FerroFED has verified no such front.
- Run a PIXm Manager for the federation beside it. SanteMPI, set up as
above, holds the members’
ehr_iddomains, keyed on an identifier the hospital MPI issues, such as its enterprise identifier. The hospital MPI stays the authority for its own identifiers, and the federation’s Manager only cross-references them toehr_ids. The feed to it comes from the members, as above. - Clients name a local identifier. If clients know the patient by an
identifier only the hospital MPI holds, and the MPI answers PDQm,
[pdqm]translates it to the master identifier first. - A Manager per organization. Each
[[pixm.manager]]names the members it resolves, so each member’s own organization can run its own Manager.
The development table, [dev], is refused outside profile = "development" and is no answer in production. Without any of these, every
patient query fails 424, as Choosing one says.
Demographics first: [pdqm]
A client may name the patient by an identifier the cross-reference does not know, such as a hospital-local number. Annex A places a PDQm query ahead of localization and resolution for that case (Annex A §A.2 and §A.7): the gateway asks a Patient Demographics Supplier which person the identifier names, takes the identifier that person carries in the master domain, and localizes and resolves that identifier as it would the client’s own.
[pdqm]
url = "https://pdq.example.org/fhir/"
transaction = "iti-78" # or "iti-119"
master = "urn:oid:2.999.1" # the master domain's identifier system
timeout_ms = 1000 # each exchange with the Supplier
[pdqm.namespaces]
"urn:oid:2.999.7" = "urn:oid:2.999.7" # a client namespace = the system it is sent in
[pdqm.credentials]
bearer_token_file = "/run/secrets/pdq-token"
- Only an identifier in a namespace
[pdqm.namespaces]names goes to the Supplier. Every other one is resolved as it is, and a namespace[pixm.namespaces]maps is refused here, naming the key. transaction = "iti-78", the default, sends the Mobile Patient Demographics Query:identifier=<system>|<value>andidentifier=<master>|, which asks the Supplier for identifiers in the master domain alone (PDQm §2:3.78.4.1.2.3)."iti-119"sends the Patient Demographics Match instead, with the identifier on the input Patient andonlyCertainMatchesset (§2:3.119.4.1.2); declare it only where the Supplier offers$match.masteris the identifier system of the master domain. The master identifier is resolved in the namespace of the same name, so[pixm]or[dev]must resolve it;[pdqm]without either refuses the configuration.url, the credentials and thehttprule are those of a PIX Manager: the Supplier is sent the patient identifier, so plainhttpis refused, namingpdqm.url, unless the profile isdevelopment. No redirect is followed, and an answer is read up to 8 MiB.- A
SIGHUPreload applies a change to[pdqm], as it does to[pixm]: the reloaded federation asks the Supplier the new table names.
| The Supplier answers | The members are | The query |
|---|---|---|
one active Patient carrying exactly one identifier in the master domain (for ITI-119, graded certain) | localized and resolved by the master identifier | goes on |
| no Patient, or one with no master identifier (PDQm §2:3.78.4.1.3 Case 3, §2:3.119.4.1.3 Cases 4, 5 and 7) | not-resolved | goes on; complete is false (N6) |
more than one Patient, a Patient with two master identifiers, or an ITI-119 match not graded certain | not-resolved, with the reason in error | fails 424 under all-or-nothing |
a failure, or no answer within timeout_ms | not-localized with the error under on_failure = "closed"; not-resolved under "ask-all" and on a directed query | 200 under "closed" (§14.1); 424 otherwise |
A Patient with active set to false is a deprecated record and never a
match (§2:3.78.4.1.3 Case 6). The gateway never picks one of several
matches: ITI-78 counts them in Bundle.total (§2:3.78.4.1.3 Case 1), and
ITI-119 returns one entry for each (§2:3.119.4.1.3 Case 2). The step feeds
localization, so an outage on an undirected query follows the localization
failure policy, closed by default, as a localizer outage does; an exchange
whose audit record cannot be stored fails closed under every policy. The
read of an EHR by subject answers 404 for no match and 424 for the
rest.
The client’s identifier reaches the Supplier and nowhere else; the master
identifier reaches the cross-reference and nowhere else. The outbound gate
withholds both from every request to a node, and no log line, metric label
or error carries either (§5.4.1, N33). Each exchange is audited through
[audit], which is required outside development. The step’s
state shows as demographics on
GET {base}/operator/dependencies, and each call is counted in
ferrofed_demographics_requests_total (Metrics).
The development cross-reference: [dev]
For a laptop or a test, the gateway can resolve from a static table in the configuration instead. It binds nothing, it is no identity binding of N3, and it is accepted only in a configuration that declares itself for development (no specification governs this: our own design):
profile = "development"
[[dev.crossref]]
namespace = "urn:oid:2.999.1.1"
value = "ffd-test-0001"
member = "node-a"
ehr_id = "aaaaaaaa-aaaa-4aaa-8aaa-000000000001"
Each row maps one synthetic patient to its ehr_id at one member; a patient
with no row at a member is not-resolved there. Under any other profile,
[dev] refuses the configuration. A development deployment prints a red
notice in its startup banner that it
must not hold or reach real patient data. The
quickstart runs on this table.
Choosing one
Set [pixm] or [dev], never both: both refuse the configuration. With
neither, the gateway still starts, and every patient query fails closed: each
member is not-resolved with “no cross-reference service is configured”,
and the query is a 424. A query that names no patient needs no resolution
and runs as written.
Both sections take effect on a
reload. GET {base}/operator/dependencies
reports the resolver’s last observed state
(Health probes).
Under federation.node_selection = "localized" the [dev] table is also the
localizer: it names the members that hold a row for the patient, and every
other member is not-localized
(Node selection).
The identity feed: [pmir]
A merge at the identity source can leave a caller’s
resolution bindings naming the ehr_id of an identity that no
longer exists. Each binding expires in any case federation.binding_ttl_ms
after the last resolution that returned it, and a resolution after the merge
no longer returns the stale one. With
[pmir] set, the gateway also hears of each change as it happens: it
subscribes at your IHE PMIR Patient Identity Registry with ITI-94, and the
Registry sends every Patient Master Identity change to the gateway as an
ITI-93 message (PMIR 1.6.0, Annex A.4).
[pmir]
url = "https://pmir.example.org/fhir" # the Registry's FHIR base
callback_url = "https://gateway.example.org/pmir/feed" # where the Registry sends the feed
path = "/pmir/feed" # the route under {base}, the default
feed_token_file = "/run/secrets/pmir-feed-token" # the token the Registry sends
# identifier_system = "urn:oid:2.999.1" # only Patients with an identifier here
# timeout_ms = 5000
# check_interval_s = 60
# on_drain = "keep" # or "unsubscribe"; see Several replicas
[pmir.credentials] # how the gateway authenticates to the Registry
bearer_token_file = "/run/secrets/pmir-registry-token"
- The subscription. At start the gateway creates a
Subscriptionaturlwith amessagechannel tocallback_urland a FHIR JSON payload. Its criteria isPatient, orPatient?identifier=<system>|withidentifier_systemset, so the Registry sends only the Patients that carry an identifier from that authority. Before it creates one, the gateway searches the Registry for its own (GET [base]/Subscription?url=<callback_url>) and adopts the one it finds, so a create the Registry answered late is never made twice. When the search lists several, it adopts the usable one whose location sorts first and deletes the others. A delete the Registry answers404or410found the subscription already gone. A Registry that answers that search400or404does not support it, and the gateway creates. Everycheck_interval_sthe gateway reads the subscription back. It deletes and recreates one the Registry reportserrororoff, and recreates one the Registry no longer holds. Each failed check doubles the wait before the next, up to 32 timescheck_interval_s, and a check that succeeds resets it. If the Registry answers a create201with noLocation, or with one outsideurl, the gateway cannot manage that subscription. It creates no other until a restart and reports the fault asunmanageable; delete that subscription at the Registry. On a drain the gateway stops checking and lets the check in flight end. Withon_drain = "keep", the default, it leaves the subscription at the Registry, for the other replicas and for its next start, which adopts it. Withon_drain = "unsubscribe"it then deletes its subscription, found by search when it never learned where it was. PMIR lets a subscriber delete a subscription (§2:3.94.4.5) and does not say when. - Authenticating the feed. The subscription carries no credential for the
feed (PMIR §2:3.94.5), so you agree the feed token with the Registry’s
operator and configure it on both sides. The Registry sends it as
Authorization: Bearer <token>. A message without that token is answered401and changes nothing. - The route.
POST {base}{path}takes one ITI-93 message. The route sits outside the ITS-REST surface and its client authentication, so the path may not be under/v1,/healthor/.well-known. A message that does not hold to the PMIR profiles is answered400(415for a media type other than FHIR JSON) with anOperationOutcome, and changes nothing. An applied message is answered with the ITI-93 response. - What a message changes. Only routing state, never a record. A merge
or a delete drops every binding of the
ehr_ids its Patient carries in a member’sehr_iddomain, the domains[pixm]maps. An update states the identity as it now is and never what it lost, so it drops every binding, as does a merge or delete that carries no suchehr_id. A create drops nothing. A dropped binding costs one resolution, never a wrong node. Theehr_idindex stays: a merge moves no EHR between nodes. - What is logged. The kind of each change and their counts, and how many bindings went. The messages carry patient identifiers and demographics, so no identifier reaches a log line, a metric or an answer.
url and callback_url carry patient identities, so both are https
outside profile = "development", and neither may carry a user name or a
password. GET {base}/operator/dependencies reports the Registry as
identity_registry: up while the gateway holds a subscription in
requested or active; failing after a refusal, an answer that breaks
ITI-94, or a create it cannot manage; and down when the Registry did not
answer. identity_registry_fault names the reason (Health). Each message is counted by result in
ferrofed_identity_feed_messages_total (Metrics). A change to
[pmir] takes a restart.
PMIR 1.6.0 defines no unmerge (§2:3.93.4.1.3), so the gateway hears of a merge and never of a split. The specification marks this lifecycle track provisional, and FerroFED claims no propagation of an identity change to a later resolution: the conformance matrix keeps track 8 deferred.
Several replicas
A subscription is named by its callback_url, and the Registry sends each
ITI-93 message once to that URL. How your replicas share the feed follows
from the callback_url you give each of them:
- One
callback_urlfor every replica, your balancer’s address. The replicas share one subscription: the first to start creates it, and the others adopt it. Two replicas that create one each at the same moment are sent each change twice until the next replica start, which keeps one and deletes the other. Keepon_drain = "keep", so a rolling restart never deletes the subscription the other replicas rely on. Each message reaches the one replica the balancer picks, which drops the stale bindings. The other replicas keep theirs until they expire: a binding livesfederation.binding_ttl_msafter its caller’s last resolution (Resolution bindings), so on those replicas a stale binding is routed on until its caller has made no resolution for that long. Lowerbinding_ttl_msto narrow the window. - A
callback_urlof its own for each replica, an address the Registry reaches each replica at, such as a StatefulSet pod’s stable name. Each replica holds its own subscription, so the Registry sends every change to every replica and each drops its stale bindings as it happens. Seton_drain = "unsubscribe", so a replica deletes its own subscription when it stops; it never touches another replica’s. A replica that stops without draining leaves its subscription until it starts again under the samecallback_urland adopts it, or until you delete it at the Registry.
To stop using the feed with a shared callback_url, set on_drain = "unsubscribe" on the last replica before it stops, or delete the
subscription at the Registry.