The task
The practice works across several systems: Spruce for patient messaging, GoHighLevel for leads and intake, DrChrono for medical records and a support desk that was moving from Zoho Desk to Zendesk. Staff copied data between them by hand.
Two rules shaped everything. Patient data must never leave the protected environment, and every change to a patient record must be traceable afterwards.
The foundation
Everything runs in the practice's own AWS account: self-hosted n8n, a Postgres database, encrypted keys, an activity trail of the account and versioned backups. The n8n editor is not reachable from the internet, and every incoming webhook has to be signed.
Alerts, logs and the audit trail carry record IDs only, never patient details. All testing runs on synthetic records.
What I built
New leads from Spruce to GoHighLevel, live since July 2026. A new patient lead in Spruce appears in the CRM without staff involvement. The database keeps the link between both systems for 1,386 patient records. A duplicate guard stops the same person from being created twice, a short retry covers contacts that are not readable yet, and any lead without a CRM record after 24 hours raises an alert.
Support desk migration from Zoho Desk to Zendesk. 14,299 tickets and about 35,700 messages moved with their original dates, senders and statuses.
- I used Zendesk's import API rather than ticket creation, so no patient received a notification about an old ticket.
- Every email became its own comment, so old conversations read as they did before.
- The first archive block showed that an oversized request fails silently as a whole batch. I switched to batching by payload size, and the success rate held at 99.6%.
- I checked the result with a second, independent count rather than trusting the importer's own report. All 199 active tickets arrived with zero failures.
Medical records in DrChrono. A tag in GoHighLevel creates or updates the patient's chart in DrChrono and writes the chart link back to the CRM. A second flow moves insurance card photos from the CRM into the chart. Both are tested end to end against the DrChrono sandbox, and the production switch is waiting for the practice's credentials.
The riskiest case was a duplicate chart. DrChrono hides inactive patients from a normal search, so the flow searches by date of birth including inactive records. When the match is not exact, it stops and leaves a note for staff instead of guessing.
How it stays reliable
- An audit trail that cannot be rewritten. Database triggers block any edit or deletion of audit rows for every role, and the workflows can only add rows.
- Seven detectors on timers. A signed webhook check every five minutes, a check that the editor stays private, a failure monitor with separate alert classes (rejected credentials, error spikes, silence during clinic hours, stuck leads), a daily heartbeat and a daily backup.
- Changes in maintenance windows. Every release starts with a backup and has a rollback, and downtime stays between 15 and 40 seconds.
Results
- Spruce to GoHighLevel sync in production since July 2026, 1,386 patient records linked.
- 14,299 tickets and about 35,700 messages migrated, with no notifications sent to patients.
- DrChrono flows tested end to end in the sandbox and ready for the production switch.