Skip to content
LambogeniusDocs

HL7 Edge Gateway

Partners → MEDITECH

HTTPS REST with an API key per partner and SOAP with basic auth, into MEDITECH over MLLP.

Runs onWindows · Linux · macOS · DockerFree developer license for test traffic. Production traffic needs a paid license.

What it does

Outside partners can't open MLLP connections into the hospital, but they can call a web service. One HTTPS port (8443) serves two routes, told apart by their path. Both deliver to MEDITECH (a stand-in that answers AR "patient not found" for unknown patients) over MLLP:

RouteSourceModeWhat the partner gets back
reference-lab-resultsREST POST /hl7/results, one API key per lab, JSON ACKstore-and-forward200 with ack_code AA as soon as the result is stored. If MEDITECH rejects it (AR), it goes to dead letters.
clinic-registrationsSOAP 1.1 POST /Hl7Service, basic authpass-throughMEDITECH's own ACK inside the SOAP response: AA, or AR with MEDITECH's reason. AE from the gateway if MEDITECH can't be reached.

The sample is in 02-partners-to-meditech in the examples repository.

Diagrams

Architecture diagram of the 02-partners-to-meditech sample
Architecture. What runs where, and how the systems connect. Source: architecture.mmd
Data flow diagram of the 02-partners-to-meditech sample
Data flow. What happens to a message at each step, including the failure paths. Source: data-flow.mmd
Sequence diagram of the 02-partners-to-meditech sample
Sequence. One run of the sample, message by message. Source: sequence.mmd

The configuration

02-partners-to-meditech/gateway.toml

# Sample 02: partners → HTTPS (REST and SOAP) → HL7 Edge Gateway → MLLP → MEDITECH
#
# One HTTPS port (8443) serves two routes, told apart by their path:
#   reference-lab-results  POST /hl7/results     REST, a raw HL7 message as the body.
#                          Each partner has its own API key; the key's name is logged as the caller.
#                          Store-and-forward: the partner gets 200 once the result is stored, and the
#                          gateway delivers it to MEDITECH in order, retrying while MEDITECH is down.
#   clinic-registrations   POST /Hl7Service      SOAP 1.1, the HL7 message inside the envelope.
#                          Pass-through: the clinic gets MEDITECH's own ACK (AA, or AR with the reason)
#                          in the SOAP response. If MEDITECH cannot be reached, the gateway answers AE
#                          itself, and the clinic resends later.
#
# Secrets come from .env through ${NAME}.

node_id = "gateway-1"

[admin]
listen = "0.0.0.0:8080"
token = "${HL7EDGE_ADMIN_TOKEN}"
allow_payload_view = true

[store]
backend = "embedded"
path = "/var/lib/hl7-edge-gateway"

[[route]]
name = "reference-lab-results"
mode = "store_and_forward"

[route.source]
type = "http"
listen = "0.0.0.0:8443"
path = "/hl7/results"
ack_format = "json"                  # {"ack_code": "AA", "control_id": "…", "text": "…", "ack": "MSH|…"}
tls = { cert = "/etc/hl7-edge-gateway/certs/gateway.crt", key = "/etc/hl7-edge-gateway/certs/gateway.key" }
# allow_from = ["203.0.113.0/24"]    # in production, also limit each partner to its address range
auth = { type = "api_key", header = "X-API-Key", keys = { northside-reference-lab = "${NORTHSIDE_LAB_API_KEY}", valley-pathology = "${VALLEY_PATHOLOGY_API_KEY}" } }

[route.destination]
type = "mllp"
address = "meditech:6661"            # MEDITECH's inbound results interface

[route.store_and_forward]
on_reject = "dead_letter"            # MEDITECH answered AR: park it, keep the rest flowing
retry = { initial = "2s", max = "30s", multiplier = 2.0 }

[[route]]
name = "clinic-registrations"
mode = "passthrough"

