One API, 27 PMS: How Our Adapter Architecture Scales
Every dental PMS stores data differently — a SQL database here, a thick-client integration there, a cloud-only system with no local database at all. Presenting all of them as one clean API is an architecture problem. Here is how we solve it.
“27 integrations” is not 27 API clients
When we say CRMBridge connects to 27 practice management systems, it’s tempting to picture 27 tidy REST clients — a folder of SDKs, each with a base URL and an API key. That is not what a dental PMS integration looks like. Behind that number are 27 fundamentally different data-access models, and the differences aren’t cosmetic. They’re architectural.
Some PMSs are a SQL database sitting on the practice server — you connect and query. Some are a Windows thick client with no meaningful data layer you’re allowed to touch, so you drive the application itself over an RPC or automation surface. Some are cloud-only, with no local database at all; you authenticate the way a staff member would and read the practice’s data through the vendor’s own private API. And some barely have an API — imaging systems that hand you a folder of file exports and call it a day.
One partner building on CRMBridge should not have to care which of those four worlds a given practice lives in. A developer calling GetPatients shouldn’t need to know whether that resolves to a SQL query, a thick-client automation call, or an authenticated cloud request. Making that true is the entire job. This is a companion to our earlier post, Build vs Buy for dental integrations — that post argues you shouldn’t build this yourself; this one is about how the thing you’d be buying actually works.
The adapter pattern: one interface, many translators
The load-bearing decision in the whole system is this: every PMS is wrapped by an adapter, and every adapter implements exactly one common interface. Same method names, same signatures, same return shapes — GetPatients, GetAppointments, GetProviders, GetProcedureCodes on the read side, and writebacks like CreateNewPatient, UpdateAppointment, and CreateNote on the write side.
The adapter’s only job is translation. It takes that clean, uniform contract and turns each call into whatever the underlying PMS actually speaks — a parameterized SQL statement, a sequence of automation calls against a thick client, an authenticated HTTP request to a vendor cloud, a scan of an export directory. All of the PMS-specific weirdness lives inside the adapter and nowhere else. That containment is the point: the mess is real, but it is quarantined.
// One contract that every PMS must satisfy
interface PmsAdapter {
IEnumerable<Patient> GetPatients();
IEnumerable<Appointment> GetAppointments();
IEnumerable<Provider> GetProviders();
Patient CreateNewPatient(NewPatient p);
void UpdateAppointment(Appointment a);
void CreateNote(Note n);
}
// A SQL-based PMS: the adapter runs queries
class SqlPmsAdapter : PmsAdapter {
// GetPatients() -> SELECT ... FROM patients ...
// CreateNote() -> INSERT INTO notes ...
}
// A cloud-only PMS: same interface, very different plumbing
class CloudPmsAdapter : PmsAdapter {
// GetPatients() -> authenticate, call vendor's private API
// CreateNote() -> POST to the vendor cloud as the practice
}The canonical model: the narrow waist
An interface alone isn’t enough. Two PMSs can both return “patients,” but one calls the field PatNum and the other patient_guid; one stores a single name string, the other splits first and last; one gives you appointment status as an integer code, the other as free text. If those differences leaked upward, every partner would still be writing per-PMS logic — just one layer higher.
So each adapter doesn’t just call the right method — it maps a wildly different source schema into one normalized set of models: a canonical patient, a canonical appointment, a canonical provider, a canonical procedure. Downstream, nothing else ever sees the source shape. The public API, the sync engine, the partner apps — they only ever handle the canonical form.
This is the “narrow waist” of the architecture. Many messy inputs on one side, many consumers on the other, and a single normalized model in the middle that both sides agree on. The narrow waist is what makes one API possible across 27 systems that share almost nothing in common.
The registry: resolving the right adapter at runtime
Adapters register themselves by PMS name. When a practice is configured with a given CRM — say the config says its system is “Tracker” or “Curve Hero” — the integrator resolves the correct adapter at runtime from that name. The core engine never contains a giant switch statement of PMS-specific branches. It asks the registry for the adapter matching the configured system and then talks to it through the common interface, blind to which one it got. The practical upshot: adding a PMS means adding an adapter, not touching the core. New adapters slot into the registry; the sync engine and API are untouched.
On-prem and cloud: same interface, different deployment
Where an adapter runs depends on where the data lives. For PMSs whose data sits on a practice’s local server, an on-prem integrator runs the adapter right next to the data, reads it locally, and syncs the canonical results up to the cloud. For cloud-only PMSs, there’s nothing on the practice server to sit next to — the adapter authenticates directly to the vendor cloud and reads from there.
Both cases implement the same interface and produce the same canonical model. Deployment is a detail underneath the contract, not a fork in it. A partner integrating a cloud PMS and a partner integrating an on-prem PMS write identical code.
The four awkward worlds, handled
Every hard case a dental integration can throw at you falls into one of a few shapes. Here’s how each one gets absorbed behind the same interface:
The SQL-based PMS.
The adapter connects to the practice database and queries it directly. The cleanest case — but the queries still get mapped into the canonical model so callers never see the raw schema.
The cloud-only PMS with no local database.
There’s nothing to query on-site. The adapter authenticates as the practice and drives the vendor’s own API to read and write — this is how newer cloud systems like Curve Hero are handled. Same GetPatients, entirely different plumbing underneath.
The thick-client PMS.
No supported data layer, so the adapter drives the application itself through an automation or RPC surface — the same operations a user would perform, executed programmatically and normalized on the way out.
Imaging and file exports.
Some systems only produce files. The adapter watches for exports and maps them into canonical documents attached to the right patient, so a folder of images becomes chart-ready records like any other source.
The supported list keeps growing on exactly these terms. Recently we added Tracker (by The Bridge Network Inc.) — a new adapter behind the same interface, invisible to every partner who was already integrated.
Why this scales
The economics of this design are the whole argument. The cost of PMS #28 is a single adapter, fully isolated from the other 27. It touches its own translation code and the registry entry — nothing else. A bug in one adapter cannot break another, because they share no runtime path beyond the interface and the canonical model. And a partner writes their app once, against the canonical API, and inherits every PMS we support — including the ones we add after they ship.
Compare that to the alternative laid out in Build vs Buy: maintaining 27 integrations in-house, each a different data-access model, each with its own failure modes, each needing care every time a vendor changes an API or ships a client update. That isn’t 27 units of work — it’s 27 ongoing, independently breaking commitments. The adapter architecture doesn’t make that work disappear; it concentrates it into one isolated place per system and keeps it away from everyone building on top.
The interface is the product
It’s worth being precise about what CRMBridge actually sells. The canonical interface — the clean, uniform contract a partner codes against — is the product. The adapters are how the messy world gets there. Every SQL quirk, every thick-client automation, every cloud authentication dance, every imaging export exists so that one method call means the same thing across 27 systems. Get the waist right, and the number on the end — 27, 28, 50 — stops being an architecture problem and becomes a list you extend.
Integrate once. Get every PMS.
Write your app against a single canonical API and inherit 35+ dental and veterinary practice management systems — SQL, thick-client, cloud-only, and imaging — without ever touching a PMS-specific line of code.