ADR-003: EMR Abstraction Layer¶
IMPLEMENTED — Production in v4.0.0+
The EMR abstraction layer is fully implemented. IEmrAdapter interface, EmrAdapterFactory, and OscarSoapAdapter are in production. See Adapter Implementations.
Architecture Decision Record for Multi-EMR Support
Status: Accepted — Updated v4.0.0 Date: January 14, 2026 (Updated February 15, 2026) Epic: 1.2 Implementation: Complete (Phase 1 + Phase 2 + v4.0 Onboarding Integration)
Context¶
VitaraVox currently has tight coupling to OSCAR EMR through:
- Hardcoded endpoint paths (/ws/rs/*, /demographics)
- OSCAR-specific field names (demographicNo, providerNo)
- OAuth 1.0a credential structure in database schema
- Availability detection assumptions (empty demographicNo = available)
- Single-char appointment type codes
To support multiple EMR systems (Oscar Universal, Oscar Pro, Telus Health, Juno), we need an abstraction layer.
Decision¶
Implement the EMR Adapter Pattern with a unified interface that all EMR integrations must implement.
Architecture Overview (Phase 2 Complete)¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ VOICE AGENT (vapiEndpoints.js) │
│ │
│ search_patient find_appointment book_appointment get_providers │
└─────────────────────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ EMR SERVICE LAYER (Phase 2) │
│ services/EmrService.js │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ FEATURE FLAGS │ │
│ │ ┌────────────────────┐ ┌────────────────────────────────────────┐ │ │
│ │ │ USE_EMR_ADAPTER │ │ EMR_ADAPTER_CLINICS │ │ │
│ │ │ (global toggle) │ │ (per-clinic rollout) │ │ │
│ │ └────────────────────┘ └────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────┐ ┌────────────────────────────────────┐ │
│ │ Legacy Fallback │◄────OR───│ EmrAdapterFactory │ │
│ │ oscarRestService │ │ (Clinic Config → Adapter) │ │
│ │ (if flag=false) │ │ │ │
│ └────────────────────┘ └────────────────┬───────────────────┘ │
│ │ │
└────────────────────────────────────────────────────┼─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ EMR ADAPTER INTERFACE (Phase 1) │
│ adapters/IEmrAdapter.js │
│ │
│ IEmrAdapter { │
│ searchPatient(query) getAvailability(providerId, date) │
│ getPatient(id) getAppointments(filter) │
│ createPatient(data) createAppointment(data) │
│ getProviders() updateAppointment(id, data) │
│ getProvider(id) cancelAppointment(id) │
│ healthCheck() getCircuitStatus() │
│ } │
└─────────────────────────────────────┬────────────────────────────────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ OscarSoapAdapter │ │ OscarBridgeAdapter │ │ AccuroRestAdapter │ │ TelusHealthAdapter │
│ ✅ PRODUCTION │ │ ✅ DEV/FALLBACK │ │ 📋 PLANNED │ │ 📋 PLANNED │
│ │ │ │ │ │ │ │
│ - Direct SOAP │ │ - REST Bridge │ │ - REST/OAuth 2.0 │ │ - GraphQL/JWT │
│ - WS-Security │ │ - X-API-Key auth │ │ - 150+ endpoints │ │ - TELUS Cloud │
│ - UsernameToken │ │ - Circuit breaker │ │ - Schedule template│ │ Connect │
│ - Circuit breaker │ │ - 5-min cache │ │ │ │ │
│ - ~870 lines │ │ - ~200 lines │ │ │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘
│ │ │ │
│ TRANSFORMERS │ │
│ ┌────────────────────────────────┐ │ │
│ │ PatientTransformer │ │ │
│ │ ProviderTransformer │ │ │
│ │ AppointmentTransformer │ │ │
│ │ (EMR ↔ Canonical format) │ │ │
│ └────────────────────────────────┘ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ OSCAR EMR (Direct) │ │ OSCAR REST Bridge │ │ Accuro (QHR) │ │ TELUS PS Suite │
│ SOAP/WS-Security │ │ (15.222.50.48:3000)│ │ (REST API) │ │ (Cloud Connect) │
│ Port 8443 │ │ Legacy, dev-only │ │ │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘
EMR Adapter Interface¶
TypeScript Interface Definition¶
// adapters/IEmrAdapter.ts
export interface PatientSearchQuery {
phone?: string;
lastName?: string;
firstName?: string;
healthCardNumber?: string;
dateOfBirth?: string;
}
export interface Patient {
id: string; // EMR-agnostic ID (was demographicNo)
firstName: string;
lastName: string;
dateOfBirth: string; // YYYY-MM-DD
gender?: 'M' | 'F' | 'O';
phone?: string;
email?: string;
address?: {
street: string;
city: string;
province: string;
postalCode: string;
country: string;
};
healthCardNumber?: string;
healthCardProvince?: string;
}
export interface Provider {
id: string; // EMR-agnostic ID (was providerNo)
firstName: string;
lastName: string;
displayName: string; // "Dr. Sarah Anderson"
specialty?: string;
acceptingNewPatients: boolean;
}
export interface TimeSlot {
startTime: string; // ISO 8601: 2026-01-14T09:00:00
endTime: string; // ISO 8601: 2026-01-14T09:15:00
duration: number; // Minutes
status: 'available' | 'booked' | 'blocked';
appointmentType?: string; // EMR-agnostic type code
description?: string; // Human-readable type name
}
export interface Availability {
providerId: string;
date: string; // YYYY-MM-DD
holiday: boolean;
timeSlots: TimeSlot[];
}
export interface Appointment {
id: string; // EMR-agnostic ID (was appointmentNo)
patientId: string;
providerId: string;
startTime: string; // ISO 8601
endTime: string; // ISO 8601
duration: number;
status: 'scheduled' | 'confirmed' | 'cancelled' | 'completed' | 'no_show';
reason?: string;
notes?: string;
appointmentType?: string;
}
export interface AppointmentCreateRequest {
patientId: string;
providerId: string;
startTime: string; // ISO 8601
duration: number;
reason?: string;
appointmentType?: string;
notes?: string;
}
export interface EmrHealthStatus {
status: 'healthy' | 'degraded' | 'unhealthy';
latencyMs?: number;
services?: Record<string, 'connected' | 'disconnected'>;
version?: string;
}
export interface IEmrAdapter {
// Metadata
readonly emrType: string; // 'oscar-universal' | 'oscar-pro' | 'telus-health'
readonly version: string;
// Patient Operations
searchPatient(query: PatientSearchQuery): Promise<Patient[]>;
getPatient(id: string): Promise<Patient | null>;
createPatient(data: Omit<Patient, 'id'>): Promise<Patient>;
// Provider Operations
getProviders(): Promise<Provider[]>;
getProvider(id: string): Promise<Provider | null>;
// Schedule Operations
getAvailability(providerId: string, date: string): Promise<Availability>;
getAppointmentTypes(): Promise<{ code: string; name: string; duration: number }[]>;
// Appointment Operations
getAppointments(filter: {
patientId?: string;
providerId?: string;
startDate?: string;
endDate?: string;
}): Promise<Appointment[]>;
createAppointment(data: AppointmentCreateRequest): Promise<Appointment>;
updateAppointment(id: string, data: Partial<Appointment>): Promise<Appointment>;
cancelAppointment(id: string): Promise<boolean>;
// Health Check
healthCheck(): Promise<EmrHealthStatus>;
}
Impact to Current Architecture¶
Files Requiring Modification¶
| File | Current State | Required Changes | Risk |
|---|---|---|---|
| vapiEndpoints.js | Direct oscarRestService calls | Use adapter factory, generic field names | High - 14 endpoints |
| oscarRestService.js | REST Bridge client | Refactor into OscarUniversalAdapter | Medium |
| oscarService.js | Legacy OAuth client | Refactor into OscarProAdapter | Medium |
| clinicRouter.js | Loads OSCAR credentials | Load EMR type + credentials | Medium |
| vitaraDb.js | OSCAR-specific schema | Add emr_type column | Low |
| utils.js | OSCAR-specific formatting | EMR-agnostic formatters | Low |
Database Schema Changes¶
-- Add EMR type to clinics table
ALTER TABLE clinics ADD COLUMN emr_type VARCHAR(50) DEFAULT 'oscar-universal';
ALTER TABLE clinics ADD COLUMN emr_config JSONB DEFAULT '{}';
-- emr_config structure varies by type:
-- oscar-universal: { "bridgeUrl": "...", "apiKey": "..." }
-- oscar-pro: { "baseUrl": "...", "oauthConsumerKey": "...", ... }
-- telus-health: { "fhirEndpoint": "...", "clientId": "...", "clientSecret": "..." }
New Files to Create¶
/opt/vitara-platform/voice-agent/
├── adapters/
│ ├── IEmrAdapter.js # Interface definition
│ ├── EmrAdapterFactory.js # Factory to create adapters
│ ├── OscarUniversalAdapter.js # Current REST Bridge
│ ├── OscarProAdapter.js # OAuth 1.0a direct
│ └── BaseEmrAdapter.js # Common functionality
├── transformers/
│ ├── PatientTransformer.js # EMR → Canonical patient
│ ├── AppointmentTransformer.js
│ └── ProviderTransformer.js
└── __tests__/
├── adapters/
│ ├── OscarUniversalAdapter.test.js
│ └── EmrAdapterFactory.test.js
└── transformers/
└── PatientTransformer.test.js
Implementation Phases¶
Phase 1: Interface & Factory (Week 1)¶
- Create
IEmrAdapterinterface - Create
EmrAdapterFactory - Create
BaseEmrAdapterwith common functionality - Wrap current
oscarRestService.jsasOscarUniversalAdapter
Phase 2: Refactor Voice Agent (Week 2)¶
- Update
clinicRouter.jsto load EMR type - Refactor
vapiEndpoints.jsto use adapter factory - Update database schema
- Update field names throughout
Phase 3: Testing & Documentation (Week 3)¶
- Unit tests for all adapters
- Integration tests with mock EMR
- E2E tests with real OSCAR instance
- Documentation updates
v4.0.0 Update (February 2026)¶
The EMR abstraction layer has been extended for the onboarding redesign:
Renamed adapters:
OscarUniversalAdapter→OscarBridgeAdapter(clarifies purpose — talks to REST Bridge, dev/fallback only)OscarSoapAdapteradded as production path — direct SOAP/WS-Security to OSCAR CXF (~870 lines)
New adapter methods used by onboarding:
| Method | Used By | Purpose |
|---|---|---|
getProviders() |
Step 4 Validation | Verify provider roster pulled from EMR |
getScheduleSlots() |
Step 4 Validation | Test slot retrieval with live data |
searchPatient() |
Step 4 Validation | Test patient search with live data |
testConnection() |
Step 2 EMR Connection | Verify EMR credentials work |
EMR type selector (onboarding Step 2):
| EMR Type | Adapter | Onboarding Status |
|---|---|---|
oscar-soap |
OscarSoapAdapter |
Full self-service (credentials → test → auto-pull config) |
oscar-universal |
OscarBridgeAdapter |
Full self-service (dev/fallback) |
telus |
TelusHealthAdapter |
Coming soon — saves type, shows contact message |
accuro |
AccuroRestAdapter |
Coming soon — saves type, shows contact message |
other |
— | Coming soon — saves type, shows contact message |
Factory caching: EmrAdapterFactory.getAdapter(clinicId) uses a 5-minute TTL cache per clinic.