System Architecture
Components, Integrations, and Data Flow
Last Updated: February 15, 2026 (v4.0.0 — SOAP adapter live, onboarding redesign)
High-Level Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ VITARAVOX ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ PATIENT LAYER │ │
│ │ │ │
│ │ 📞 Patient calls clinic phone number │ │
│ │ (+1-604-XXX-XXXX via Telnyx/VoIP.ms) │ │
│ │ │ │ │
│ └──────────────────────────────┼───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ VOICE AI LAYER │ │
│ │ (Vapi.ai) │ │
│ │ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ STT │ │ GPT-4o │ │ TTS │ │ │
│ │ │ (per-lang) │───▶│ LLM │───▶│ (per-lang) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └────────────┘ └─────┬──────┘ └────────────┘ │ │
│ │ │ │ │
│ │ Tool Calls (Webhooks) │ │
│ │ │ │ │
│ └────────────────────────────┼─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ MIDDLEWARE LAYER │ │
│ │ (Vitara Platform) │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ NGINX (9443) │ │ │
│ │ │ SSL + Rate Limiting │ │ │
│ │ └─────────────────────────┬──────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────────────▼──────────────────────────────────┐ │ │
│ │ │ Voice Agent (Node.js 3002) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ 26 Vapi│ │ Clinic │ │ OSCAR │ │ │ │
│ │ │ │ Handlers│ │ Router │ │ Adapter │ │ │ │
│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │
│ │ │ │ │ │
│ │ └────────────────────────┬───────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────┴──────────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Vitara DB │ │ OSCAR EMR │ │ │
│ │ │ (PostgreSQL) │ │ (Per-Clinic) │ │ │
│ │ │ │ │ │ │ │
│ │ │ • Clinic cfg │ │ • Demographics │ │ │
│ │ │ • Hours │ │ • Appointments │ │ │
│ │ │ • Waitlist │ │ • Providers │ │ │
│ │ │ • Call logs │ │ • Schedules │ │ │
│ │ └─────────────────┘ └─────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Mermaid: High-Level Architecture
flowchart TB
subgraph PatientLayer["📞 Patient Layer"]
Patient["Patient calls clinic<br/>+1-604-XXX-XXXX"]
end
subgraph VoiceAI["🎙️ Voice AI Layer (Vapi.ai)"]
STT["STT<br/>(AssemblyAI / Deepgram)"]
LLM["GPT-4o<br/>(Understanding)"]
TTS["TTS<br/>(ElevenLabs / Azure)"]
STT --> LLM --> TTS
end
subgraph Middleware["⚙️ Middleware Layer (Vitara Platform)"]
NGINX["NGINX :9443<br/>SSL + Rate Limiting"]
VoiceAgent["Voice Agent<br/>Node.js :3002"]
Endpoints["26 Vapi Tool Handlers"]
Router["Clinic Router"]
Adapter["OSCAR Adapter"]
NGINX --> VoiceAgent
VoiceAgent --> Endpoints
VoiceAgent --> Router
VoiceAgent --> Adapter
end
subgraph DataLayer["💾 Data Layer"]
VitaraDB[("Vitara DB<br/>PostgreSQL")]
OSCAR[("OSCAR EMR<br/>Per-Clinic")]
end
Patient --> STT
TTS --> Patient
LLM -->|"Tool Calls<br/>(Webhooks)"| NGINX
VoiceAgent --> VitaraDB
Adapter --> OSCAR
style PatientLayer fill:#e0f2fe,stroke:#0284c7
style VoiceAI fill:#fef3c7,stroke:#d97706
style Middleware fill:#f0fdf4,stroke:#16a34a
style DataLayer fill:#fce7f3,stroke:#db2777
Architecture Decisions Review
| Decision |
Choice |
Rationale |
Alternatives Considered |
| Voice AI Platform |
Vapi.ai |
Turn-key voice orchestration; handles STT/LLM/TTS pipeline; 50ms latency target |
Build custom (too complex), Twilio Voice (no native LLM), Amazon Connect (AWS lock-in) |
| LLM |
GPT-4o |
Best-in-class intent understanding; function calling support; multilingual |
Claude (no voice integration), Llama (self-host burden), GPT-3.5 (accuracy concerns) |
| STT |
AssemblyAI Universal (v2.3) / Deepgram per-lang (v3.0) |
Bilingual detection (v2.3); per-language accuracy (v3.0) |
Whisper (higher latency), Google STT (cost), Deepgram multi (forces EN on ZH) |
| TTS |
ElevenLabs (EN) / Azure Xiaoxiao (ZH in v3.0) |
Natural English voice; native Mandarin quality |
Amazon Polly (robotic), Google TTS (less natural), ElevenLabs turbo_v2_5 (EN-only) |
| Middleware |
Node.js Express |
Event-driven; large ecosystem; team expertise |
Python FastAPI (fewer libs), Go (learning curve), Java Spring (heavy) |
| Reverse Proxy |
NGINX |
Proven; rate limiting; SSL termination |
Traefik (Kubernetes focus), HAProxy (less features), Caddy (less mature) |
| Database |
PostgreSQL 16 |
ACID compliance; JSONB support; proven at scale |
MySQL (less features), MongoDB (consistency concerns), SQLite (not production) |
Component Details
Telephony Layer
| Component |
Provider |
Purpose |
| Phone Number |
Telnyx or VoIP.ms |
Inbound DID |
| SIP Trunk |
Via Vapi |
Call routing |
Voice AI Layer (Vapi.ai)
| Component |
v2.3.0 (Production) |
v3.0 (Deployed) |
| Speech-to-Text |
AssemblyAI Universal (all agents) |
Router: AssemblyAI Universal; EN: Deepgram nova-2 en; ZH: Deepgram nova-2 zh |
| LLM |
OpenAI GPT-4o |
OpenAI GPT-4o |
| Text-to-Speech |
ElevenLabs multilingual_v2 (all agents) |
EN: ElevenLabs multilingual_v2; ZH: Azure zh-CN-XiaoxiaoNeural |
| Squad Size |
6 assistants |
9 assistants (Router + 4 roles x 2 langs) |
| Component |
Technology |
Purpose |
| Reverse Proxy |
NGINX |
SSL termination, rate limiting |
| API Server |
Node.js + Express + TypeScript |
Webhook handling, admin API |
| ORM |
Prisma (13 models) |
Database access, migrations |
| Database |
PostgreSQL 16 |
Configuration, logs, audit trail |
| Process Manager |
PM2 |
Auto-restart, graceful shutdown |
EMR Abstraction Layer (v4.0 — IEmrAdapter + SOAP)
| Component |
Technology |
Purpose |
| IEmrAdapter |
adapters/IEmrAdapter.ts |
Canonical EMR interface (ADR-003) |
| EmrAdapterFactory |
adapters/EmrAdapterFactory.ts |
Per-clinic adapter creation with 5-min TTL cache |
| OscarSoapAdapter |
adapters/OscarSoapAdapter.ts |
ACTIVE — Direct OSCAR SOAP/WS-Security (v4.0) |
| OscarBridgeAdapter |
adapters/OscarBridgeAdapter.ts |
DEPRECATED — Legacy REST Bridge client (dev only) |
v4.0 change (2026-02-14): SOAP adapter is now the active production path. Connects directly to OSCAR's 3 SOAP endpoints (ScheduleService, DemographicService, ProviderService) using WS-Security UsernameToken authentication. No middleman, no sidecar on customer OSCAR instances. The REST Bridge adapter is retained for dev/fallback only. Factory selects adapter based on emr_type in clinic_config (oscar-soap or oscar-universal).
EMR Layer
| Component |
Technology |
Purpose |
| OSCAR EMR |
Per-clinic instance (AWS ca-central-1 for dev) |
Patient data, appointments |
| Integration |
SOAP/WS-Security (active) or REST Bridge (deprecated) |
Secure API access via 3 WSDL endpoints |
Network Topology
Internet
│
▼
┌─────────────────┐
│ Cloudflare │ DNS + DDoS protection
│ (DNS) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ OCI Toronto │ Production server
│ (Vitara) │
│ │
│ └─ NGINX:9443 │
│ └─ Node:3002 │
│ └─ Postgres │
└────────┬────────┘
│
│ SOAP/WS-Security (HTTPS :8443)
▼
┌─────────────────┐
│ OSCAR EMR │ AWS ca-central-1 (dev)
│ (Per-Clinic) │ Customer instances in production
└─────────────────┘
Mermaid: Network Topology
flowchart TB
Internet["🌐 Internet"]
subgraph CloudFlare["Cloudflare"]
DNS["DNS + DDoS Protection"]
end
subgraph OCI["OCI Toronto (Vitara)"]
NGINX2["NGINX :9443"]
Node["Node.js :3002"]
PG["PostgreSQL"]
end
subgraph ClinicNetwork["Clinic Network"]
OSCAR2[("OSCAR EMR<br/>(External)")]
end
Internet --> DNS
DNS --> NGINX2
NGINX2 --> Node
Node --> PG
Node -->|"SOAP/WS-Security<br/>(HTTPS :8443)"| OSCAR2
style CloudFlare fill:#fff7ed,stroke:#ea580c
style OCI fill:#f0fdf4,stroke:#16a34a
style ClinicNetwork fill:#fef2f2,stroke:#dc2626
Network Decisions Review
| Decision |
Choice |
Rationale |
Alternatives Considered |
| DNS Provider |
Cloudflare |
Free tier; DDoS protection; fast propagation; analytics |
Route53 (AWS lock-in), Google DNS (less features), self-hosted (no DDoS) |
| Hosting |
OCI Toronto |
Canadian data residency (PIPEDA); free tier; low latency to BC |
AWS ca-central-1 (cost), Azure Canada (cost), GCP (no Canadian region at time) |
| SSL/TLS |
Let's Encrypt |
Free; auto-renewal; widely trusted |
Paid certs (cost), self-signed (trust issues), Cloudflare origin (complexity) |
| OSCAR Connection |
SOAP/WS-Security (v4.0) |
OSCAR's native SOAP API ships with every version since OSCAR 12; no sidecar needed on customer instances; PHIPA/PIPA compliant |
REST Bridge (requires sidecar — compliance risk), OAuth 1.0a REST (not universally available), Direct DB (security risk) |
Multi-Tenancy Model
┌─────────────────────────────────────────────────────────────────────────────┐
│ MULTI-CLINIC ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Vapi Phone Numbers │
│ │
│ +1-604-555-0001 +1-604-555-0002 +1-604-555-0003 │
│ │ │ │ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Vitara │ │
│ │ Middleware │ │
│ │ │ │
│ │ Phone → Clinic │ │
│ │ Router │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Clinic A │ │ Clinic B │ │ Clinic C │ │
│ │ Config │ │ Config │ │ Config │ │
│ │ (Vitara DB) │ │ (Vitara DB) │ │ (Vitara DB) │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ OSCAR A │ │ OSCAR B │ │ OSCAR C │ │
│ │ (Clinic EMR) │ │ (Clinic EMR) │ │ (Clinic EMR) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Mermaid: Multi-Tenancy Model
flowchart TB
subgraph PhoneNumbers["📱 Vapi Phone Numbers"]
Phone1["+1-604-555-0001"]
Phone2["+1-604-555-0002"]
Phone3["+1-604-555-0003"]
end
subgraph VitaraMiddleware["⚙️ Vitara Middleware"]
Router2["Phone → Clinic<br/>Router"]
end
subgraph ClinicConfigs["📋 Clinic Configurations (Vitara DB)"]
ConfigA["Clinic A Config<br/>emr_live: true"]
ConfigB["Clinic B Config<br/>emr_live: false"]
ConfigC["Clinic C Config<br/>emr_live: true"]
end
subgraph EMRSystems["🏥 EMR Systems"]
OSCARA[("OSCAR A")]
OSCARB[("OSCAR B<br/>(Demo Mode)")]
OSCARC[("OSCAR C")]
end
Phone1 --> Router2
Phone2 --> Router2
Phone3 --> Router2
Router2 --> ConfigA
Router2 --> ConfigB
Router2 --> ConfigC
ConfigA --> OSCARA
ConfigB -.->|"Demo Data"| OSCARB
ConfigC --> OSCARC
style PhoneNumbers fill:#dbeafe,stroke:#2563eb
style VitaraMiddleware fill:#f0fdf4,stroke:#16a34a
style ClinicConfigs fill:#fef3c7,stroke:#d97706
style EMRSystems fill:#fce7f3,stroke:#db2777
Multi-Tenancy Decisions Review
| Decision |
Choice |
Rationale |
Alternatives Considered |
| Tenant Isolation |
Phone number routing |
Simple; Vapi provides caller context; no auth complexity |
Subdomain routing (DNS complexity), API key per clinic (more complex), session-based (stateful) |
| Config Storage |
Centralized Vitara DB |
Single source of truth; easy backup; ACID transactions |
Per-clinic DB (operational burden), Redis (durability concerns), Files (no ACID) |
| EMR Connection |
Per-clinic credentials |
Security isolation; independent scaling; clinic ownership |
Shared service account (security risk), VPN per clinic (network complexity) |
| Demo Mode |
emr_live flag in DB |
Per-clinic control; instant toggle; no code changes |
Env var (all-or-nothing), Feature flag service (overkill), Code branch (risky) |
| Adapter Pattern |
Factory with caching |
5-min TTL balances performance vs config freshness |
New adapter per request (slow), Global singleton (no isolation), IoC container (heavy) |
Request Flow
Inbound Call
1. Patient dials +1-604-555-0001
2. Telnyx routes to Vapi
3. Vapi answers, starts assistant
4. Vapi calls search_patient_by_phone webhook
5. Vitara looks up clinic by phone number
6. Vitara queries clinic's OSCAR for patient
7. Response returned to Vapi
8. Vapi speaks response to patient
Booking Flow
1. Patient: "I'd like to book an appointment"
2. Vapi: Understands intent, calls find_earliest_appointment
3. Vitara: Queries OSCAR for available slots
4. Vitara: Returns earliest slot
5. Vapi: "The earliest is Tuesday at 9:30 AM"
6. Patient: "Yes, book that"
7. Vapi: Calls create_appointment
8. Vitara: Creates appointment in OSCAR
9. Vitara: Logs call in Vitara DB
10. Vapi: Confirms to patient
Security Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY BOUNDARIES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ BOUNDARY 1: Internet → NGINX │
│ ───────────────────────────── │
│ • TLS 1.2/1.3 (Let's Encrypt) │
│ • Rate limiting (100 req/min) │
│ • HSTS, security headers │
│ │
│ BOUNDARY 2: Vapi → Vitara │
│ ──────────────────────────── │
│ • HMAC-SHA256 signature (primary, mandatory in production) │
│ • API Key / Bearer Token (fallback) │
│ • 5-minute replay window │
│ │
│ BOUNDARY 3: Vitara → OSCAR │
│ ───────────────────────────── │
│ • SOAP/WS-Security UsernameToken (production path) │
│ • OAuth 1.0a HMAC-SHA1 (legacy bridge path) │
│ • Per-clinic credentials │
│ • Encrypted at rest (AES-256-GCM) │
│ │
│ BOUNDARY 4: Admin UI → Vitara │
│ ───────────────────────────── │
│ • JWT authentication (access + refresh tokens) │
│ • Role-based access (VITARA_ADMIN, CLINIC_MANAGER) │
│ • Audit logging (append-only) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Mermaid: Security Architecture
flowchart LR
subgraph Internet["🌐 Internet"]
Caller["Caller"]
Admin["Admin User"]
end
subgraph Boundary1["🔒 Boundary 1: TLS"]
NGINX3["NGINX<br/>TLS 1.2/1.3<br/>Rate Limiting<br/>HSTS"]
end
subgraph Boundary2["🔑 Boundary 2: Webhook Auth"]
VapiAuth["HMAC-SHA256<br/>5-min Replay Window<br/>API Key Fallback"]
end
subgraph Boundary3["🔐 Boundary 3: Admin Auth"]
JWTAuth["JWT Auth<br/>RBAC Middleware<br/>Audit Logging"]
end
subgraph Boundary4["🏥 Boundary 4: EMR"]
OAuth["OAuth 1.0a<br/>HMAC-SHA1<br/>Per-Clinic Creds"]
end
subgraph Core["⚙️ Voice Agent"]
VoiceAgent2["Node.js<br/>Express"]
end
subgraph External["🏥 Clinic"]
OSCAR3[("OSCAR EMR")]
end
Caller -->|"HTTPS"| NGINX3
Admin -->|"HTTPS"| NGINX3
NGINX3 -->|"Vapi calls"| VapiAuth
NGINX3 -->|"Admin UI"| JWTAuth
VapiAuth --> VoiceAgent2
JWTAuth --> VoiceAgent2
VoiceAgent2 --> OAuth
OAuth -->|"HTTPS"| OSCAR3
style Boundary1 fill:#fef2f2,stroke:#dc2626
style Boundary2 fill:#fff7ed,stroke:#ea580c
style Boundary3 fill:#fef3c7,stroke:#d97706
style Boundary4 fill:#f0fdf4,stroke:#16a34a
Security Decisions Review
| Decision |
Choice |
Rationale |
Alternatives Considered |
| TLS Termination |
NGINX with Let's Encrypt |
Industry standard; free certs; easy renewal |
App-level TLS (performance hit), Cloudflare SSL (proxy mode issues) |
| Vapi Authentication |
HMAC-SHA256 (primary) |
Vapi sends x-vapi-signature + x-vapi-timestamp; 5-min replay window; fail-closed in production |
Bearer Token only (no replay protection), API Key only (static secret), mTLS (overkill), IP allowlist (fragile) |
| Admin Authentication |
PassportJS + JWT |
Stateless; scalable; proven library |
Sessions (stateful), OAuth2 (external dependency), Basic Auth (insecure) |
| Password Storage |
bcrypt (cost 12) |
Adaptive hashing; GPU-resistant |
Argon2 (newer but less support), PBKDF2 (faster cracking), SHA256 (too fast) |
| Account Lockout |
5 attempts / 15 min |
Balances security vs usability; industry standard |
Progressive delays (complex), CAPTCHA (accessibility), No lockout (brute force risk) |
| EMR Auth |
OAuth 1.0a |
OSCAR's native protocol; signature-based |
OAuth 2.0 (OSCAR doesn't support), API keys (OSCAR doesn't support) |
| Credential Storage |
AES-256-GCM encryption at rest |
PIPEDA compliance; lib/crypto.ts; ENCRYPTION_KEY required in production |
Plain text (non-compliant), Vault (complexity), KMS (cloud lock-in) |
| Audit Logging |
Append-only Prisma model |
Immutable; compliance-ready; middleware auto-captures mutations |
File logs (harder to query), External service (latency), None (non-compliant) |
| Input Validation |
Zod schemas with .strict() |
18 schemas; fail-fast on invalid input; TypeScript type inference |
Joi (less TS integration), Manual checks (error-prone), None (injection risk) |
| PHI Log Protection |
redactPhi() helper |
PHI keys replaced with [REDACTED] before logging; PIPEDA compliance |
Pino redact paths (limited), No logging (no debugging), Full logging (PHI exposure) |
Next Steps