Skip to content

OSCAR REST Bridge — Technical Architecture

LEGACY — Dev/Fallback Only

The REST Bridge is retained for development and fallback only. Production uses the OscarSoapAdapter for direct SOAP/WS-Security connectivity. See Adapter Implementations.

REST-to-SOAP/DB bridge enabling modern API access to OSCAR EMR

Status: Legacy / Dev Fallback Version: 1.4.0 Last Updated: February 13, 2026

See Also

For the authoritative OSCAR entity relationship reference with all REST API field requirements, see OSCAR Data Architecture.


1. System Overview

The OSCAR REST Bridge is a Node.js microservice that translates modern REST/JSON API calls into two backend data paths: SOAP web services and direct MariaDB queries against the OSCAR EMR database. It exists because OSCAR EMR — a legacy Java/Tomcat system — exposes only WS-Security SOAP endpoints, and several critical operations (patient creation, schedule retrieval) are either missing or broken in the SOAP layer.

┌──────────────────────────────────────────────────────────────────────┐
│                        EXTERNAL CONSUMERS                            │
│  VitaraVox Voice Agent  │  Admin Dashboard  │  Future: Mobile Apps   │
└────────────┬─────────────────────┬──────────────────────┬────────────┘
             │                     │                      │
             │         REST/JSON + X-API-Key header       │
             ▼                     ▼                      ▼
┌──────────────────────────────────────────────────────────────────────┐
│                    OSCAR REST Bridge (:3000)                          │
│                                                                      │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │                     Middleware Chain                            │  │
│  │  Helmet → Rate Limiter → API Key Auth → Logger → Routes        │  │
│  └────────────────────────────────────────────────────────────────┘  │
│                                                                      │
│  ┌─────────────────────────────────────────────────┐                │
│  │                    Services                      │                │
│  │  DemographicService  ScheduleService  Provider   │                │
│  │  AppointmentService  ClinicalService  Health     │                │
│  └─────────────┬─────────────────────┬─────────────┘                │
│                │                     │                               │
│  ┌─────────────┴──────────┐  ┌──────┴──────────────────┐           │
│  │      SOAP Client       │  │    Database Service      │           │
│  │  WS-Security + Retry   │  │    mysql2 Connection     │           │
│  │  (Layer 2 Auth)        │  │    Pool (10 conns)       │           │
│  └─────────────┬──────────┘  └──────────┬──────────────┘           │
│                │                         │                           │
└────────────────┼─────────────────────────┼───────────────────────────┘
                 │ SOAP/XML                │ SQL (parameterized)
                 ▼                         ▼
┌────────────────────────────────┐  ┌──────────────────────────────┐
│   OSCAR EMR (Tomcat :8080)     │  │   MariaDB 10.5 (:3306)      │
│   SOAP Web Services:           │  │   Database: oscar_mcmaster   │
│   /ws/rs/DemographicWs         │  │   Tables: demographic,       │
│   /ws/rs/AppointmentWs         │  │   scheduledate, appointment, │
│   /ws/rs/ProviderWs            │  │   admission, provider, etc.  │
│   /ws/rs/AllergyWs             │  │                              │
│   /ws/rs/PrescriptionWs        │  │                              │
└────────────────────────────────┘  └──────────────────────────────┘

2. OSCAR Database Entity Relationship Diagram

The OSCAR EMR uses a MariaDB (Aria engine) database with no foreign key constraints — relationships are enforced by application logic only.

Core Tables

┌─────────────────────────┐         ┌─────────────────────────┐
│       provider          │         │      demographic        │
│─────────────────────────│         │─────────────────────────│
│ PK provider_no (vc 6)  │◄────────│ FK provider_no (vc 11)  │
│    first_name           │         │ PK demographic_no (int) │
│    last_name            │         │    first_name, last_name│
│    specialty            │         │    sex, year/month/date  │
│    provider_type        │         │    _of_birth (varchars!) │
│    ohip_no, billing_no  │         │    hin, ver, hc_type    │
│    status (A/I)         │         │    phone, phone2, email │
│    team                 │         │    address/city/province │
│    email, phone         │         │    chart_no             │
│    practitionerNo       │         │    patient_status       │
│    lastUpdateDate       │         │    roster_status        │
└────────┬────────────────┘         │    lastUpdateDate       │
         │                          └─────────┬───────────────┘
         │                                    │
         │  ┌─────────────────────────────────┼──────────────────────┐
         │  │                                 │                      │
         ▼  ▼                                 ▼                      ▼
