Data Flow & Guardrails
Data Ownership, Security Boundaries, and Conversation Flows
Last Updated: February 14, 2026 (v4.0.0 — SOAP adapter live)
Data Ownership Model
VitaraVox maintains clear separation between its own data and clinic EMR data.
What VitaraVox Owns (Vitara Database)
| Data Type |
Purpose |
Retention |
Protection |
| Clinic profiles |
Name, address, timezone |
Permanent |
— |
| OSCAR credentials |
OAuth tokens |
Permanent |
AES-256-GCM encrypted at rest |
| Working hours |
Per-day open/close times |
Permanent |
— |
| Holidays |
Closure dates |
Annual review |
— |
| Provider display names |
Voice-friendly names |
Permanent |
— |
| Waitlist entries |
New patient queue |
Until registered |
DB-persisted (v3.2.1) |
| Call logs |
Analytics, outcomes |
Configurable (default 365 days) |
Transcripts nulled after 90 days |
| User accounts |
Admin dashboard access |
Permanent |
bcrypt password hashing |
| Audit logs |
Admin action tracking |
7 years |
Append-only (no DELETE) |
What OSCAR Owns (Clinic EMR)
| Data Type |
VitaraVox Access |
Purpose |
| Patient demographics |
Read/Write |
Lookup, registration |
| Appointments |
Read/Write |
Booking, rescheduling |
| Provider schedules |
Read |
Availability check |
| Medical records |
NONE |
Never accessed |
| Lab results |
NONE |
Never accessed |
| Prescriptions |
NONE |
Never accessed |
| Billing |
NONE |
Never accessed |
Data Flow Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA FLOW OVERVIEW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ INBOUND (Patient → System) │
│ ───────────────────────── │
│ │
│ Voice Input │
│ │ │
│ ▼ │
│ ┌──────────┐ Transcript ┌──────────┐ Webhook ┌──────────┐ │
│ │ Vapi │ ──────────────▶ │ LLM │ ───────────▶ │ Vitara │ │
│ │ (STT) │ │ (GPT-4o) │ │Middleware│ │
│ └──────────┘ └──────────┘ └────┬─────┘ │
│ │ │
│ ┌───────────────────────────┤ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Vitara │ │ OSCAR │ │
│ │ DB │ │ EMR │ │
│ │ │ │ │ │
│ │ Config │ │ Patients │ │
│ │ Logs │ │ Appts │ │
│ └──────────┘ └──────────┘ │
│ │
│ OUTBOUND (System → Patient) │
│ ─────────────────────────── │
│ │
│ ┌──────────┐ Response ┌──────────┐ Speech ┌──────────┐ │
│ │ OSCAR │ ─────────────▶ │ Vitara │ ───────────▶ │ Vapi │ │
│ │ EMR │ │Middleware│ │ (TTS) │ │
│ └──────────┘ └──────────┘ └────┬─────┘ │
│ │ │
│ ▼ │
│ Voice Output │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Mermaid: Data Flow Overview
flowchart TB
subgraph Inbound["⬇️ INBOUND (Patient → System)"]
direction LR
Voice1["🎤 Voice Input"]
VapiSTT["Vapi<br/>(STT)"]
LLM2["LLM<br/>(GPT-4o)"]
Middleware1["Vitara<br/>Middleware"]
Voice1 -->|"Audio"| VapiSTT
VapiSTT -->|"Transcript"| LLM2
LLM2 -->|"Webhook<br/>Tool Call"| Middleware1
end
subgraph Storage["💾 Data Storage"]
VitaraDB2[("Vitara DB<br/>• Config<br/>• Logs<br/>• Audit")]
OSCAR4[("OSCAR EMR<br/>• Patients<br/>• Appointments")]
end
subgraph Outbound["⬆️ OUTBOUND (System → Patient)"]
direction LR
OSCAR5[("OSCAR")]
Middleware2["Vitara<br/>Middleware"]
VapiTTS["Vapi<br/>(TTS)"]
Voice2["🔊 Voice Output"]
OSCAR5 -->|"Data"| Middleware2
Middleware2 -->|"Response"| VapiTTS
VapiTTS -->|"Speech"| Voice2
end
Middleware1 --> VitaraDB2
Middleware1 --> OSCAR4
style Inbound fill:#dbeafe,stroke:#2563eb
style Storage fill:#fef3c7,stroke:#d97706
style Outbound fill:#dcfce7,stroke:#16a34a
Data Flow Decisions Review
| Decision |
Choice |
Rationale |
Alternatives Considered |
| Data Ownership |
Split: Vitara owns config/logs, OSCAR owns PHI |
Clear boundaries; PIPEDA compliance; clinic retains patient data |
Single DB (compliance risk), Vitara caches PHI (liability), Sync copies (complexity) |
| PHI Access |
Read/Write for demographics + appointments ONLY |
Minimum necessary principle; no medical records ever accessed |
Full EMR access (overkill), Read-only (can't book), No access (can't function) |
| Audit Retention |
7 years |
PIPEDA requirement; provincial health regulation alignment |
1 year (non-compliant), Forever (storage cost), None (non-compliant) |
| Call Log Content |
Metadata only, no PHI |
Compliance; analytics without risk; patient names never logged |
Full transcripts (PHI risk), No logging (no analytics), Encrypted PHI (key management) |
OSCAR EMR Connection — Direct SOAP (v4.0)
As of v4.0 (2026-02-14), the Vitara Platform connects directly to OSCAR's built-in SOAP web services — no REST Bridge middleman required. This is the production path, validated with 35/35 E2E tests passing.
Vitara Platform (OCI Toronto)
│
│ SOAP/XML + WS-Security UsernameToken
│ HTTPS port 8443
▼
┌──────────────────────────────────────┐
│ OSCAR EMR (AWS Montreal) │
│ │
│ 3 SOAP Endpoints: │
│ ┌────────────────────────────────┐ │
│ │ /ws/ScheduleService?wsdl │ │ Schedules, appointments
│ │ /ws/DemographicService?wsdl │ │ Patient records
│ │ /ws/ProviderService?wsdl │ │ Doctor list
│ └────────────────────────────────┘ │
│ │
│ MariaDB 10.5 (oscar_mcmaster) │
└──────────────────────────────────────┘
| Capability |
SOAP Method |
Status |
| Search patients |
searchDemographicByNameAsync |
Working |
| Get patient |
getDemographicAsync |
Working |
| Get providers |
getProviders2Async |
Working |
| Get schedule |
getDayWorkScheduleAsync |
Working |
| Get appointments |
getAppointmentsForDateRangeAndProvider2Async |
Working |
| Book appointment |
addAppointmentAsync |
Working |
| Cancel/update |
updateAppointmentAsync |
Working |
| Register patient |
— |
NOT AVAILABLE via SOAP (use OSCAR web UI) |
REST Bridge (Legacy)
The OSCAR REST Bridge at 15.222.50.48:3000 is retained for dev/fallback only. It required a sidecar deployment on the same server as OSCAR, which is a compliance concern for customer instances (PHIPA/PIPA prohibit custom components on clinic EMRs). The SOAP adapter eliminates this requirement entirely.
Security Boundaries
Boundary 1: Vapi → Vitara
| Control |
Implementation |
| Transport |
HTTPS (TLS 1.2+) |
| Authentication |
Multi-auth: Bearer token, HMAC-SHA256, or API key (any one sufficient) |
| Verification |
Constant-time comparison (crypto.timingSafeEqual) |
| Rate limit |
100 req/min (general), 50 burst (webhook paths) |
| Caller ID |
Server extracts real phone from Telnyx call.customer.number — LLM input ignored |
Boundary 2: Vitara → OSCAR
| Control |
Implementation |
| Transport |
HTTPS (TLS 1.2+, port 8443) |
| Authentication |
WS-Security UsernameToken (PasswordText, no Timestamp, no Nonce) |
| Credentials |
Per-clinic, encrypted at rest (AES-256-GCM) |
| Scope |
3 SOAP services: Schedule, Demographic, Provider |
| Protection |
Circuit breaker (opossum, 10s timeout, 50% error threshold) |
Boundary 3: Admin UI → Vitara
| Control |
Implementation |
| Transport |
HTTPS (TLS 1.2+) |
| Authentication |
JWT tokens |
| Authorization |
Role-based (Clinic Admin, Super Admin) |
| Audit |
All actions logged |
Conversation Flow: Booking
v3.0 — 9-Agent Dual-Track
┌─────────────────────────────────────────────────────────────────────────────┐
│ BOOKING FLOW (v3.0 — 9-Agent Dual-Track) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. CALL CONNECTS → ROUTER (AssemblyAI Universal STT) │
│ ───────────────────────────────────────────────── │
│ System greeting: "Hi, this is Vitara Clinic. How can I help you?" │
│ Patient speaks → Router detects language from first utterance │
│ Router: "One moment please." (says ONLY this — silent transfer) │
│ Router: handoff_to_patient_id_en (or _zh) │
│ │
│ 2. PATIENT-ID AGENT (Deepgram nova-2 EN/ZH specific) │
│ ───────────────────────────────────────────── │
│ Server: Extracts real phone from call.customer.number │
│ Patient-ID: search_patient_by_phone(real_phone) │
│ │
│ ┌──────────────────────┐ │
│ │ Patient found? │ │
│ └──────────┬───────────┘ │
│ ┌─────┴─────┐ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ "Hi [Name]!" "Are you a new patient?" │
│ │ → handoff to Registration │
│ │ │
│ Detect intent: "I'd like to book" │
│ Patient-ID: handoff_to_booking_en (silent) │
│ │
│ 3. BOOKING AGENT — IMMEDIATE SLOT FINDING │
│ ────────────────────────────────────── │
│ Booking: find_earliest_appointment() — any provider │
│ Booking: "I have Wed Feb 11 at 9 AM with Dr. Anderson. Does that work?"│
│ │
│ 4. PATIENT ACCEPTS OR REFINES │
│ ──────────────────────────── │
│ Patient: "To a different doctor" → re-search with different provider │
│ Patient: "That's good" → proceed to step 5 │
│ │
│ 5. INLINE CONFIRMATION & BOOKING │
│ ───────────────────────────── │
│ Booking: "What is this visit for?" │
│ Patient: "Just meeting the doctor" │
│ Booking: create_appointment(demographicId, providerId, startTime, ...) │
│ Booking: log_call_metadata(callOutcome=booked, language=en, ...) │
│ Booking: "All set Wed Feb 11 at 9 AM with Dr. Chen. Arrive 10 min │
│ early with your health card. Anything else?" │
│ │
│ 6. CLOSE CALL │
│ ────────── │
│ Patient: "Thanks. Bye." │
│ Booking: "Have a wonderful day." │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
v2.3.0 — 6-Agent Squad (Production)
┌─────────────────────────────────────────────────────────────────────────────┐
│ BOOKING FLOW (v2.3.0 — 6-Agent Squad) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. CALL CONNECTS → ROUTER AGENT │
│ ───────────────────────────── │
│ Router greets: "Welcome to the clinic! Let me look up your info." │
│ Server: Extracts real phone from Telnyx call.customer.number │
│ Server: search_patient_by_phone(real_phone) — ignores LLM arg │
│ │
│ ┌──────────────────────┐ │
│ │ Patient found? │ │
│ └──────────┬───────────┘ │
│ ┌─────┴─────┐ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ "Hello [Name], "I couldn't find your │
│ how can I help?" records. Are you a │
│ │ new patient?" │
│ │ │ │
│ ▼ ▼ │
│ │
│ 2. INTENT → SILENT HANDOFF TO BOOKING AGENT │
│ ────────────────────────────────────── │
│ Patient: "I'd like to book an appointment" │
│ Router: [silently transfers to Booking agent — patient doesn't hear] │
│ │
│ 3. BOOKING AGENT — IMMEDIATE SLOT FINDING │
│ ────────────────────────────────────── │
│ Booking: "Let me find the earliest appointment for you." │
│ Booking: find_earliest_appointment() — NO filters, any provider │
│ Booking: "Earliest is Thursday Feb 6 at 9:30 AM with Dr. Chen" │
│ │
│ 4. PATIENT ACCEPTS OR REFINES │
│ ──────────────────────────── │
│ Patient: "That works!" → proceed to step 5 │
│ Patient: "Can I see Dr. Wong instead?" → re-search with providerName │
│ │
│ 5. INLINE CONFIRMATION & BOOKING │
│ ───────────────────────────── │
│ Booking: "Shall I book that for you?" │
│ Patient: "Yes" │
│ Booking: create_appointment(...) + asks for visit reason │
│ Booking: "Confirmed for Thursday at 9:30 AM with Dr. Chen." │
│ │
│ 6. CLOSE CALL │
│ ────────── │
│ Patient: "No, thank you" │
│ Booking: "Have a great day! Goodbye." │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Mermaid: Booking Flow
flowchart TD
Start(["📞 Call Connects"]) --> SearchPhone["search_patient_by_phone(caller_id)"]
SearchPhone --> Found{Patient Found?}
Found -->|Yes| Greet1["Hello [Name]"]
Found -->|No| Greet2["Thank you for calling"]
Greet1 --> Intent["Patient: 'I'd like to book'"]
Greet2 --> Intent
Intent --> NeedVerify{Need Verification?}
NeedVerify -->|No| VisitType
NeedVerify -->|Yes| SearchName["search_patient(name)"]
SearchName --> AskDOB["Ask for DOB"]
AskDOB --> VerifyDOB["Verify match"]
VerifyDOB --> VisitType
VisitType["Ask: In-person or phone?"] --> AskReason["Ask: What's the visit for?"]
AskReason --> MapType["Map to appointment type<br/>(e.g., 'B' = 15 min follow-up)"]
MapType --> FindSlot["find_earliest_appointment<br/>(type=B, provider=any)"]
FindSlot --> Offer["'Earliest is Tuesday at 9:30 AM<br/>with Dr. Chen'"]
Offer --> Confirm{Patient Confirms?}
Confirm -->|Yes| Book["create_appointment(...)"]
Confirm -->|No| FindAlternative["Offer alternatives"]
FindAlternative --> Offer
Book --> Success["'Confirmed for Tuesday, Jan 14<br/>at 9:30 AM'"]
Success --> Reminder["'Please arrive 5 minutes early'"]
Reminder --> LogCall["log_call(outcome=booked)"]
LogCall --> End(["📞 'Thank you for calling!'"])
style Start fill:#dbeafe,stroke:#2563eb
style End fill:#dcfce7,stroke:#16a34a
style Found fill:#fef3c7,stroke:#d97706
style Confirm fill:#fef3c7,stroke:#d97706
Booking Flow Decisions Review
| Decision |
Choice |
Rationale |
Alternatives Considered |
| Patient Identification |
Caller ID first, then name+DOB |
Fast path for known patients; secure fallback |
Always ask name (slow), Only caller ID (misses unlisted phones), PIN system (friction) |
| DOB Verification |
Required for name-based lookup |
PIPEDA identity verification; prevents PHI disclosure |
Security question (hard to implement), Voice biometrics (accuracy concerns), None (non-compliant) |
| Appointment Type Mapping |
Voice reason → type code |
Clinic-specific; LLM determines based on symptoms |
Explicit type selection (confusing), Fixed durations (inflexible), Staff callback (defeats automation) |
| Provider Selection |
"Any" by default, specific on request |
Fastest booking; respects patient preferences |
Always ask (slow), First available only (no choice), Require specific (delays) |
| Confirmation Read-back |
Full date/time/provider |
Prevents misunderstanding; audit trail |
Just "confirmed" (ambiguous), SMS (not implemented), None (errors) |
Conversation Flow: New Patient Registration
┌─────────────────────────────────────────────────────────────────────────────┐
│ REGISTRATION FLOW (NEW PATIENT) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. PATIENT NOT FOUND │
│ ───────────────── │
│ System: "I couldn't find your record. Would you like to register?" │
│ Patient: "Yes" │
│ │
│ 2. CHECK REGISTRATION STATUS │
│ ────────────────────────── │
│ System: get_clinic_info() │
│ │
│ ┌──────────────────────────┐ │
│ │ accepting_new_patients? │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ Continue ┌─────────────────────────┐ │
│ │ waitlist_enabled? │ │
│ └──────────┬──────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ "Add to waitlist?" "Not accepting. Call back later" │
│ │
│ 3. COLLECT BC HEALTH INFORMATION │
│ ────────────────────────────── │
│ │
│ Required (one at a time): │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Field │ Prompt │ │
│ ├──────────────────┼──────────────────────────────────────────────┤ │
│ │ Full legal name │ "What is your name as on BC Services Card?" │ │
│ │ Date of birth │ "What is your date of birth?" │ │
│ │ PHN │ "What is your 10-digit PHN?" │ │
│ │ Gender │ "What is your gender?" │ │
│ │ Phone │ "Best phone number to reach you?" │ │
│ │ Email (optional) │ "Email address? This is optional." │ │
│ │ Address │ "Your mailing address?" │ │
│ └──────────────────┴──────────────────────────────────────────────┘ │
│ │
│ 4. CONFIRM INFORMATION │
│ ─────────────────── │
│ System: Read back all collected information │
│ Patient: "Yes, that's correct" │
│ │
│ 5. REGISTER IN OSCAR │
│ ───────────────── │
│ System: register_new_patient(...) │
│ │
│ ┌──────────────────────┐ │
│ │ Registration result? │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ SUCCESS ERROR │
│ │ │ │
│ ▼ ▼ │
│ "Welcome!" "Let me transfer you to staff" │
│ │ │
│ ▼ │
│ "Book first appointment?" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Mermaid: Registration Flow
flowchart TD
NotFound(["Patient Not Found"]) --> AskRegister["'Would you like to register?'"]
AskRegister --> WantsRegister{Patient says Yes?}
WantsRegister -->|No| EndCall["End call"]
WantsRegister -->|Yes| CheckClinic["get_clinic_info()"]
CheckClinic --> Accepting{Accepting<br/>New Patients?}
Accepting -->|Yes| CollectInfo
Accepting -->|No| WaitlistCheck{Waitlist<br/>Enabled?}
WaitlistCheck -->|Yes| AskWaitlist["'Add to waitlist?'"]
WaitlistCheck -->|No| NotAccepting["'Not accepting.<br/>Call back later'"]
AskWaitlist --> AddWaitlist["add_to_waitlist(...)"]
subgraph CollectInfo["📋 Collect BC Health Information"]
Field1["Full legal name<br/>(as on BC Services Card)"]
Field2["Date of birth"]
Field3["PHN (10 digits)"]
Field4["Gender"]
Field5["Phone number"]
Field6["Email (optional)"]
Field7["Mailing address"]
Field1 --> Field2 --> Field3 --> Field4 --> Field5 --> Field6 --> Field7
end
CollectInfo --> ReadBack["Read back all information"]
ReadBack --> Correct{Patient confirms<br/>correct?}
Correct -->|No| CollectInfo
Correct -->|Yes| Register["register_new_patient(...)"]
Register --> Result{Registration<br/>Result?}
Result -->|Success| Welcome["'Welcome!'"]
Result -->|Error| Transfer["'Let me transfer you<br/>to staff'"]
Welcome --> AskBook["'Book first appointment?'"]
AskBook -->|Yes| BookingFlow(["→ Booking Flow"])
AskBook -->|No| ThankYou(["'Thank you!'"])
style NotFound fill:#fecaca,stroke:#dc2626
style Welcome fill:#dcfce7,stroke:#16a34a
style CollectInfo fill:#dbeafe,stroke:#2563eb
style Accepting fill:#fef3c7,stroke:#d97706
style Result fill:#fef3c7,stroke:#d97706
Registration Flow Decisions Review
| Decision |
Choice |
Rationale |
Alternatives Considered |
| Registration Gate |
Check accepting_new_patients first |
Prevents wasted effort; clinic controls enrollment |
Always collect info (wastes time), No gate (clinic can't control), Manual approval (delays) |
| Waitlist Option |
Configurable per clinic |
Some clinics want leads; others don't |
Always offer (annoys some clinics), Never offer (loses leads), Clinic-wide only (inflexible) |
| BC Health Data |
PHN required for BC patients |
Provincial health insurance requirement; EMR integration |
Health card scan (no voice), Self-reported only (EMR mismatch), Skip PHN (billing issues) |
| Field Collection |
One at a time, voice-paced |
Reduces errors; allows corrections; natural conversation |
Form dump (overwhelming), All at once (error-prone), Transfer to staff (defeats purpose) |
| Confirmation |
Full read-back required |
Prevents data entry errors; verbal consent |
Skip confirmation (errors), SMS confirmation (not implemented), None (compliance risk) |
| Error Recovery |
Transfer to staff |
Complex cases need human; maintains trust |
Retry loop (frustrating), End call (poor experience), Queue callback (delay) |
Guardrails
Medical Safety
| Scenario |
System Response |
| Emergency keywords (chest pain, can't breathe, etc.) |
"Please hang up and call 911 immediately" |
| Medical questions |
"I handle scheduling only. Let me transfer you." |
| Medication questions |
Transfer to staff |
| Test result interpretation |
Transfer to staff |
Conversation Quality
| Scenario |
System Response |
| Tool delay > 3 seconds |
"Just one moment please..." |
| Patient not found (2 attempts) |
Offer registration or transfer |
| Frustrated caller |
Transfer after 2 failed attempts |
| Unclear intent |
Ask clarifying question |
| Off-topic request |
Redirect to scheduling or transfer |
Data Protection
| Scenario |
System Response |
| Caller asks for other patient's info |
"I can only discuss your own appointments" |
| Request for medical records |
Transfer to staff |
| PHI in logs |
redactPhi() replaces patient names, DOB, phone, health cards with [REDACTED] |
| Credentials in logs |
AES-256-GCM encrypted at rest; masked in admin UI (last 4 chars only) |
| Webhook without HMAC signature |
Rejected with 401 in production mode |
Error Handling Matrix
| Error Type |
User Message |
System Action |
| OSCAR unreachable |
"Let me try again..." |
Retry once, then transfer |
| Patient not found |
"I couldn't find your record" |
Offer search by phone or register |
| No appointments |
"No openings for that date" |
Offer alternative dates |
| Booking failed |
"I wasn't able to complete that" |
Transfer to staff |
| Registration failed |
"Let me transfer you to staff" |
Transfer with context |
| Invalid PHN format |
"PHN should be 10 digits starting with 9" |
Ask to repeat |
Audit Trail
All calls generate audit records:
{
"call_id": "call_abc123xyz",
"clinic_id": "uuid-here",
"caller_phone": "+16045551234",
"demographic_id": 12345,
"language": "en",
"intent": "book",
"outcome": "booked",
"appointment_id": 99001,
"duration_seconds": 127,
"transferred": false,
"created_at": "2026-01-12T14:32:00Z"
}
Note: No PHI is stored in call logs. Patient names, DOB, and medical details are never logged.
Next Steps