Skip to content

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 complexitygetDayWorkSchedule returns a complex DayWorkScheduleTransfer object 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 getDayWorkSchedule is 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.