┌─────────────────────┐          ┌──────────────────────┐  ┌────────────────────┐
│    appointment      │          │     allergies         │  │      drugs         │
│─────────────────────│          │──────────────────────│  │────────────────────│
│ PK appointment_no   │          │ PK allergyid         │  │ PK drugid          │
│ FK provider_no      │          │ FK demographic_no    │  │ FK demographic_no  │
│ FK demographic_no   │          │    DESCRIPTION       │  │ FK provider_no     │
│    appointment_date  │          │    reaction (text)   │  │    BN (brand name) │
│    start_time, end_  │          │    severity (1/2/3)  │  │    GN (generic)    │
│    time             │          │    onset_of_reaction │  │    dosage, route   │
│    reason, notes    │          │    TYPECODE          │  │    quantity, repeat│
│    status (t/B/C)   │          │    HICL/HIC/AGCSP   │  │    rx_date         │
│    type, location   │          │    regional_id       │  │    end_date        │
│    creator          │          │ FK providerNo        │  │    long_term, prn  │
│    createdatetime   │          │    archived          │  │    nosubs, archived│
│    updatedatetime   │          │    lastUpdateDate    │  │    script_no       │
└─────────┬───────────┘          └──────────────────────┘  │    lastUpdateDate  │
          │                                                └────────────────────┘
┌──────────────────────┐     ┌─────────────────────────┐
│   measurements       │     │   measurementsExt       │
│──────────────────────│     │─────────────────────────│
│ PK id                │◄────│ FK measurement_id       │
│ FK demographicNo     │     │ PK id                   │
│ FK providerNo        │     │    keyval               │
│ FK appointmentNo     │     │    val (text)            │
│    type (e.g. "BP")  │     └─────────────────────────┘
│    dataField (value) │
│    comments          │     ┌─────────────────────────┐
│    dateObserved      │     │   measurementType       │
│    dateEntered       │     │─────────────────────────│
└──────────────────────┘     │ PK id                   │
                             │    type (ref → measurements.type)
                             │    typeDisplayName       │
                             │    typeDescription       │
                             │    validation            │
                             └─────────────────────────┘

Schedule Subsystem

OSCAR's scheduling uses a special architecture where scheduledate.hour is not a time — it's a template name that joins to scheduletemplate.name to retrieve a timecode string. Each character in the timecode maps to a scheduletemplatecode entry defining 15-minute slot types.

┌───────────────────────┐      JOIN on           ┌───────────────────────────┐
│    scheduledate       │   sd.hour = st.name     │    scheduletemplate       │
│───────────────────────│   AND provider match    │───────────────────────────│
│ PK id                 │ ─────────────────────►  │ PK (provider_no, name)   │
│ FK provider_no        │                         │    name (vc 20)          │
│    sdate (date)       │                         │    timecode (text)       │
│    hour (vc 255)  ════╪══ template NAME ref ══► │    summary               │
│    status (A/D)       │                         └───────────────────────────┘
│    available (Y/N)    │
│    creator            │                         ┌───────────────────────────┐
└───────────────────────┘                         │  scheduletemplatecode     │
                                                  │───────────────────────────│
  Example:                                        │ PK id                    │
  hour = "P"  ──JOIN──►  name = "P"               │    code (char 1)         │
                         timecode = "111_111..."   │    description           │
                                                  │    duration              │
  Each char in timecode maps to ──────────────►   │    color                 │
  a code in scheduletemplatecode                  │    bookinglimit          │
                                                  └───────────────────────────┘
  Timecode: "1 1 1 _ _ _ 1 1 1 ..."
             │         │
             ▼         ▼
         code '1'   code '_'
         15min      unavailable

Notable Table Quirks