[route.source]
type = "http"
listen = "0.0.0.0:8443"              # the same port; the TLS settings must match
path = "/Hl7Service"
format = "soap"
tls = { cert = "/etc/hl7-edge-gateway/certs/gateway.crt", key = "/etc/hl7-edge-gateway/certs/gateway.key" }
auth = { type = "basic", username = "lakeside-clinic", password = "${CLINIC_SOAP_PASSWORD}" }

[route.destination]
type = "mllp"
address = "meditech:6662"            # MEDITECH's inbound ADT interface

02-partners-to-meditech/.env.example

# Copy to .env and fill in. .env is ignored by git.
LAMBOGENIUS_LICENSE=
HL7EDGE_ADMIN_TOKEN=change-me-to-a-long-random-string

# One API key per partner (send it as X-API-Key). Use long random values in production.
NORTHSIDE_LAB_API_KEY=northside-sample-key-4f8a2c
VALLEY_PATHOLOGY_API_KEY=valley-sample-key-9b31d7

# Basic auth password for the clinic's SOAP calls (user lakeside-clinic).
CLINIC_SOAP_PASSWORD=clinic-sample-password

GATEWAY_VERSION=0.3.0

A one-off certs container creates a test CA and a certificate for localhost in ./certs on the first start:

02-partners-to-meditech/compose.yaml

# Sample 02: partners → HTTPS (REST and SOAP) → HL7 Edge Gateway → MLLP → MEDITECH
#   docker compose up -d          creates a test certificate, starts the gateway and a stand-in MEDITECH
name: hl7edge-sample-02

services:
  # One-off: a test CA and a server certificate for the gateway, in ./certs (kept if already there).
  certs:
    image: alpine:3.20
    volumes: ["./certs:/certs"]
    entrypoint: ["sh", "-c"]
    command:
      - |
        [ -f /certs/gateway.crt ] && exit 0
        apk add --no-cache -q openssl && cd /certs &&
        openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj "/CN=Sample 02 test CA" -keyout ca.key -out ca.crt 2>/dev/null &&
        openssl req -newkey rsa:2048 -nodes -subj /CN=localhost -keyout gateway.key -out gateway.csr 2>/dev/null &&
        printf 'subjectAltName=DNS:localhost,DNS:gateway,IP:127.0.0.1\n' > san.ext &&
        openssl x509 -req -in gateway.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -extfile san.ext -out gateway.crt 2>/dev/null &&
        chmod 644 gateway.key && rm gateway.csr san.ext ca.srl && echo "test certificates created in ./certs"

  gateway:
    image: lambogenius/hl7-edge-gateway:${GATEWAY_VERSION:-0.3.0}
    env_file: .env
    ports:
      - "8443:8443"   # HTTPS: REST /hl7/results and SOAP /Hl7Service
      - "8080:8080"   # admin console
    volumes:
      - ./gateway.toml:/etc/hl7-edge-gateway/hl7-edge-gateway.toml:ro
      - ./certs:/etc/hl7-edge-gateway/certs:ro
      - queue:/var/lib/hl7-edge-gateway
    depends_on:
      certs: { condition: service_completed_successfully }
      meditech: { condition: service_started }

  # Stand-in for MEDITECH's inbound MLLP interfaces (results on 6661, ADT on 6662).
  meditech:
    image: python:3.12-slim
    command: ["python", "-u", "/app/receiver.py"]
    volumes: ["./meditech:/app:ro"]

volumes:
  queue:

Step by step

  1. 1
    Start it.
    cd 02-partners-to-meditech
    cp .env.example .env            # add your license
    docker compose up -d

    The commands below are for Linux and macOS. On Windows, use curl.exe with --ssl-no-revoke (Windows can't check revocation for a test CA), or run .\demo.ps1.

  2. 2
    A reference lab posts a result with its API key.
    curl --cacert certs/ca.crt \
      -H "X-API-Key: northside-sample-key-4f8a2c" \
      -H "Content-Type: x-application/hl7-v2+er7" \
      --data-binary @messages/oru-r01-ref-1.hl7 \
      https://localhost:8443/hl7/results
    {"ack":"MSH|^~\\&|MEDITECH|…\rMSA|AA|REF-3001\r","ack_code":"AA","control_id":"REF-3001","text":""}
    
    # A wrong or missing key never reaches the queue:
    HTTP 401  missing or unknown API key in x-api-key

    With RUST_LOG=info,hl7_edge_gateway=debug, the log names the partner by its key: caller="northside-reference-lab".

  3. 3
    The clinic registers a patient over SOAP.

    The request is an ordinary SOAP 1.1 envelope, with the HL7 message as the text of an element:

    02-partners-to-meditech/messages/soap-adt-a04.xml

    <?xml version="1.0" encoding="utf-8"?>
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
      <soap:Body>
        <SubmitHL7 xmlns="urn:lakeside-clinic:hl7">
          <message>MSH|^~\&amp;|LAKESIDE_EHR|LAKESIDE_CLINIC|MEDITECH|GENERAL_HOSPITAL|20261019110000||ADT^A04^ADT_A01|CLN-4001|T|2.5.1&#13;EVN|A04|20261019110000&#13;PID|1||100131^^^GENERAL_HOSPITAL^MR||LEE^MIN||19840722|F&#13;PV1|1|O|CLINIC^^^LAKESIDE&#13;</message>
        </SubmitHL7>
      </soap:Body>
    </soap:Envelope>
    
    curl --cacert certs/ca.crt -u lakeside-clinic:clinic-sample-password \
      -H 'Content-Type: text/xml; charset=utf-8' -H 'SOAPAction: "urn:lakeside-clinic:hl7/SubmitHL7"' \
      --data-binary @messages/soap-adt-a04.xml https://localhost:8443/Hl7Service
    
    <soap:Envelope …><soap:Body><SubmitHL7Response xmlns="urn:lakeside-clinic:hl7">
      <ack>MSH|^~\&amp;|MEDITECH|…&#13;MSA|AA|CLN-4001|filed&#13;</ack>
    </SubmitHL7Response></soap:Body></soap:Envelope>

    The gateway found the HL7 text by itself (the first element that starts with MSH) and answered with <SubmitHL7Response><ack>, in the request's namespace. An unknown patient gets MEDITECH's AR in the same place: MSA|AR|CLN-4099|patient not found: 555002.

    Two routes on HTTPS port 8443: reference-lab-results to mllp meditech:6661 and clinic-registrations (SOAP) to meditech:6662
    Both HTTPS routes on one port. The SOAP route's last error is MEDITECH's AR for the unknown patient.
  4. 4
    MEDITECH goes down.
    docker compose stop meditech

    Results posted now still get 200 with AA: they are queued. A SOAP registration gets AE with the connection error, because pass-through stores nothing.

    reference-lab-results with 2 queued, last error meditech:6661 connection failed
    Two results waiting for MEDITECH, with the connection error.
  5. 5
    MEDITECH comes back.

    docker compose start meditech. Within 30 seconds the two results are filed, in order.

  6. 6
    A result for a patient MEDITECH doesn't know.

    Post oru-r01-ref-unknown-patient.hl7. The lab gets AA (it's stored); MEDITECH then answers AR, so the gateway moves it to dead letters:

    Dead letter REF-3099: rejected: AR ACK: patient not found: 555001
    MEDITECH's own reason, kept with the message.

Adapting it

  • Certificate: use one your partners trust. Every route on a port must have the same tls.
  • Partners: one entry per partner under keys, and allow_from with each partner's address ranges. For stronger authentication, use mutual TLS (tls.client_ca) or OAuth2 tokens. See Security.
  • A different WSDL: soap = { message_element = "…", response_template = "…" }, or version = "1.2". See HTTP, SOAP and MLLP out.