OSCAR REST Bridge — Technical Architecture¶
REST-to-SOAP/DB bridge enabling modern API access to OSCAR EMR
Status: Production 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¶
- DB direct for patient registration — OSCAR's SOAP has no
addDemographicmethod, so INSERT is done directly - DB direct for availability — The SOAP
getDayWorkSchedulewas buggy (broken JOIN), so the bridge queriesscheduledate/scheduletemplatetables directly with the correct JOIN - DB direct for search — Phone search and quick multi-field search aren't available via SOAP
- SOAP for everything else — Appointments, allergies, prescriptions, and measurements all go through SOAP which handles validation and business logic
- DELETE = status update — OSCAR doesn't support true appointment deletion; the bridge sets
status='C'(cancelled) viaupdateAppointment
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¶
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.
| 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)¶
Register New Patient (DB)¶
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:
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¶
Response:
{
"success": true,
"data": [
{
"providerNo": "100",
"firstName": "Sarah",
"lastName": "Anderson",
"specialty": "Family Medicine"
}
]
}
Get Provider by ID¶
Get Current Provider¶
Appointments¶
Get Provider Availability (DB)¶
Returns available time slots based on provider's schedule template.
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)¶
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)¶
Create Appointment (SOAP)¶
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)¶
Cancel Appointment (SOAP)¶
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:
- Receives shutdown signal
- Stops accepting new connections
- Waits for in-flight requests to complete
- Closes MariaDB connection pool
- 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.