-
Notifications
You must be signed in to change notification settings - Fork 0
Adapter Configuration Guide
This guide explains how to build the configuration map needed to connect your system to an Electronic Health Record (EHR) provider using ehr-adapter. In this library, everything is configured using plain Clojure data structures (maps, vectors, and keywords).
The root configuration is a single Clojure map. Think of it as a complete blueprint that tells the engine where the EHR server lives, how to log in step-by-step, how to handle network errors, and what medical endpoints are available.
| Key | Type | Required | Plain English Description & Live Behavior | Example |
|---|---|---|---|---|
:domain |
Keyword |
Yes | A unique, namespaced keyword identifying this specific clinic or setup. Used internally to route and catalog configurations. | :eclinicalworks/tenant-beta |
:base-url |
String |
Yes | The root web address of the EHR API. Rule: Do not add a trailing slash / at the end; the path compiler normalizes this automatically. |
"https://api.interop-ehr.com/v2" |
:http-client-fn |
Function |
Yes | The actual Clojure function your application uses to talk to the internet (like clj-http.client/request or http-kit). |
clj-http.client/request |
:middlewares |
Vector |
Yes | A list of plumbing functions that process data before it goes out. Must contain at least your base client adapter to handle network calls cleanly. | [ehr-adapter.middleware/clj-http-client] |
:auth |
Vector |
Yes | A step-by-step list of login rules. The engine runs them in order, meaning a later step can use tokens generated by an earlier step. | (See Authentication Layers) |
:network-config |
Map |
No | Settings for safety belts: network timeouts, retries when a server drops, and token refresh thresholds. | (See Network Configuration) |
:operations |
Vector |
No | A list of template paths for pulling clinical data (like Patients or Groups) that the engine turns into real functions. | (See Endpoint Operations) |
EHR systems rarely let you log in with a single password. Often, you must complete a multi-step handshake. To handle this, :auth takes a sequential vector of maps. The engine processes each map from top to bottom, passing any temporary cookies or access tokens forward down the line.
If a field needs a value that isn't known until the program runs (like a fresh token from a previous step or a patient ID passed on the fly), use a namespaced keyword starting with :ref/.
When the engine sees :ref/my-variable, it performs a strict lookup:
- It checks the live memory context for a value named
my-variable. - If missing, it looks at the extraction instructions you set up in the preceding layer's
:bindings. - If it still can't find anything, it halts immediately and throws a detailed exception to prevent sending corrupted or unauthenticated data to the EHR.
The :bindings key is declared exclusively within the layer that needs to receive the data. It is a configuration map where the receiving layer tells the engine: "In order for me to construct my request, I need you to go to the global response map, extract a piece of data, and assign it to me locally."
The structure of the map is as follows:
-
Key: The internal keyword that this layer will use (for example,
:client_assertion). -
Value: A standard lookup vector used to extract the data directly from the context (for example,
[:body :access_token]).
This way, the sending layer does not know—nor does it care—who will consume its data.
Used when the EHR or an API Gateway simply requires a static token or an application identifier injected into the request headers.
-
:type[Required]Must be exactly:api-key. -
:api-key[Required]The actual string token or secret key. -
:client-id[Optional] An optional application identifier string if the gateway asks for a specific app ID alongside the key. -
:bindings[Optional] Map used to extract specific data that the handler needs.
{:type :api-key
:api-key "app-gateway-secure-token-9901"
:client-id "integrator-service-id"}A traditional username and password exchange, typically sent to a login endpoint to obtain a short-lived token.
-
:type[Required]Must be exactly:basic-auth. -
:token-url[Required]The full URL address where the engine must send the credentials. Rule: Do not add a trailing slash/. -
:username[Required]The client or system username string. -
:password[Required]The client or system password string. -
:payload[Optional] An extra map of key-value parameters to include in the request body (e.g., specifying form fields the EHR requires). -
:bindings[Optional] Map used to extract specific data that the handler needs.
{:type :basic-auth
:token-url "https://auth.interop-ehr.com/v2/login"
:username "integrator_app"
:password "SecurePassword987!"
:payload {:grant_type "client_credentials"}}The standard industry flow for server-to-server communication (Client Credentials). It is highly flexible; you can pass credentials cleanly or map custom internal structural payloads for unique vendor requirements.
-
:type[Required]Must be exactly:oauth2. -
:token-url[Required]The central address of the OAuth2 token server. Rule: Do not add a trailing slash/. -
:grant-type[Required]The OAuth2 flow type string. Usually"client_credentials". -
:client-id[Required]Your application's unique client ID string. -
:client-secret[Required]Your application's secure client secret string. -
:scopes[Optional] A native Clojure vector of strings containing individual permissions (e.g.,["system/Patient.read" "system/Group.read"]). The engine automatically joins these with spaces into a singlescopeparameter right before network transmission to match the specification perfectly. -
:payload[Optional] A map used to inject custom parameters directly into the request body if an EHR implements non-standard token fields. -
:bindings[Optional] Map used to extract specific data that the handler needs.
{:type :oauth2
:token-url "https://auth.interop-ehr.com/v2/oauth2/token"
:grant-type "client_credentials"
:client-id "client_usr_prod_01x"
:client-secret "super-secret-oauth-string-xyz"}The official, ultra-strict security standard for medical software talking server-to-server. Instead of passwords, it secures connections by generating an encrypted, short-lived JSON Web Token (JWT) locally using cryptographic keys.
-
:type[Required]Must be exactly:smart-on-fhir/backend-services. -
:client-id[Required]The production client ID string issued to your application by the EHR developer portal. -
:token-url[Required]The EHR's official token authentication endpoint. Rule: Do not add a trailing slash/. -
:scopes[Required]A native Clojure vector of strings containing individual permissions (e.g.,["system/Patient.read" "system/Group.read"]). The engine automatically joins these with spaces into a singlescopeparameter right before network transmission to match the specification perfectly. -
:audience[Required]The authorization server's identity URL, embedded inside the JWT signature for validation. Per the SMART on FHIR specification, this claim is mandatory in the JWT assertion and is typically identical to the token URL. -
:key-id[Required]The key identifier (kid) string matching your registered public key profile in the EHR backend. The authorization server uses this value to locate the correct public key for signature verification. -
:algorithm[Required]The signing encryption standard string (e.g.,"RS384"or"ES384"). The SMART on FHIR specification mandates support for at leastRS384andES384. -
:private-key-path[Conditional] A string path pointing to your local private key file (e.g.,.pemor.key) used to sign the assertion. Exactly one of:private-key-pathor:private-keymust be provided. -
:private-key[Conditional] The private key material as a string, provided inline instead of via a file path. Exactly one of:private-keyor:private-key-pathmust be provided. -
:bindings[Optional] Map used to extract specific data that the handler needs.
{:type :smart-on-fhir/backend-services
:client-id "my-smart-app-id"
:token-url "https://fhir.epic.com/interconnect-fhiroauth/oauth2/token"
:scopes ["system/Group.read" "system/Patient.read"]
:audience "https://fhir.epic.com/interconnect-fhiroauth/oauth2/token"
:key-id "prod-key-v1"
:algorithm "RS384"
:private-key-path "resources/keys/production_private.pem"}A safety valve map used when an EHR does something entirely proprietary. It lets you run a custom Clojure function to shuffle, transform, or fix data structures mid-pipeline.
-
:type[Required]Must be exactly:custom. -
:handler[Required]A reference to a standalone Clojure function that receives the pipeline state and returns a transformed map. -
:data[Required]A custom configuration map passed directly into your handler function as context parameters. -
:bindings[Optional] Map used to extract specific data that the handler needs.
{:type :custom
:handler my-app.auth.transformer/restructure-json-payload
:data {:target-node [:response :auth_context :token]}}This section acts as your network safety settings, dictating how long the client should wait for a slow EHR server and how to retry safely if a call drops.
Co-dependency Rule: If you turn on
:retries, you must supply a:retry-delay-msvalue. Attempting to loop retries without a clear resting interval between attempts is an invalid state that will cause the configuration validator to reject the entire map.
| Key | Type | Required? | Plain English Description & Allowed Values | Example |
|---|---|---|---|---|
:timeout-ms |
Integer |
No | Maximum time in milliseconds to wait for a server response before giving up and cutting the wire. | 5000 |
:retries |
Integer |
No | How many times the engine should automatically try the request again if the network drops or throws a server error. | 3 |
:retry-delay-ms |
Integer |
No | The resting wait time in milliseconds before firing off the next retry attempt. | 200 |
:retry-on |
Vector |
No | A vector of specific HTTP status codes that should trigger a retry attempt. | [500 502 503 504] |
:retry-strategy |
Keyword |
No | The mathematical rhythm of retries. Allowed options: :linear (wait exactly the same time every turn) or :exponential (double the wait time each failure). |
:exponential |
:refresh-token-on |
Vector |
No | A vector of HTTP status codes (usually [401]) that mean "Our token expired". When hit, the engine clears its cache, re-runs the entire :auth list from scratch, and retries the call. |
[401] |
Operations are templates for the actual medical data endpoints you want to fetch. The engine reads these data maps and automatically compiles them into fast, fully executable Clojure functions.
Relative Path Rule: Path segments must be completely clean and relative. Never write out the full EHR domain URL here. The compiler takes your pieces, strips out any accidental slashes at the beginning or end of strings, and binds them perfectly onto the root
:base-urlwith a clean, single separator.
| Key | Type | Required? | Plain English Description & Live Behavior | Example |
|---|---|---|---|---|
:name |
Keyword |
Yes | The unique system keyword used to identify and trigger this function in your application code. | :$export |
:method |
Keyword |
Yes | The network action verb used over the wire. Allowed: :get, :post, :put, :patch, :delete, :head, :options, :trace, :connect. |
:get |
:path |
Vector |
Yes | A clean vector of segments to build the destination URL. String literals are hardcoded. Keywords (like :ref/group-id) are variables filled dynamically when you call the function. |
["v1/r4/Group" :ref/group-id "$export"] |
:expected-status |
Vector |
No | A vector of HTTP status codes the engine considers a successful response (e.g., [200 202]). |
[200 202] |
:base-headers |
Map |
No | Permanent HTTP headers that this specific operation always needs (e.g., custom payload formats or transaction rules). Can contain dynamic :ref/... lookups. |
{"Prefer" "respond-async"} |
:description |
String |
No | Plain-text documentation explaining the real-world purpose of the endpoint for team readability. | "Initialize an $export FHIR operation." |
Here is what a complete, valid production blueprint looks like when connecting to a standard interoperable EHR gateway. It uses a clean dual-stage authentication layout (Perimeter Key + Strict OAuth2) and sets up safe routing properties. Notice how none of the path segments or base URLs use messy trailing or leading slashes.
{:domain :provider-group/tenant-alpha
:base-url "https://api.interop-ehr.com/v2"
:http-client-fn clj-http.client/request
:middlewares [ehr-adapter.middleware/clj-http-client]
:auth [;; Layer 1: Secure Perimeter API Gateway
{:type :api-key
:api-key "app-gateway-secure-token-9901"
:client-id "integrator-service-id"}
;; Layer 2: Standard Core OAuth2 Client Credentials Flow
{:type :oauth2
:bindings {:secret [:client :secret]}
:token-url "https://auth.interop-ehr.com/v2/oauth2/token"
:grant-type "client_credentials"
:client-id "client_usr_prod_01x"
:client-secret :ref/secret}]
:network-config {:timeout-ms 5000
:retries 3
:retry-delay-ms 200
:retry-strategy :exponential
:retry-on [502 503 504]
:refresh-token-on [401]}
:operations [{:name :$export
:description "Initialize a bulk data export. Requires a map containing a valid :group-id."
:method :get
:path ["v1/r4/Group" :ref/group-id "$export"]
:base-headers {"Prefer" "respond-async"}}]}
An open-source Clojure library for EHR integration
Maintained by Samuel Carriles Baños • Licensed under MIT