← Writeups

The EHR Is a UI, Not a Service

August 6, 2026 · 5 min read

The EHR vendor didn't build you an API. They built a product for a doctor sitting at a desk. The fact that you need machine-readable data out of it is your problem, not theirs. You get a login screen, a session that expires, and a UI that can change overnight without notice. That's the integration surface. Now make it reliable.

The problem

The job: pull data from clinic systems on a schedule, across dozens of clinics, each running its own EHR instance, each with its own credentials — unattended, indefinitely.

The catch is what "EHR" actually means in practice:

  • Some are web-based. There's a login flow, a session cookie, a DOM you have to navigate like a user would. The session dies quietly, the MFA fires on re-auth, and the vendor changes the UI with no changelog.
  • Some are thick Windows clients. No web interface at all. You connect via RDP, automate a desktop, handle OTP automatically, and the only way to see what happened is to record the session.
  • None of them know you exist. You are reverse-engineering undocumented behavior with no support contract and no recourse.

"Connect once, read the data, retry on failure" is not a design here. A dead session isn't an error you retry — it's an authentication problem that requires MFA, which requires a human unless you've automated that too.

The solution

The way through was to stop modeling this as a data retrieval problem and start modeling it as a session management problem. The data is almost incidental. What the system actually has to do is maintain a live, authenticated connection to each EHR — permanently, without anyone touching it.

The session is the unit of work

For web-based EHRs, the connector owns a real browser. Not a headless scraper — a full browser, because the EHR's login flow does things that only work in a real session: JavaScript challenges, redirects, cookies that tie to fingerprints. The connector decodes the login behavior from observation, replays it on expiry, and keeps the session alive with background activity when there's nothing else to do. A session that goes idle too long doesn't fail gracefully — it disappears, and the next request gets a login page instead of data.

The hard part isn't the happy path. It's that MFA fires unpredictably. A session expires at 2 AM, re-authentication triggers a TOTP challenge, and there is no human to approve it. So the connector reads the OTP itself — the shared secret is stored securely, the code is generated on demand, and the re-auth flow is exercised in staging until it's indistinguishable from a human logging in. Session death is a system event, not a human problem.

When there is no browser

For thick-client EHRs, the problem is structurally different. There is no DOM, no HTTP session, no cookie jar. There is a Windows desktop, a native application, and a keyboard. The connector is a Go binary that runs on a Windows host, connects over RDP, and automates the desktop through the same interface a human would use — clicking buttons, reading screen state, entering credentials. OTP is handled the same way: generated automatically, typed into the field, submitted.

The binary is remotely deployable. It installs silently at the clinic, connects back on a schedule, and records every session to S3. That last part isn't optional — when something goes wrong with a UI you can't inspect remotely, the session recording is the only evidence you have. It's your audit trail and your debugging surface at the same time.

Multi-tenant means each clinic is its own failure domain

Every clinic has its own session, its own credentials, its own EHR instance. A connector that fails for Clinic A tells you nothing about Clinic B. The isolation is the point: one clinic's expired certificate or changed password doesn't cascade. Each connector manages its own session lifecycle independently and reports its own state. The system can have twenty connectors healthy and one stuck, and the one stuck shouldn't require finding the other twenty first.

The invariant is connection liveness

The load-bearing rule: at any point in time, each clinic's connector is either in a live authenticated session, in a documented re-authentication attempt, or in a terminal failure state that has fired an alert. Never in an ambiguous nothing — never silently wedged with no signal that anything is wrong. That rule forces the full enumeration of everything that can kill a session: expiry, MFA challenge, DOM change, RDP drop, host restart, network partition. For each one, the connector has a documented path back to live or a loud exit.

You can't visit the clinic to fix a broken connector. That constraint shapes everything. Recovery has to be automatic, the signal on failure has to be immediate, and the re-authentication path has to work the first time without a human in the loop — because there won't be one.

What was actually achieved

None of this gave the EHR an API. The EHR is still a UI — a brittle, undocumented one, built for a person, changed without notice, protected by MFA. What changed is that the system no longer needs it to be anything else.

You didn't solve the EHR's design. You built a layer on top of it that handles session death, automated authentication, multi-tenant isolation, and remote recovery — so the EHR's fundamental unsuitability for machine integration stops being a reliability problem. That's a very different kind of engineering. The EHR is the same bad integration surface it always was. The system just doesn't require it to be a good one.