UnifyEMPI is a high-performance, multi-tenant master patient index (MPI) foundation for FHIR R4 and HL7v2. It is a modular monolith with three independently scalable hosts:
UnifyEmpi.Api: FHIR R4, SMART discovery, and reviewer APIs.UnifyEmpi.Hl7v2.Host: native MLLP ingestion for ADT identity events.UnifyEmpi.Portal: an enterprise operations portal using Blazor Interactive Server and generic OIDC.
The registry is hybrid. Source systems remain authoritative for their Patient snapshots; UnifyEMPI owns enterprise UUIDv7 identities, Person links, candidate indexes, canonical Patient views, review cases, receipts, and audit evidence.
Important
UnifyEMPI is an engineering foundation, not a certified clinical product. Demo deployments must contain synthetic data only. Production NHS use requires the independent safety, privacy, security, operational and contractual work described below.
See core paths and processing model for annotated diagrams of real-time duplicate detection, FHIR and HL7 ingestion, $match, merge, split, survivorship, tenant isolation, and provider boundaries.
For the operational meaning of tenants, source Patients, canonical Patients, Person,
HMAC blocking tags and review errors, start with
identity concepts and frequently asked questions.
- Master patient index: ingest source Patient records, create enterprise identities, maintain Person links, and serve a survivorship-based canonical Patient view.
- FHIR R4 API: Patient create, update, read, search, and
$match; Person lookup; JSON and XML; ETags; structuredOperationOutcomeerrors; and bounded UK Core validation. - Native HL7v2 ingestion: MLLP support for ADT A01, A04, A08, A28, A31, A40, and A47 messages from HL7 versions 2.3.1, 2.4, and 2.5.1.
- Safe, explainable matching: configurable blocking rounds, field weights and thresholds; NHS-number validation; HMAC-protected candidate indexes; field-level evidence; and no population-wide scans.
- Enterprise operations portal: tenant dashboard, optional governed create/update for a separately configured UI-managed source, patient registry search, explainable duplicate workbench, governed merge and source-level unlink/split workflows, review queue, source trust, audited policy configuration, and audit search.
- Review workflow: probable matches create review cases that authorised reviewers can inspect, link, reject, correct, or close as superseded when an identity is replaced or its evidence changes. Tenant-configurable dual approval defaults to two distinct reviewers, with no self-approval.
- Multi-tenant isolation: every identity, query, decision, receipt, and audit event is bound to a trusted tenant and source-system context.
- Replaceable storage: an ephemeral in-memory provider for development and a durable GCP Healthcare API provider are included behind the same storage contract.
- Production foundations: external OIDC and SMART scopes, mutual-TLS MLLP, health checks, OpenTelemetry, JSON logging, containers, Compose, and a Helm chart.
There is one hosted demo. It consists of two services backed by the same dedicated FHIR store:
- Operations portal
- Swagger API explorer
(API base:
https://unifyempi-demo-api-mjpwolhr6q-nw.a.run.app)
The Compose quick start below runs a separate local development stack; it is not a second hosted demo.
The repository includes the reproducible GCP deployment that creates those
unifyempi-demo resources and prints the assigned Cloud Run URLs after deployment.
Never enter real patient or confidential information into a demo environment.
Public MLLP is deliberately excluded because production HL7v2 ingress must use a
private endpoint and mutual TLS.
See the public-demo guide for the GCP topology, verified scenarios, cost controls, reproducible deployment and safe teardown.
The quickest way to run both services is with Docker and Docker Compose:
docker compose up --buildOnce the containers are ready:
Invoke-RestMethod http://localhost:8080/health/ready
Invoke-RestMethod http://localhost:8080/fhir/R4/metadata
Start-Process http://localhost:8081- FHIR and review API:
http://localhost:8080 - Swagger UI:
http://localhost:8080/ - Operations portal:
http://localhost:8081 - HL7v2 MLLP listener:
localhost:2575 - Development tenant:
demo - Development source system:
demo-source
The local stack deliberately disables authentication and uses an isolated ephemeral in-memory provider in each host. The portal seeds synthetic patients so its workflows can be explored immediately. Data is discarded when a host stops, and records ingested through the API or MLLP host are not shared with the portal in this development mode. This is not suitable for production.
The portal is a server-rendered operations surface; patient data and OIDC tokens are not placed in browser storage. One validated tenant_id is active for the entire signed-in session, and every application call is constructed from that trusted claim.
Out of the box, authorised staff can:
- optionally create and update Patient records owned by a separate server-configured UI-managed source, with NHS-number validation and optimistic concurrency protection;
- find canonical patients using a strong identifier or family name with date of birth;
- compare canonical and source demographics with provenance and survivorship trust;
- run a bounded duplicate search and inspect the evidence behind every score;
- open a governed merge review, where the selected candidate is clearly identified as the surviving enterprise identity;
- unlink selected source records into a new identity through the corrective split workflow;
- approve or reject work with a required rationale and optimistic concurrency protection;
- recognise outdated reviews automatically, explain redirects or version changes, and close superseded work with immutable audit evidence;
- inspect source authority and trust, audit events, operational health, and review workload; and
- update non-secret tenant matching policy with an immutable audit event.
Run it directly with:
dotnet run --project src/UnifyEmpi.PortalThe development configuration signs in a demo reviewer for tenant demo, seeds synthetic records, and uses no real patient data.
To build and test the source locally, install the .NET 10 SDK and run:
dotnet restore UnifyEMPI.slnx --locked-mode
dotnet build UnifyEMPI.slnx -c Release --no-restore
dotnet test UnifyEMPI.slnx -c Release --no-buildStop the local containers with docker compose down.
To explore discovery, canonical search, JSON/XML $match, safe error cases, local source
writes and reviewer reads without assembling requests, use the
Postman collection and guide. Public requests target the
demo API by default; mutating requests deliberately target localhost.
UnifyEMPI protects performance at three different levels; each test answers a different question and the results should not be mixed:
| Level | What it measures | Current gate or target |
|---|---|---|
| Core scoring benchmark | One in-process comparison against 500 already-normalised candidates, without network or storage time | CI baseline mean: 0.629 ms on the hosted Ubuntu runner; hard ceiling: 10 ms; fail on a regression greater than 10% |
End-to-end $match load test |
HTTP, authentication, candidate lookup, scoring, serialisation and the configured provider | Acceptance target: 250 requests/second for 30 minutes, p95 below 250 ms, and under 0.1% failed requests |
| Registry scale test | Candidate blocking and matching against a realistically populated durable registry | Acceptance target: 10 million canonical identities using synthetic data in an isolated test store |
The core benchmark runs on every default CI build. Its checked-in number is a
BenchmarkDotNet ShortRun mean for regression detection, not a p95/p99 result and not
an end-to-end throughput claim. The k6 and 10-million-identity figures are release
acceptance targets that require a representative, same-region GCP environment; they
have not been demonstrated merely by running the demo.
Run the scoring benchmark and its regression gate with:
dotnet run --project benchmarks/UnifyEmpi.Benchmarks -c Release -- `
--filter "*ScoreFiveHundred*" --job short --exporters json
./scripts/Test-BenchmarkRegression.ps1Run the end-to-end scenario only against an isolated synthetic store:
$env:BASE_URL = "https://unifyempi.example"
$env:ACCESS_TOKEN = "<short-lived system token>"
k6 run benchmarks/k6/match.jsPerformance comes primarily from bounded work: ingestion precomputes HMAC blocking keys, candidate lookup never scans the patient population, at most 500 normalised candidates reach the scoring engine, and API, MLLP and portal hosts scale independently. See the benchmark guide and core-path diagrams for methodology and data flow.
- Identity model and frequently asked questions: tenants, national deployment, source/canonical records, blocking, reviews, existing-store onboarding and troubleshooting.
- NHS Wales source model: organisation-level sources for all seven health boards, WDS and Velindre.
- Matching and blocking rules: normative normalisation, configurable blocking rounds, score formula, certainty/conflict rules, routing, examples and feature-alignment gaps.
- Core paths and processing model: annotated diagrams for ingestion, matching, merge, split, survivorship and tenant isolation.
- Architecture: dependency boundaries, materialisation and the replaceable provider contract.
- Configuration: API, portal, OIDC, tenant, HMAC and MLLP settings.
- GCP demo deployment: deployable topology, demo scenarios, cost controls and safe teardown.
- Postman collection: importable discovery, matching, source write and reviewer-operation examples.
- Contributing and security policy: development expectations and private vulnerability reporting.
- Storage-provider ADR: why the modular monolith and provider contract were selected.
Do not promote the Compose development settings directly. A production deployment needs durable storage, real identity and access control, managed secrets, encrypted endpoints, validation packages, monitoring, and an agreed clinical and information-governance process.
-
Define the tenant boundary. Decide which organisation or data controller each tenant represents, which source systems belong to it, which identifiers are authoritative, and whether a shared deployment provides sufficient isolation.
-
Provision durable infrastructure. Select
GcpHealthcareand create an R4 FHIR store with versioning, referential integrity, update-create, and strict search handling. The reference GCP deployment creates a private GKE cluster, Healthcare API dataset and store, and a Workload Identity service account:terraform -chdir=deploy/terraform init terraform -chdir=deploy/terraform plan ` -var="project_id=YOUR_PROJECT" ` -var="gke_network=YOUR_NETWORK_SELF_LINK" ` -var="gke_subnetwork=YOUR_SUBNETWORK_SELF_LINK" terraform -chdir=deploy/terraform apply
-
Publish immutable images. Build the API, MLLP and portal Dockerfiles, scan all three images, generate an SBOM, and push versioned tags or digests to your approved registry. Do not deploy the mutable
latesttag. -
Configure identity, tenants, and secrets. Connect the API and portal to an external OIDC issuer. API service credentials must supply trusted
tenant_idandsource_systemclaims; interactive portal identities must supply exactly one trustedtenant_idplus the required MPI permissions. Welsh health-board, WDS, Velindre and other organisation-owned records remain read-only in the portal. Grantmpi.patient.writeonly if the deployment deliberately adds a separatePortal:ManagedSourceSystemfor UI-created records. Register the portal as an authorisation-code client with PKCE and a server-side client secret. Supply tenant definitions, source trust, authoritative sources, and at least one active 256-bit HMAC secret through a managed secret store; never commit secrets to a values file. -
Install validation packages. Fetch the pinned FHIR R4 and UK Core packages, scan them, and make them available to the API through the read-only validation-package volume:
./scripts/Get-FhirPackages.ps1 -Destination ./artefacts/fhir-packages
-
Configure private HL7 ingress. Give every MLLP listener a fixed tenant and source-system binding, a server certificate, and an explicit client-certificate allow-list. Keep
AllowPlaintext=falseand expose MLLP only through a private endpoint or an approved TLS load balancer. -
Deploy with Helm. Copy the supplied values file, replace the example image locations, set the GCP store, OIDC issuer, Workload Identity service account, resource limits, and secret references, then deploy:
helm upgrade --install unifyempi deploy/helm/unifyempi ` --namespace unifyempi ` --create-namespace ` --values production-values.yaml
Keep the Helm namespace aligned with Terraform's
kubernetes_namespacevalue. The chart expects you to provide the validation-package PVC, a shared portal Data Protection key PVC, configuration and MLLP TLS Secrets, trusted HTTPS ingress, certificate management, and any cloud-specific private load balancer. The portal uses Blazor Interactive Server, so its ingress must support WebSockets and session affinity; the chart also enablesClientIPservice affinity. Encrypt and restrict the shared Data Protection volume. -
Verify before admitting patient data. Confirm
/health/live,/health/ready, and/fhir/R4/metadata; run synthetic FHIR and HL7 smoke tests; test tenant isolation, JWT scopes, client certificates, audit delivery, alerts, scaling, backup/restore, and failure recovery.
Production NHS use additionally requires an approved DPIA, clinical-safety case, penetration test, data-retention and disaster-recovery policies, operational ownership, and contractual/compliance review. UnifyEMPI supplies technical controls but does not itself provide production approval or certification.
Detailed settings are documented in the configuration reference. Use identity concepts and frequently asked questions when selecting a tenant boundary or planning an existing-store onboarding. The portable chart is under deploy/helm/unifyempi, and the optional GCP reference infrastructure is under deploy/terraform.
FHIR R4:
GET /fhir/R4/metadataGET /.well-known/smart-configuration- Patient create, update, read, and canonical search
- Person read and search for reviewer scopes
POST /fhir/R4/Patient/$match
Reviewer API:
GET /api/v1/review-casesGET /api/v1/review-cases/{id}POST /api/v1/review-cases/{id}/decisionsPOST /api/v1/review-cases/manual-duplicatePOST /api/v1/review-cases/splitGET /api/v1/registry/patients/{id}GET /api/v1/registry/patients/{id}/duplicatesGET /api/v1/operations/summaryGET /api/v1/audit-eventsGETandPUT /api/v1/tenant/settings
HL7v2:
- Versions 2.3.1, 2.4, and 2.5.1
- ADT A01, A04, A08, A28, A31, A40, and A47
- MLLP over explicitly enabled plaintext (development only) or mutual TLS
Set exactly one RegistryProvider:Type:
InMemory: ephemeral development and provider-conformance tests.GcpHealthcare: GCP Healthcare API R4 using Application Default Credentials / Workload Identity.
The application depends only on IIdentityRegistryStore; Firely resources, GCP URLs, and database query concepts do not cross that boundary. A future PostgreSQL, SQL Server, or NoSQL adapter must pass the shared provider contract tests.
The GCP adapter applies a mandatory tenant meta.security label, injects _security into every search, requests strict handling, verifies the response self-link retained the filter, validates labels after direct reads, and uses tenant-labelled FHIR transactions for atomic writes.
Production requires:
- an external OIDC issuer (
Authentication:Authority); - one or more tenant definitions, each with exactly one active 256-bit-or-larger HMAC key;
- a configured durable provider;
- an unpacked
hl7.fhir.r4.core#4.0.1andfhir.r4.ukcore.stu2#2.0.2package directory for the bounded Firely validator pool; - TLS and client-certificate allow-lists for MLLP.
See configuration, matching and blocking rules, architecture, core paths and processing model, and the Helm values.
No raw inbound FHIR or HL7 bodies are retained by default. Logs contain event types, counts, latency, outcomes, and trace identifiers, not patient values.
This milestone does not include arbitrary clinical resources, runtime assembly loading, live federation, or a second durable database adapter.
Production NHS use still requires an external DPIA, clinical-safety case, penetration test, data-retention policy, and contractual/compliance review. This project supplies technical controls; it does not claim certification.
The original .NET Framework 4.5 prototype is preserved by the legacy-net45 Git tag. The active solution is UnifyEMPI.slnx.
The repository retains its original CC0 1.0 dedication.