Quirk Details
Birth date split into 3 varchar columns year_of_birth, month_of_birth, date_of_birth are separate strings, not a DATE field
No foreign key constraints Relationships are enforced only by application logic
drugs table, not prescriptions The SOAP service is called PrescriptionWs but the underlying table is drugs
All tables use Aria engine MariaDB's MyISAM variant — no transactional safety at the DB level
scheduledate.hour is a template name NOT a time value — it's a reference to scheduletemplate.name
Appointment status codes t = To Do, H = Here, B = Billed, C = Cancelled, N = No Show

3. Authentication — Two-Layer Model

Layer 1: External (Client → Bridge)

All API requests (except /health) require an X-API-Key header.

Header Value Notes
X-API-Key 256-bit hex string Generated via CSPRNG (crypto.randomBytes(32).toString('hex'))

Key Properties: - 64 hex characters (256 bits of entropy) - Stored server-side in environment variable API_KEY - Compared using constant-time equality to prevent timing attacks - Rate-limited: 100 requests per 15 minutes per key

Layer 2: Internal (Bridge → OSCAR SOAP)

SOAP requests to OSCAR EMR use WS-Security (OASIS WSS) with UsernameToken Profile:

<wsse:Security>
  <wsse:UsernameToken>
    <wsse:Username>admin</wsse:Username>
    <wsse:Password Type="PasswordText">admin</wsse:Password>
  </wsse:UsernameToken>
</wsse:Security>

The SOAP credentials are configured via environment variables (SOAP_USERNAME, SOAP_PASSWORD) and injected into every SOAP envelope by the SOAP client.

Direct database queries bypass SOAP authentication entirely — they use MySQL credentials (DB_USER, DB_PASSWORD) with a connection pool.


4. Request Lifecycle

Every inbound request passes through this middleware chain:

HTTP Request
┌──────────────────────┐
│  1. Helmet           │  Security headers (CSP, HSTS, X-Frame-Options, etc.)
└──────────┬───────────┘
┌──────────────────────┐
│  2. Rate Limiter     │  100 req / 15 min per API key; 429 on exceed
└──────────┬───────────┘
┌──────────────────────┐
│  3. API Key Auth     │  X-API-Key header validation; 401 on failure
└──────────┬───────────┘
┌──────────────────────┐
│  4. Body Parser      │  JSON parsing with size limit
└──────────┬───────────┘
┌──────────────────────┐
│  5. Route Handler    │  Maps URL → Service method
└──────────┬───────────┘
┌──────────────────────┐
│  6. Service Layer    │  Business logic + data path selection
└──────────┬───────────┘
      ┌────┴────┐
      ▼         ▼
┌──────────┐ ┌──────────┐
│ SOAP/XML │ │ Direct   │   Based on operation (see §4)
│ Client   │ │ SQL      │
└──────────┘ └──────────┘
      │         │
      ▼         ▼
┌──────────────────────┐
│  7. Transformer      │  Normalize response → JSON envelope
└──────────┬───────────┘
┌──────────────────────┐
│  8. JSON Response    │  { success: true/false, data?, error? }
└──────────────────────┘

5. Dual Data Path — SOAP vs Direct DB

OSCAR EMR has incomplete and sometimes buggy SOAP support. The bridge uses two data paths based on the operation:

Operation Path Reason
Search patient by name SOAP Full DemographicWs support
Search patient by phone DB SOAP doesn't support phone search
Quick search (multi-field) DB More flexible than SOAP
Get patient by ID SOAP Full DemographicWs support
Register new patient DB SOAP provides no patient creation method
Get providers SOAP Full ProviderWs support
Get provider availability DB SOAP getDayWorkSchedule is buggy (returns empty arrays)
List appointments SOAP Full AppointmentWs support
Create appointment SOAP Full AppointmentWs support
Update appointment SOAP Full AppointmentWs support
Cancel appointment SOAP Status change to 'C' (Cancelled)
Get appointment types DB Direct query on scheduletemplatecode table
Get allergies SOAP Full AllergyWs support
Get prescriptions SOAP Full PrescriptionWs support
Get measurements SOAP Full MeasurementWs support

Why Direct DB?

Two OSCAR SOAP limitations forced the direct database path:

1. No patient creation SOAP method: OSCAR's DemographicWs has getDemographic, searchDemographic, updateDemographic — but no createDemographic. New patient registration requires INSERT INTO demographic plus creating an admission record linking the patient to the OSCAR Service program.

