Every patient was typed twice. Now a browser does the second typing.

A paediatric therapy clinic ran intake in Salesforce and clinical records in an EMR that shipped without an API. Staff re-keyed every name, date of birth, insurance ID and case note by hand. We put a headless browser on Heroku in the gap, and it has done the second typing ever since.

Client
A paediatric therapy clinic
Sector
Healthcare
Platforms
Heroku · Salesforce · Integration
Engagement
3-month engagement
Integration surface
The EMR’s own web portal. No API, no webhook, no export.
Study
03 of five

One patient intake, before and after

Struck through: the way it used to work
Systems openedTwo. Salesforce, then the EMR portal.One. Salesforce, and nothing else.
Times a field is typedTwice. Once in each system.Once. Salesforce is the source of truth.
Who does the second typingClinic staff, by hand.A headless Chrome session on a Heroku worker.
Hours coveredWhile somebody is at a desk.Around the clock, unsupervised.
When it goes wrongFound later, if somebody notices.Written back onto the Salesforce record as an error.

Why it stayed that way.

Four constraints

Healthcare operations teams rarely choose their software. The EMR is mandated, the CRM is the enterprise standard, and the gap between them quietly becomes a staffing problem. Here it was one clinic, two systems, and the same information entered field by field, form by form, twice.

  1. 01

    Every intake meant opening both systems and entering the same details twice: the patient, the parent or guardian, the insurance cover, and the clinical case notes.

  2. 02

    Transcription errors carry real risk in a clinical setting. A wrong date of birth, an incorrect insurance ID or a missing parent phone number turns into a billing, scheduling or care-coordination problem downstream.

  3. 03

    Time spent typing the same record twice was not spent on patient care, scheduling or clinical support — the work the clinic actually hired for.

  4. 04

    The EMR vendor offered no public API, no webhooks and no export endpoint. The only integration surface was the portal a human logs into.

    This one decides the architecture

What we ruled out first.

Four candidates, none of them fit

A missing API is not a configuration problem — it is an architectural one. Zapier, MuleSoft and Heroku Connect all want a REST API or a database at the far end, and this system exposes neither. When the only interface is a JavaScript-heavy portal, the only viable bridge is a programmatic browser that uses it the way a person does.

ZapierNo-code automation

DoesSimple trigger-and-action workflows between systems that already have connectors.

Rules it outNeeds an API or a native connector. No EMR connector exists, and it cannot drive a portal.

MuleSoftiPaaS

DoesEnterprise-grade API connectivity with real integration logic behind it.

Rules it outOnly ever as capable as the APIs it is given. On the EMR side there is nothing to connect to.

Heroku ConnectDatabase sync

DoesTwo-way Salesforce and Postgres sync with no custom code to maintain.

Rules it outSyncs to a database, not to a portal. The EMR has no database access layer to point it at.

Carrying on by handThe status quo

DoesAlways works. No technical risk and nothing to build.

Rules it outStaff time on every patient, transcription errors nobody catches, and no path as volume grows.

What we built instead.

Four stages, one queue between them

A two-process Heroku application. The web process owns the Salesforce OAuth credentials; the worker process listens to a RabbitMQ queue, drives the portal, and reports back. Nothing in Salesforce waits for a browser.

  1. 01

    Salesforce triggers the queue

    Salesforce → RabbitMQ

    When a patient record is created or updated, an Apex trigger publishes a job to a RabbitMQ queue on CloudAMQP. The queue decouples the event from the work: Salesforce does not wait for the browser, bursts do not stack up Puppeteer sessions, and a failed job is nack’d rather than lost.

  2. 02

    Credentials cached in Redis

    Web dyno → Heroku Redis

    The web process handles the Salesforce OAuth 2.0 flow and keeps access tokens, refresh tokens and instance URLs in Heroku Redis. jsforce refreshes expired tokens and writes them back, so the integration stays connected without anyone re-authenticating it by hand.

  3. 03

    Puppeteer drives the portal

    Worker → EMR portal

    For each job the worker starts headless Chrome through the Google Chrome buildpack, logs in, and works the interface: clicking, selecting, filling fields, waiting for network idle before each step. It notices the idle lock screen and clears it, and it reads a field before writing it, so only values that actually changed are touched.

  4. 04

    The EMR ID goes back to Salesforce

    Worker → Salesforce

    The worker lifts the EMR patient ID out of the portal and writes it back through an Apex REST endpoint, so the two records are linked by ID and the next update finds the right patient without searching. If anything fails, an error callback puts that on the record rather than leaving it quietly stale.

What it is made ofTen components

RuntimeNode.js · TypeScript

Web frameworkExpress.js

Browser automationPuppeteer, headless Chromium

Salesforce APIjsforce, OAuth 2.0 and Apex REST callbacks

Job queueRabbitMQ via CloudAMQP

Token cacheHeroku Redis (mini)

Process managementThrong, clustered workers

HostingHeroku, web and worker dynos

Chrome runtimeGoogle Chrome buildpack

LoggingPapertrail

Engineering notes.

Why it runs at 3am on a Monday

A browser as the integration layer

Driving a full Chromium instance is nobody’s first-choice integration pattern, but with no API it is the only one that exists. The architecture treats the browser as a typed interface: CSS selectors as contracts, network idle as the synchronisation point, and read-before-write at field level so nothing is mutated without cause.

The queue is what makes it survivable

A synchronous call would mean Salesforce waiting on Puppeteer — a multi-second operation that can time out, stall on a lock screen, or meet a DOM it did not expect. RabbitMQ absorbs the bursts, holds failed jobs instead of dropping them, and lets the worker go at its own pace without blocking a Salesforce transaction.

Lock screens and idempotent writes

The portal throws a password re-entry dialog after an idle period. The worker checks for that overlay at every interaction point and clears it before continuing. Writes are idempotent too: the current value is read first, and the field is only touched if it differs, which keeps portal-side validation from firing on fields that never changed.

Add-ons instead of infrastructure

The Chrome buildpack puts a browser on the dyno without Docker. Redis caches tokens, CloudAMQP runs the queue, Papertrail collects the logs. No container orchestration and nothing of our own to look after — the whole stack deploys with a git push.

What it settled.

Three figures, still true
0

Records re-entered by hand. Every patient sync runs without a person in it.

24/7

Coverage. The worker runs around the clock and nobody supervises it.

0

Transcription errors between the two systems. Salesforce holds the record, and it is typed once.

What this one shows

Four things you can hold us to.

A case study is only worth reading if it generalises. These are the parts of this build that would show up again on yours.

  1. We find the integration path, even when there isn’t one.No API does not mean no solution. It means a different kind of engineering. Treating a headless browser as a legitimate integration layer rather than a workaround is what made this solvable at all.
  2. Architecture decisions prevent operational debt.A direct synchronous call would have worked on day one and broken under the first timeout. Queue decoupling, token caching and error callbacks are the difference between something that works at 3am and something that works while you watch it.
  3. Clinical constraints get defensive engineering.Patient data accuracy is not a UX concern. Idempotent writes, lock-screen detection and failure callbacks exist because a silent error in a medical context is not something you get to explain later.
  4. Platform choice is an operating cost.Heroku’s add-ons removed a whole infrastructure footprint: no Redis to run, no AMQP cluster to stand up, no Chrome runtime to containerise. The add-ons are the infrastructure, and that simplicity compounds for as long as the integration runs.