Skip to content

The four-plane architecture

The domain controller is the centre of gravity, but on its own it is just identity. A real hospital is identity plus the clinical systems that consume it, the activity flowing through those systems, and the plumbing that connects them. The lab models all four as separate, reproducible Docker stacks, each its own repository, joined by shared networks.

flowchart LR
  subgraph identity["Identity plane"]
    dc["Samba AD DC · dc1<br/>Kerberos · LDAP · DNS"]
  end
  subgraph application["Application plane"]
    emr["OpenEMR<br/>emr.l.supported.systems"]
    db[("MariaDB")]
    emr --- db
  end
  subgraph activity["Activity plane"]
    sim["Simulated Hospital<br/>sim.l.supported.systems"]
  end
  subgraph integration["Integration plane"]
    oie["OpenIntegrationEngine<br/>integration.l.supported.systems"]
  end

  emr -. "LDAPS bind-as-user" .-> dc
  sim -- "HL7v2 / MLLP" --> oie
  oie -- "JDBC write" --> db
PlaneStackRepoWhat it is
IdentitySamba AD DC (dc1)samba-domain-controllerThe Kerberos/LDAP domain — users, groups, the PHI access boundary
ApplicationOpenEMR + MariaDBgh-openemrA real electronic medical record clinicians log into
ActivitySimulated Hospitalgh-simhospitalA synthetic HL7v2 generator — a hospital that is always “open”
IntegrationOpenIntegrationEnginegh-integrationThe HL7 interface engine that routes activity into the record

Each plane is disposable and defined in code. You can run just the DC, or the DC plus the EMR, or the whole ecosystem.

The instinct is to point the HL7 feed straight at the EMR. Real hospitals do not do this, and neither do we. OpenEMR’s native HL7v2 inbound is weak; its strength is being the clinician-facing chart. So an interface engine sits in the middle to receive, acknowledge, transform, and route — exactly the role Mirth Connect plays in production hospitals. We use OpenIntegrationEngine, the FOSS fork that continues Mirth after it went proprietary at version 4.5.

Simulated Hospital ──MLLP (HL7v2)──▶ OpenIntegrationEngine ──JDBC──▶ OpenEMR
(activity) (integration) (application)

This is the faithful topology, and it means each piece stays good at one job: the simulator generates, the engine integrates, the EMR presents.

The simulator follows YAML pathways — believable patient journeys (ED chest pain, acute kidney injury, surgical admission). Each step emits HL7v2 messages over MLLP on port 6661. The engine’s single channel parses every message and writes the corresponding OpenEMR records, all idempotent:

HL7 segment / eventOpenEMR destination
PIDpatient_data (the patient)
PV1form_encounter + forms (the visit, keyed on visit number)
OBR / OBXprocedure_orderprocedure_reportprocedure_result (labs)
AL1lists type allergy
DG1lists type medical_problem
PR1lists type surgery
ADT^A02transfer — updates the encounter
ADT^A03discharge — sets date_end + disposition
ADT^A11 / A13cancel admit / cancel discharge

The result is a chart that fills itself: open a synthetic patient and you see their allergy, their diagnosis, their surgery, and their lab panels with reference ranges and abnormal flags — none of it hand-authored, all of it arriving as live HL7 traffic.

The application plane closes the loop back to identity: clinicians log into OpenEMR with their Active Directory credentials over an encrypted LDAP connection, binding as the user themselves. OpenEMR never stores the password — disable an account in AD and the chart login is gone too. See OpenEMR ↔ AD authentication.

Both production paths land in the same tables and render through the same OpenEMR widgets, which is the mark of doing the integration correctly:

  • The live stream — synthetic patients flowing in continuously through the HL7 pipeline.
  • A curated caseload — a small, hand-seeded “diagnostic caseload” assigned to a specific physician, for a stable set of recognizable charts in a demo.

OpenEMR cannot tell them apart; both are just rows it would have written itself.

The simulator can also emit FHIR R4 resources. In this lab the FHIR export works for Patient, Encounter, AllergyIntolerance, Condition, Location, and Practitioner, but not Observation — a google/fhir library-version incompatibility in the from-source build breaks the lab-value (Quantity) marshalling. HL7v2/MLLP, the path the lab actually uses, is unaffected. The full root cause is in the gh-simhospital README.

  • samba — the bridge sibling app servers use to reach the DC for LDAP.
  • hl7-bus — a dedicated bridge carrying the MLLP feed (Simhospital → OIE) and the JDBC write (OIE → OpenEMR’s MariaDB). The MariaDB joins it but is never published to the host.
  • caddy — the edge reverse proxy fronting every web UI on a *.l.supported.systems hostname.

To bring the planes up and wire them together, see Run the clinical ecosystem.