Data Flow & Guardrails
Data Ownership, Security Boundaries, and Conversation Flows
Data Ownership Model
VitaraVox maintains clear separation between its own data and clinic EMR data.
What VitaraVox Owns (Vitara Database)
| Data Type |
Purpose |
Retention |
| Clinic profiles |
Name, address, timezone |
Permanent |
| OSCAR credentials |
Encrypted OAuth tokens |
Permanent |
| 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 |
| Call logs |
Analytics, outcomes |
1 year |
| User accounts |
Admin dashboard access |
Permanent |
| Audit logs |
Admin action tracking |
7 years |
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) |
Security Boundaries
Boundary 1: Vapi → Vitara
| Control |
Implementation |
| Transport |
HTTPS (TLS 1.2+) |
| Authentication |
Bearer Token (Vapi credential system) |
| Verification |
Timing-safe token comparison |
| Rate limit |
100 req/min (general), 20 req/min (booking) |
Boundary 2: Vitara → OSCAR
| Control |
Implementation |
| Transport |
HTTPS (TLS 1.2+) |
| Authentication |
OAuth 1.0a (HMAC-SHA1) |
| Credentials |
Per-clinic, encrypted at rest |
| Scope |
Scheduling endpoints only |
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
┌─────────────────────────────────────────────────────────────────────────────┐
│ BOOKING FLOW (EXISTING PATIENT) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. CALL CONNECTS │
│ ───────────── │
│ System: search_patient_by_phone(caller_id) │
│ │
│ ┌──────────────────────┐ │
│ │ Patient found? │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ "Hello [Name]" "Thank you for calling" │
│ │
│ 2. IDENTIFY INTENT │
│ ──────────────── │
│ Patient: "I'd like to book an appointment" │
│ System: Proceed to booking flow │
│ │
│ 3. VERIFY PATIENT (if not identified) │
│ ───────────────────────────────── │
│ System: search_patient(name) │
│ System: "May I have your date of birth?" │
│ Patient: Provides DOB │
│ System: Verify match │
│ │
│ 4. DETERMINE VISIT TYPE │
│ ──────────────────── │
│ System: "In-person or phone consultation?" │
│ Patient: "In-person" │
│ │
│ 5. DETERMINE REASON │
│ ──────────────── │
│ System: "What would you like to see the doctor about?" │
│ Patient: "Prescription refill" │
│ System: Map to type "B" (15 min follow-up) │
│ │
│ 6. FIND APPOINTMENT │
│ ──────────────── │
│ System: find_earliest_appointment(type=B, provider=any) │
│ System: "Earliest is Tuesday at 9:30 AM with Dr. Chen" │
│ │
│ 7. CONFIRM BOOKING │
│ ─────────────── │
│ System: "Shall I book that for you?" │
│ Patient: "Yes" │
│ System: create_appointment(...) │
│ │
│ 8. CONFIRMATION │
│ ──────────── │
│ System: "Confirmed for Tuesday, Jan 14 at 9:30 AM" │
│ System: "Please arrive 5 minutes early" │
│ │
│ 9. CLOSE CALL │
│ ────────── │
│ System: log_call(outcome=booked, language=en) │
│ System: "Thank you for calling!" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
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 |
Never logged; only metadata |
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