2. Buggy schedule retrieval: OSCAR's SOAP getDayWorkSchedule consistently returns { timeSlots: [] } even when the database has schedule data. The bridge reads directly from scheduledate.hour (a character-pattern field) and scheduletemplatecode to compute available slots.


6. SOAP-to-REST Bridge Mapping

All REST endpoints are under /api/v1/. Auth is via X-API-Key header (REST side) and WS-Security UsernameToken (SOAP side).

Demographics

REST Endpoint Method SOAP / DB Call What It Does
GET /demographics/:id GET SOAP DemographicWs.getDemographic(id) Fetch one patient by ID
GET /demographics?active=true&afterDate=... GET SOAP DemographicWs.getActiveDemographicsAfter2(date, null) List patients updated after date
GET /demographics/search?name=... GET SOAP DemographicWs.searchDemographicByName(name, 0, 100) Search patients by last name
GET /demographics/search?phone=... GET DB SELECT FROM demographic WHERE phone LIKE ... Search by phone (DB direct)
GET /demographics/quickSearch?term=... GET DB multi-column LIKE on name/HIN/phone/chart Quick search across all fields
POST /demographics POST DB INSERT INTO demographic + INSERT INTO admission Register new patient (DB direct)
POST /demographics/:id/verify POST DB SELECT year/month/date_of_birth Verify identity via DOB

Providers

REST Endpoint Method SOAP Call What It Does
GET /providers GET SOAP ProviderWs.getProviders2() List all providers
GET /providers/current GET SOAP ProviderWs.getLoggedInProviderTransfer() Get current auth'd provider

Appointments

REST Endpoint Method SOAP / DB Call What It Does
GET /appointments?providerId=&startDate=&endDate= GET SOAP ScheduleWs.getAppointmentsForDateRangeAndProvider2(start, end, provId, false) List appointments for provider in date range
GET /appointments/availability?providerId=&date= GET DB SELECT sd.*, st.timecode FROM scheduledate sd LEFT JOIN scheduletemplate st ON sd.hour=st.name Get available time slots (DB direct — bypasses buggy SOAP)
GET /appointments/types GET SOAP ScheduleWs.getScheduleTemplateCodes() List appointment slot type codes
POST /appointments POST SOAP ScheduleWs.addAppointment(transfer) Book new appointment
PUT /appointments/:id PUT SOAP ScheduleWs.updateAppointment(transfer) Update existing appointment
DELETE /appointments/:id DELETE SOAP ScheduleWs.updateAppointment({id, status:'C'}) Cancel appointment (sets status to C)

Allergies

REST Endpoint Method SOAP Call What It Does
GET /allergies?demographicId= GET SOAP AllergyWs.getAllergiesByProgramProviderDemographicDate(0, "", demoId, epoch, ...) Get all allergies for patient
GET /allergies/:id GET SOAP AllergyWs.getAllergy(id) Get single allergy record

Prescriptions

REST Endpoint Method SOAP Call What It Does
GET /prescriptions?demographicId= GET SOAP PrescriptionWs.getPrescriptionsByProgramProviderDemographicDate(0, "", demoId, epoch, ...) Get all prescriptions (includes nested drug array)
GET /prescriptions/:id GET SOAP PrescriptionWs.getPrescription(id) Get single prescription

Measurements

REST Endpoint Method SOAP Call What It Does
GET /measurements?demographicId=&type= GET SOAP MeasurementWs.getMeasurementsByProgramProviderDemographicDate(0, "", demoId, epoch, ...) Get measurements (optional type filter applied client-side)
GET /measurements/:id GET SOAP MeasurementWs.getMeasurement(id) Get single measurement
POST /measurements POST SOAP MeasurementWs.addMeasurement(transfer) Record new measurement

Bridge Internal Data Flow

External Client (Veribook, Portal, Voice Agent, etc.)
        │  REST/JSON + X-API-Key header
