Skip to content

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