ADR-004: Booking Product Architecture¶
IMPLEMENTED — All milestones complete (M0-M6)
This ADR has been fully implemented. OscarSoapAdapter is in production. BookingEngine with advisory locking is live. See Adapter Implementations.
Where booking intelligence lives — and why NOT in the OSCAR bridge
Status: Approved Date: February 13, 2026 Decision Makers: VitaraVox Architecture Team Supersedes: None (greenfield decision)
Context¶
VitaraVox is building an appointment booking product on top of OSCAR EMR. The current development environment uses a custom REST-SOAP bridge that translates modern REST calls into OSCAR's SOAP API and direct MariaDB queries. The bridge has direct database access and already parses OSCAR's schedule templates.
The fundamental question: Should booking intelligence (availability calculation, conflict detection, double-booking prevention) live in the bridge, in the platform, or somewhere else?
This decision determines how much work is required when connecting to commercial OSCAR instances that clinics actually run in production.
Decision¶
All booking intelligence lives in the VitaraVox platform layer (Layer 1), running on our infrastructure. The bridge is a dev/testing tool only. Production customer connections use a SOAP Adapter that connects directly to OSCAR's standard SOAP API.
No custom code is deployed on customer OSCAR instances. Ever.
The Three-Layer Architecture¶
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ LAYER 1: BOOKING PRODUCT (our servers, our infrastructure) │
│ ───────────────────────── │
│ Contains ALL booking intelligence: │
│ • Availability calculation (template slots − existing appts) │
│ • Conflict detection (overlap checking, not just exact match) │
│ • Double-booking prevention (slot locking in our DB/Redis) │
│ • Bookinglimit enforcement │
│ • Post-booking verification │
│ • Booking queue / serialization │
│ • Clinic rule enforcement (hours, max advance, min notice) │
│ • Patient-facing UX / Voice Agent │
│ │
│ This is our competitive advantage. Written once, tested once. │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 2: EMR ADAPTERS (pluggable, one per integration type) │
│ ──────────────────── │
│ Abstract interface hiding HOW we talk to OSCAR: │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ SOAP Adapter │ │ REST Adapter │ │ Bridge Adapter│ │
│ │ (universal │ │ (OSCAR 15+, │ │ (dev/test │ │
│ │ production) │ │ partial) │ │ ONLY) │ │
│ └──────────────┘ └──────────────┘ └───────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 3: CUSTOMER'S OSCAR (their infra — hands off) │
│ ────────────────────────── │
│ We connect to it. We install NOTHING on it. │
│ ┌───────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Self-hosted│ │ Juno (WELL) │ │ KAI / Other │ │
│ │ OSCAR │ │ managed │ │ managed │ │
│ └───────────┘ └─────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Rationale: Why NOT the Bridge¶
1. Access — You Don't Get SSH¶
Most Canadian clinics run managed OSCAR: Juno (WELL Health), KAI Innovations, PurpleHub, CloudPractice. When you sign a customer, you get OAuth or WS-Security credentials. You do not get:
- SSH access to their server
- Permission to run Docker containers
- Direct database credentials
- Filesystem access of any kind
The sidecar deployment model (bridge alongside each customer's OSCAR) is physically impossible for the majority of the market.
2. Liability — Direct DB Access = Regulatory Exposure¶
The moment you put a component with direct database write access on a customer's production EMR, you own every data integrity problem that occurs. Under Canadian healthcare privacy law:
- PHIPA (Ontario) — Personal Health Information Protection Act
- HIA (Alberta) — Health Information Act
- PIPA (British Columbia) — Personal Information Protection Act
A custom bridge touching patient data on a customer's server creates a compliance surface that must be audited per customer. Standard API integration (SOAP/OAuth) is the same pattern every certified booking system uses. The compliance story is clean and precedented.
3. Maintenance — N Bridges = N Maintenance Points¶
Every OSCAR version upgrade, every Juno patch, every MariaDB update could break the bridge. With N customers, that's N separate breakage points, each requiring coordination with the customer's IT team for access and deployment. This does not scale.
4. Precedent — How Commercial Integrations Actually Work¶
| System | Deploys Code on EMR? | Connection Method |
|---|---|---|
| Cortico | No | OAuth/SOAP from Cortico cloud |
| Ocean (CognisantMD) | No* | Cloud Connect via OSCAR's standard API |
| Veribook | No | SOAP/OAuth from Veribook cloud |
| Porton Health | No | SOAP/OAuth from Porton cloud |
| OSCAR Self-Book | Yes (built-in) | Internal — it's part of OSCAR itself |
Ocean's "Cloud Connect" is a configuration in OSCAR's admin, not a deployed sidecar.
Every commercial OSCAR integration connects from their own cloud via standard APIs. This is the proven model. Every clinic IT admin understands "give them OAuth creds."
What Every OSCAR Instance Exposes¶
| Always Available (since OSCAR 12) | Usually Available (OSCAR 15+) | Never Available to You |
|---|---|---|
SOAP API (/oscar/ws/*) |
REST API (/oscar/ws/services/*) |
Direct database access |
ScheduleWs: getDayWorkSchedule, addAppointment, getAppointmentsForDateRangeAndProvider2, getScheduleTemplateCodes |
Demographics search/get, some scheduling | SSH / filesystem access |
DemographicWs: searchDemographic, getDemographic, searchDemographicByAttributes |
OAuth 1.0a authentication | Custom sidecar containers |
ProviderWs: getProviders2 |
Custom Java code in Tomcat | |
| WS-Security UsernameToken auth |
Critical: getDayWorkSchedule is SOAP-only. No REST equivalent exists. This is the most important method for booking — it returns the schedule template for a provider on a given date.
Adapter Interface Contract¶
Every adapter implementation must provide these methods:
getTemplateAvailability(providerId, date)
→ { slots: [{ startTime, endTime, code, duration, bookinglimit }] }
getExistingAppointments(providerId, startDate, endDate)
→ { appointments: [{ id, demographicId, startTime, endTime, status }] }
getTemplateCodes()
→ { codes: [{ code, description, duration, bookinglimit, color }] }
createAppointment({ demographicId, providerId, date, startTime, duration, reason })
→ { appointmentId }
updateAppointment(id, { date, startTime, duration, reason, status })
→ { success }
cancelAppointment(id)
→ { success }
getProviders()
→ { providers: [{ id, firstName, lastName, specialty }] }
searchPatients(query)
→ { patients: [{ id, firstName, lastName, phone, hin }] }
getPatient(id)
→ { patient: { id, firstName, lastName, dob, ... } }
SOAP Adapter (Universal Production Connector)¶
| Method | SOAP Call |
|---|---|
getTemplateAvailability() |
ScheduleWs.getDayWorkSchedule → parse DayWorkScheduleTransfer |
getExistingAppointments() |
ScheduleWs.getAppointmentsForDateRangeAndProvider2 |
getTemplateCodes() |
ScheduleWs.getScheduleTemplateCodes |
createAppointment() |
ScheduleWs.addAppointment |
cancelAppointment() |
ScheduleWs.updateAppointment (status → 'C') |
getProviders() |
ProviderWs.getProviders2 |
searchPatients() |
DemographicWs.searchDemographic / searchDemographicByAttributes |
Bridge Adapter (Dev/Test Only)¶
| Method | Bridge Endpoint |
|---|---|
getTemplateAvailability() |
GET /api/v1/appointments/availability (DB query) |
getExistingAppointments() |
GET /api/v1/appointments (SOAP passthrough) |
createAppointment() |
POST /api/v1/appointments (SOAP passthrough) |
cancelAppointment() |
DELETE /api/v1/appointments/:id (SOAP passthrough) |
BookingEngine — Core Logic (Platform Layer 1)¶
getTrueAvailability(providerId, date)¶
1. adapter.getTemplateAvailability(providerId, date) → raw template slots
2. adapter.getExistingAppointments(providerId, date, date) → booked appointments
3. FOR each template slot:
count = appointments overlapping this slot (not just exact startTime match)
limit = slot.bookinglimit (from template code, 0 = unlimited)
IF count >= limit: mark OCCUPIED
ELSE: mark AVAILABLE (remaining = limit - count)
4. Apply clinic rules:
- Within operating hours?
- Beyond minAdvanceBookingHours?
- Within maxAdvanceBookingDays?
5. Return only genuinely available slots
bookAppointment(providerId, patientId, slot)¶
1. ACQUIRE LOCK
Key: "provider:{id}:date:{date}:slot:{time}"
In YOUR Redis/DB — NOT OSCAR's database
If lock taken → return SLOT_TAKEN immediately
2. FRESH AVAILABILITY CHECK
adapter.getTemplateAvailability(providerId, date)
→ Is this a valid template slot?
→ What's the bookinglimit?
adapter.getExistingAppointments(providerId, date, date)
→ How many appointments already overlap this slot?
IF existing_count >= bookinglimit:
RELEASE LOCK → return SLOT_TAKEN
3. CREATE IN OSCAR
adapter.createAppointment({...})
4. VERIFY
adapter.getExistingAppointments(...)
Confirm appointment exists and no new conflicts appeared
(handles multi-system coexistence: Cortico/Ocean/front desk
also writing to the same OSCAR)
IF conflict detected post-write:
adapter.cancelAppointment(newApptId)
RELEASE LOCK → return SLOT_TAKEN
5. RELEASE LOCK → return SUCCESS
This pattern is adapter-agnostic. It works identically whether the adapter talks SOAP, REST, or the bridge.
Bridge Disposition¶
The REST-SOAP bridge does not die — it changes roles:
| Current Role | Future Role |
|---|---|
| Only way to talk to OSCAR | Reference implementation for the SOAP Adapter |
| Integration layer (booking logic candidate) | Dev/testing convenience tool |
| Potential production connector | Useful for YOUR instance's direct-DB features (registration, quick search) |
| — | NOT part of the portable booking product |
Migration Path¶
| Phase | Action | Customer Impact |
|---|---|---|
| Phase 1 (now) | Build BookingEngine + Bridge Adapter | Dev speed, prove the logic |
| Phase 2 (next) | Build SOAP Adapter alongside Bridge Adapter | Both adapters pass same test suite |
| Phase 3 (launch) | First customer uses SOAP Adapter | No bridge deployed on their instance |
| Phase 4 (mature) | Add REST/Hybrid adapters for Juno-specific features | Optional optimizations |
Consequences¶
Positive¶
- Zero changes per customer — BookingEngine works identically regardless of adapter
- Clean compliance story — Standard API integration, same pattern as every certified system
- Single maintenance point — Booking logic updated once, on our servers
- Multi-system coexistence — Our booking queue handles concurrent writes from other systems
Negative¶
- SOAP parsing complexity —
getDayWorkSchedulereturns a complexDayWorkScheduleTransferobject that must be parsed in the SOAP Adapter - No direct DB fallback — If SOAP is buggy (as it is for some endpoints), we can't work around it on customer instances
- Higher latency — SOAP calls over the internet vs. local DB queries on our dev instance
Risks¶
- getDayWorkSchedule bugs — Our bridge bypasses SOAP for availability because
getDayWorkScheduleis buggy on our instance. If it's consistently buggy across OSCAR versions, the SOAP Adapter may need workarounds. - SOAP API versioning — Different OSCAR versions may have slightly different SOAP schemas. The adapter must handle these variations.
Related Documentation¶
- OSCAR Schedule Deep Dive — 4-table chain, duplicate booking analysis
- OSCAR REST Bridge — Current bridge architecture
- EMR Abstraction Layer (ADR-003) — IEmrAdapter pattern
- Data Flow & Guardrails