┌──────────────────────────────────────┐
│         REST Bridge (Node.js)        │
│  ┌─────────┐  ┌──────────────────┐  │
│  │ Routes   │→│ Services          │  │
│  │ (Express)│  │ ┌──────────────┐ │  │
│  └─────────┘  │ │ SOAP Client  │─┼──┼──► OSCAR Tomcat (/oscar/ws/*)
│               │ │ (WS-Security)│ │  │     via SOAP/XML
│               │ └──────────────┘ │  │
│               │ ┌──────────────┐ │  │
│               │ │ DB Service   │─┼──┼──► MariaDB (direct SQL)
│               │ │ (mysql2)     │ │  │     for: registration, search,
│               │ └──────────────┘ │  │     availability, verification
│               └──────────────────┘  │
│  ┌─────────────┐                    │
│  │ Transformers │ REST↔SOAP field   │
│  │              │ mapping & cleanup  │
│  └─────────────┘                    │
└──────────────────────────────────────┘

Key Design Decisions in the Bridge

  1. DB direct for patient registration — OSCAR's SOAP has no addDemographic method, so INSERT is done directly
  2. DB direct for availability — The SOAP getDayWorkSchedule was buggy (broken JOIN), so the bridge queries scheduledate/scheduletemplate tables directly with the correct JOIN
  3. DB direct for search — Phone search and quick multi-field search aren't available via SOAP
  4. SOAP for everything else — Appointments, allergies, prescriptions, and measurements all go through SOAP which handles validation and business logic
  5. DELETE = status update — OSCAR doesn't support true appointment deletion; the bridge sets status='C' (cancelled) via updateAppointment

7. Full API Reference

Base URL: http://15.222.50.48:3000/api/v1 Auth: X-API-Key header on all endpoints except /health

Health Check

GET /api/v1/health

No authentication required. Verifies both SOAP and database connectivity.

{
  "status": "healthy",
  "timestamp": "2026-02-11T15:30:00Z",
  "services": {
    "soap": "connected",
    "database": "connected"
  }
}

Demographics (Patients)

Quick Search (DB)

Multi-field search across name, phone, HIN, chart number.

GET /api/v1/demographics/quickSearch?term={searchTerm}&limit={limit}
Parameter Type Required Default Description
term string Yes Search term (min 2 chars)
limit number No 20 Max results (max 100)

Search by Name (SOAP) or Phone (DB)

GET /api/v1/demographics/search?name={lastName}&firstName={firstName}
GET /api/v1/demographics/search?phone={phoneNumber}

Response:

{
  "success": true,
  "data": [
    {
      "demographicNo": 78,
      "firstName": "Charles",
      "lastName": "Don",
      "dateOfBirth": "1985-06-15",
      "sex": "M",
      "phone": "+12367770690",
      "healthCardNumber": "1234567890",
      "chartNumber": "NEW-78"
    }
  ]
}

Get Patient by ID (SOAP)

GET /api/v1/demographics/:id

Register New Patient (DB)

POST /api/v1/demographics

Request Body:

{
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1990-05-20",
  "gender": "female",
  "phone": "+12367770690",
  "email": "jane@example.com",
  "address": "123 Main St",
  "city": "Vancouver",
  "province": "BC",
  "postalCode": "V6B 1A1",
  "healthCardNumber": "1234567890"
}

Response:

{
  "success": true,
  "data": {
    "demographicNo": 79,
    "admissionId": 71,
    "programId": 10034
  }
}

Creates both a demographic record and an admission record (linking to the OSCAR Service program). Patient appears in provider rosters immediately.

Providers (SOAP)

List Providers

GET /api/v1/providers

Response:

{
  "success": true,
  "data": [
    {
      "providerNo": "100",
      "firstName": "Sarah",
      "lastName": "Anderson",
      "specialty": "Family Medicine"
    }
  ]
}

Get Provider by ID

GET /api/v1/providers/:id

Get Current Provider

GET /api/v1/providers/current

Appointments

Get Provider Availability (DB)

Returns available time slots based on provider's schedule template.

GET /api/v1/appointments/availability?providerId={id}&date={YYYY-MM-DD}

Response:

{
  "success": true,
  "data": {
    "providerId": "100",
    "date": "2026-02-11",
    "available": true,
    "holiday": false,
    "timeSlots": [
      {
        "startTime": "09:00",
        "endTime": "09:15",
        "status": "available",
        "code": "B",
        "description": "Behavioral Science",
        "duration": 15
      }
    ]
  }
}

Get Appointment Types (DB)

GET /api/v1/appointments/types

Response:

{
  "success": true,
  "data": [
    { "code": "B", "name": "Behavioral Science", "duration": 15 },
    { "code": "2", "name": "New Concern", "duration": 30 },
    { "code": "3", "name": "Annual Physical", "duration": 45 },
    { "code": "P", "name": "Phone Consultation", "duration": 15 }
  ]
}

List Appointments (SOAP)

GET /api/v1/appointments?providerId={id}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}

Create Appointment (SOAP)

POST /api/v1/appointments

Request Body:

{
  "demographicId": 78,
  "providerId": "100",
  "appointmentDate": "2026-02-15",
  "startTime": "09:00",
  "duration": 15,
  "reason": "Follow-up",
  "status": "t"
}

Note: Status t = To Do (OSCAR default). The bridge sends status t explicitly.

Update Appointment (SOAP)

PUT /api/v1/appointments/:id

Cancel Appointment (SOAP)

DELETE /api/v1/appointments/:id

Sets status to C (Cancelled). OSCAR does not support true appointment deletion.

Clinical Data (SOAP)

GET /api/v1/allergies?demographicId={id}
GET /api/v1/prescriptions?demographicId={id}
GET /api/v1/measurements?demographicId={id}&type={type}
POST /api/v1/measurements

Endpoint Summary

GET  /api/v1/health                                    [no auth] [SOAP+DB]
GET  /api/v1/demographics                              [DB]
GET  /api/v1/demographics/:id                          [SOAP]
GET  /api/v1/demographics/search?name= OR ?phone=      [SOAP/DB]
GET  /api/v1/demographics/quickSearch?term=             [DB]
POST /api/v1/demographics                              [DB] (+ admission record)

GET  /api/v1/providers                                 [SOAP]
GET  /api/v1/providers/:id                             [SOAP]
GET  /api/v1/providers/current                         [SOAP]

GET  /api/v1/appointments?providerId=&startDate=&endDate=  [SOAP]
GET  /api/v1/appointments/availability?providerId=&date=   [DB]
GET  /api/v1/appointments/types                            [DB]
POST /api/v1/appointments                                  [SOAP]
PUT  /api/v1/appointments/:id                              [SOAP]
DELETE /api/v1/appointments/:id                            [SOAP]

GET  /api/v1/allergies?demographicId=                  [SOAP]
GET  /api/v1/prescriptions?demographicId=              [SOAP]
GET  /api/v1/measurements?demographicId=               [SOAP]
POST /api/v1/measurements                              [SOAP]

8. Security Controls — Six Layers

Layer 1: Network Isolation

Control Implementation
VPC Private subnet in AWS ca-central-1 (Montreal)
Security Groups Port 3000 (bridge) open only to VitaraVox server; port 3306 (MariaDB) internal only
No public DB access MariaDB bound to Docker network, not exposed to host

Layer 2: Transport

Control Implementation
HTTPS termination Caddy reverse proxy with auto-TLS (Let's Encrypt)
TLS 1.2+ only Enforced at reverse proxy level
Internal plaintext Bridge ↔ OSCAR ↔ MariaDB use Docker network (localhost only)

Layer 3: Rate Limiting

Tier Limit Scope
API (general) 100 req / 15 min Per API key
Burst protection Token bucket algorithm

Layer 4: Authentication

Method Mechanism Notes
External API access X-API-Key header 256-bit CSPRNG, constant-time comparison
OSCAR SOAP calls WS-Security UsernameToken Embedded in SOAP envelope
Database queries MySQL credentials Connection pool, never exposed

Layer 5: Data Safety

Control Implementation
SQL injection prevention All queries use parameterized statements (mysql2 prepared statements)
Input validation Route-level parameter validation; type checking on all inputs
Response filtering Transformers strip internal fields before returning to clients
Error sanitization Internal error details never exposed to clients

Layer 6: Container Isolation

Control Implementation
Docker network Bridge, OSCAR, MariaDB on isolated Docker network
Read-only filesystem Container runs with minimal write permissions
Non-root user Node.js process runs as non-root inside container
Graceful shutdown SIGTERM/SIGINT handled — closes DB pool cleanly

9. Deployment Architecture

┌──────────────────────────────────────────────────────────────────┐
│                  AWS EC2 (ca-central-1, Montreal)                 │
│                  t3a.medium (2 vCPU, 4GB RAM)                    │
│                  Ubuntu 24.04 LTS, 30GB gp3 SSD                  │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │                   Docker Network                          │    │
│  │                                                          │    │
│  │  ┌──────────────────┐  ┌──────────────────────────────┐  │    │
│  │  │ OSCAR REST Bridge│  │  OSCAR EMR (Tomcat 9)        │  │    │
│  │  │ Node.js :3000    │──│  Java 21 :8080               │  │    │
│  │  │ ~50MB RAM        │  │  ~2-4GB RAM                  │  │    │
│  │  └──────────────────┘  └──────────────────────────────┘  │    │
│  │           │                        │                      │    │
│  │           │    ┌───────────────────┘                      │    │
│  │           ▼    ▼                                          │    │
│  │  ┌──────────────────────────┐                            │    │
│  │  │  MariaDB 10.5 :3306      │                            │    │
│  │  │  Database: oscar_mcmaster │                            │    │
│  │  │  ~500MB RAM               │                            │    │
│  │  └──────────────────────────┘                            │    │
│  └──────────────────────────────────────────────────────────┘    │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │  VitaraVox Platform (outside Docker)                      │    │
│  │  PM2: vitara-admin-api :3002                              │    │
│  │  PostgreSQL: vitara-db (clinic data, call logs, users)    │    │
│  │  Caddy: HTTPS termination + reverse proxy                 │    │
│  └──────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────┘

Data residency: All data stays in AWS ca-central-1 (Montreal, Canada) — required for PIPEDA compliance.


10. OSCAR Schedule Pattern Encoding

OSCAR stores provider schedules as character patterns in the scheduledate.hour column:

Each character = one 15-minute slot
'_'           = unavailable
Letter (B,2,3,P...) = available (maps to scheduletemplatecode table)

48 chars = 12-hour day (8 AM – 8 PM)
96 chars = 24-hour day (midnight start)

Example (48-char pattern, 8 AM start):

____BBBBBBBBBBBB________BBBBBBBBBBBB____________
│   │            │       │            │
│   9 AM         12 PM   2 PM         5 PM
│                │                    │
8 AM (unavail)   Lunch (unavail)      Evening (unavail)

The bridge parses this pattern, cross-references booked appointments, and returns structured timeSlots with status available or booked.

Deep Dive Available

For a comprehensive analysis of the 4-table schedule chain, bookinglimit system, appointment-slot independence, full API surface comparison, duplicate booking vulnerability, and commercial competitive analysis, see OSCAR Schedule System: Complete Architecture Deep Dive.


11. Configuration

Bridge Environment Variables

Variable Default Purpose
PORT 3000 Bridge listen port
API_KEY 256-bit hex key for X-API-Key auth
SOAP_USERNAME admin OSCAR WS-Security username
SOAP_PASSWORD admin OSCAR WS-Security password
SOAP_URL http://oscar:8080 OSCAR SOAP base URL
DB_HOST localhost MariaDB host
DB_PORT 3306 MariaDB port
DB_USER root Database user
DB_PASSWORD Database password
DB_NAME oscar Database name
DB_CONNECTION_LIMIT 10 Connection pool size
OSCAR_DEFAULT_PROGRAM_ID 10034 Admission program for new patients
OSCAR_DEFAULT_PROVIDER_ID Fallback provider for admissions
OSCAR_DEFAULT_PROVINCE BC Default patient province
OSCAR_DEFAULT_COUNTRY CA Default patient country
OSCAR_DEFAULT_LANGUAGE English Default patient language
OSCAR_CHART_PREFIX NEW Chart number prefix

12. Error Handling

Standard Response Envelope

// Success
{ "success": true, "data": { ... }, "count": 5 }

// Error
{
  "success": false,
  "error": {
    "code": "SOAP_ERROR",
    "message": "Failed to connect to OSCAR SOAP service",
    "timestamp": "2026-02-11T15:30:00Z"
  }
}

Error Codes

Code HTTP Status Description
UNAUTHORIZED 401 Missing or invalid API key
RATE_LIMITED 429 Rate limit exceeded
MISSING_PARAMETERS 400 Required query parameters missing
INVALID_DATE_FORMAT 400 Date must be YYYY-MM-DD
DUPLICATE_PATIENT 409 Patient with same name + DOB exists
NOT_FOUND 404 Resource not found
SOAP_ERROR 502 OSCAR SOAP service error
DATABASE_ERROR 502 MariaDB query error
SERVICE_UNAVAILABLE 503 Backend service unreachable

VitaraVox Circuit Breaker

The VitaraVox platform wraps all Bridge calls in an opossum circuit breaker:

Parameter Value
Timeout 4 seconds per request
Error threshold 50% failure rate
Reset timeout 30 seconds (half-open retry)
Volume threshold 5 requests minimum before tripping

States: CLOSED (normal) → OPEN (fast-fail, <50ms) → HALF-OPEN (testing) → CLOSED

When the circuit is OPEN, requests fail immediately instead of waiting for the 4-second timeout — critical for voice interactions where Vapi has a 5-second tool-call timeout.


13. Key Architectural Decisions

ADR-001: REST-to-SOAP Bridge Pattern

Decision: Create a bridge microservice rather than calling OSCAR SOAP directly from the voice agent.

Rationale: - SOAP/WS-Security adds ~200 lines of XML plumbing per call - Voice agent needs sub-second response; bridge handles retry/timeout - Enables future EMR backends (Accuro, Telus Health) behind same REST API - Separates OSCAR deployment concerns from application concerns

ADR-002: Dual Data Path (SOAP + Direct DB)

Decision: Use direct database queries for operations where SOAP is missing or broken.

Rationale: - OSCAR's SOAP API has no createDemographic method - SOAP getDayWorkSchedule returns empty arrays (confirmed bug) - Direct DB queries are 10-50x faster than SOAP for simple lookups - Parameterized queries prevent SQL injection

Risk: Direct DB queries bypass OSCAR's business logic layer. Mitigated by: only using for operations where SOAP is confirmed missing/broken, and by creating admission records (not just demographic inserts) to maintain OSCAR data integrity.

ADR-003: EMR Adapter Pattern

Decision: Define a canonical IEmrAdapter interface in the VitaraVox platform, with OscarUniversalAdapter as the first implementation.

Rationale: - Canadian healthcare market has multiple EMR vendors - Same voice assistant logic should work across EMRs - Adapter pattern isolates EMR-specific code - New EMR support = new adapter implementation, no voice logic changes

ADR-004: Data Residency in ca-central-1

Decision: All infrastructure deployed in AWS Canada (Montreal) region.

Rationale: - PIPEDA requires Canadian organizations to protect personal health information - PIPA (BC) has specific requirements for health data - ca-central-1 is the only AWS region in Canada - Eliminates cross-border data transfer concerns


14. Graceful Shutdown

The bridge handles SIGTERM and SIGINT signals for clean shutdown:

  1. Receives shutdown signal
  2. Stops accepting new connections
  3. Waits for in-flight requests to complete
  4. Closes MariaDB connection pool
  5. Exits with code 0

This ensures no orphaned database connections during Docker restarts or deployments.


15. Known Issues & Workarounds

SOAP getDayWorkSchedule Bug

Issue: Returns { holiday: false, timeSlotDurationMin: 15, timeSlots: [] } even when scheduledate.hour has data.

Workaround: /appointments/availability queries scheduledate and scheduletemplatecode tables directly.

No SOAP Patient Creation

Issue: DemographicWs has no create method.

Workaround: POST /demographics inserts into demographic table and creates an admission record linking to the OSCAR Service program (ID from OSCAR_DEFAULT_PROGRAM_ID).

Appointment Status Codes

OSCAR uses single-character status codes:

Code Meaning
t To Do (default for new appointments)
H Here (patient arrived)
B Billed
C Cancelled
N No Show

The bridge defaults to t for new appointments and sets C for cancellations.