Skip to content

Server API & Data Flows

Complete reference for the VitaraVox Admin API server — Express 4 + TypeScript running on port 3002, managed by PM2 as vitara-admin-api.

Version: v4.3.0 | Last verified: 2026-03-09


Server Stack

Component Technology Purpose
Runtime Node.js + TypeScript Server language
Framework Express 4 HTTP routing
ORM Prisma 5.22 Database access
Database PostgreSQL 16 Persistent storage
Validation Zod Request/response schemas
Logging Pino + pino-http Structured JSON logging (5 layers)
Resilience opossum Circuit breakers (4s timeout, 50% threshold)
Process Manager PM2 vitara-admin-api via npx tsx src/index.ts
Auth JWT (stateless) Bearer token authentication — no DB sessions
SMS Telnyx Platform-level SMS via per-clinic sender numbers

1. Request Lifecycle

Every HTTP request passes through the middleware stack in exact mount order from index.ts:33-79.

┌──────────────────────────────────────────────────────────────────────┐
│                       INCOMING HTTP REQUEST                          │
└─────────────────────────────┬────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│  1. trust proxy                                            index:36  │
│     Express trusts X-Forwarded-For (behind nginx/Caddy)              │
├──────────────────────────────────────────────────────────────────────┤
│  2. Helmet                                                 index:39  │
│     Security headers (X-Frame-Options, CSP, HSTS, etc.)              │
├──────────────────────────────────────────────────────────────────────┤
│  3. Request Logger (pino-http)                  request-logger:14-32 │
│     Correlation ID: x-request-id header OR randomUUID()              │
│     Level: >=500 → error, >=400 → warn, else → info                 │
│     Redacts: authorization, x-api-key, cookie                        │
│     Skips: /health endpoint                                          │
├──────────────────────────────────────────────────────────────────────┤
│  4. CORS + Body Parser                                  index:45-49  │
│     Configurable origins, JSON parsing                               │
├──────────────────────────────────────────────────────────────────────┤
│  5. Audit Middleware                                      audit:1-100 │
│     POST/PUT/PATCH/DELETE → AuditLog table                           │
│     Excludes: /api/vapi/*, /vapi-webhook, /health                    │
│     Redacts 14 sensitive keys (passwords, tokens, secrets)           │
│     Non-blocking (fire-and-forget DB write)                          │
├──────────────────────────────────────────────────────────────────────┤
│  6. Rate Limiter (per route group)              rate-limit:15-39     │
│     See §2 Route Map for which limiter applies to each group         │
├──────────────────────────────────────────────────────────────────────┤
│  7. Auth (per route)                                                 │
│     Webhook routes: HMAC-SHA256 / API Key / Bearer   vapi-auth:32-80│
│     Dashboard routes: JWT Bearer → req.user             auth:15-24   │
├──────────────────────────────────────────────────────────────────────┤
│  8. Zod Validation (per route)                                       │
│     Schema validation on params, query, body                         │
└─────────────────────────────┬────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│                         ROUTE HANDLER                                │
│                                                                      │
│   Route Handler → Service Layer → EMR Adapter → OSCAR API            │
│                         │                                            │
│                         ▼                                            │
│                Prisma (PostgreSQL 16)                                 │
└─────────────────────────────┬────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│                        HTTP RESPONSE                                 │
│   { status, data?, error?, code? }                                   │
└──────────────────────────────────────────────────────────────────────┘

2. Route Map

Routes are mounted in index.ts:55-79. Mount order matters — webhook routes MUST come before /api to avoid path conflicts.

Rate Limiting (3-tier)

Limiter Window Max Applied To Source
authLimiter 60s 5 req /api/auth/* rate-limit.ts:15-21
webhookLimiter 60s 300 req /api/vapi/*, /vapi-webhook rate-limit.ts:23-29
apiLimiter 60s 100 req /api/*, /api/fax/* rate-limit.ts:31-39

All limiters use standardHeaders: true, legacyHeaders: false and return 429 Too Many Requests as JSON.

Health Check (no auth)

Method Path Handler Source
GET /health healthService.getHealth() index.ts:55-63

Auth Routes (/api/auth/* — authLimiter)

Method Path Auth Handler
POST /api/auth/login No authService.login(email, password) →
POST /api/auth/refresh No authService.refreshAccessToken(refreshToken) →
GET /api/auth/me JWT authService.getUserById(userId)
POST /api/auth/logout JWT Returns success (stateless — no DB session invalidation)

No Registration Endpoint

There is no POST /api/auth/register. Admin accounts are created via the admin provisioning flow.

Webhook Routes (/api/vapi/* — webhookLimiter + vapiWebhookAuth)

Method Path Auth Handler
POST /api/vapi HMAC/Key/Bearer Main Vapi webhook handler
POST /vapi-webhook HMAC/Key/Bearer Legacy URL (same handler)

Dashboard API Routes (/api/* — apiLimiter + JWT auth)

All routes below require authMiddleware (valid JWT).

Clinic Manager Routes (own clinic only)

Method Path Extra Auth Purpose
GET /api/clinic/me Get current user's clinic
PUT /api/clinic/me Zod Update clinic details
GET /api/clinic/stats Call statistics
GET /api/clinic/calls Call history (paginated)
GET /api/clinic/settings Clinic settings
GET /api/clinic/analytics Analytics data
GET /api/clinic/call-history Call history with filtering
GET/POST /api/clinic/waitlist Waitlist list/create
PUT/DELETE /api/clinic/waitlist/:id Waitlist update/delete
POST /api/clinic/waitlist/bulk Bulk waitlist update
GET/POST /api/clinic/providers Provider list/create
PUT/DELETE /api/clinic/providers/:id Provider update/delete
POST /api/clinic/providers/sync Sync providers from OSCAR
GET /api/clinic/providers/:id/effective-config requireClinicAccess Effective provider config (3-level merge)
PUT /api/clinic/providers/:id/config requireClinicAccess + Zod Update provider voice config
PUT /api/clinic/voice-config requireClinicAccess + Zod Update clinic voice booking config
GET/PUT /api/clinic/schedule Schedule config
POST /api/clinic/schedule/sync Sync schedule from OSCAR
GET/PUT /api/clinic/emr EMR settings
POST /api/clinic/emr/test Test EMR connection
POST /api/clinic/emr/test-schedule Test schedule data flow
POST /api/clinic/emr/sync Full OSCAR sync
POST/DELETE /api/clinic/logo Logo upload/delete
GET /api/clinic/vapi/calls Vapi call list
GET /api/clinic/vapi/calls/:id Single Vapi call
GET /api/clinic/vapi/analytics Vapi analytics
GET /api/clinic/vapi/assistants List assistants

OSCAR Config Routes (requireClinicAccess)

Method Path Purpose
POST /api/clinic/oscar-config/pull Pull all OSCAR config
POST /api/clinic/oscar-config/pull/:domain Pull domain-specific config
GET /api/clinic/oscar-config Get OSCAR config
PUT /api/clinic/oscar-config/booking-constraints Update booking constraints
PUT /api/clinic/oscar-config/template-codes Update template code overrides
PUT /api/clinic/oscar-config/appointment-types Update appointment type mappings

SMS Config Routes

Method Path Purpose
GET /api/clinic/sms-config Get SMS configuration
PUT /api/clinic/sms-config Update SMS settings
POST /api/clinic/sms/test Send test SMS

Compliance Routes

Method Path Purpose
GET/PUT /api/clinic/privacy-officer Privacy officer management
GET/PUT /api/clinic/baa-status BAA/DPA status
GET/PUT /api/clinic/data-retention Data retention settings

Onboarding Routes (requireClinicAccess)

Method Path Purpose
GET/PUT /api/clinic/onboarding Onboarding wizard state
GET /api/clinic/onboarding/validate Validate checklist
POST /api/clinic/onboarding/go-live Mark clinic go-live
POST /api/clinic/onboarding/complete Mark onboarding complete
POST /api/clinic/onboarding/test-slots Test booking slots

OSCAR OAuth Routes (no auth — browser redirect flow)

Method Path Purpose
POST /api/oscar-oauth/initiate Start 3-leg OAuth flow
GET /api/oscar-oauth/callback Receive OAuth verifier, exchange tokens

Admin-Only Routes (requireRole('vitara_admin'))

Method Path Purpose
GET /api/admin/clinics List all clinics
GET /api/admin/clinics/overview Clinic summary overview
GET /api/admin/clinics/pending-activation Clinics awaiting activation
GET /api/admin/clinics/:clinicId Get specific clinic
GET /api/admin/clinics/:clinicId/stats Clinic stats
GET /api/admin/clinics/:clinicId/calls Clinic call history
PUT /api/admin/clinics/:clinicId/vapi Configure Vapi for clinic
POST /api/admin/clinics/:clinicId/vapi/test Test Vapi connection
DELETE /api/admin/clinics/:clinicId/vapi Unconfigure Vapi
GET /api/admin/stats Platform-wide stats
GET /api/admin/health System health (all services)
GET /api/admin/health/detailed Detailed health breakdown
GET/POST /api/admin/debug Get/set debug mode (VITARA_DEBUG)
GET /api/admin/vapi/mappings Vapi phone→clinic mappings
GET /api/admin/vapi/phone-numbers Vapi phone numbers
GET /api/admin/audit Audit log viewer
POST /api/admin/data-retention/run Manually run data retention

Fax Routes (apiLimiter + JWT auth)

Method Path Auth Purpose
POST /api/fax/upload JWT Upload fax for AI processing
GET /api/fax/documents JWT List faxes (paginated, filterable by status)
GET /api/fax/documents/:id JWT Get single fax detail
PUT /api/fax/documents/:id/verify JWT Confirm/correct patient match
POST /api/fax/polling/start JWT Start fax poller
POST /api/fax/polling/stop JWT Stop fax poller
GET /api/fax/polling/status None Poller status + counts
POST /api/fax/seed JWT Seed fixture fax (demo)
POST /api/fax/reset JWT Reset mock inbox (demo)
GET /api/fax/fixtures None List available fixtures (demo)

Public Route (no auth)

Method Path Purpose
GET /api/oscar/health OSCAR connectivity check

3. Vapi Webhook Pipeline

When a Vapi voice agent invokes a tool, the request flows through a dedicated pipeline in vapi-webhook.ts.

┌──────────────────────────────────────────────────────────────────────┐
│                       VAPI VOICE AGENT                               │
│  (Squad: Router → Patient-ID → Booking/Modification/Registration)    │
└────────────────────────────┬─────────────────────────────────────────┘
                             │  POST /api/vapi
                             │  Body: { message: { type, ... } }
┌──────────────────────────────────────────────────────────────────────┐
│  webhookLimiter (300 req/min)                    rate-limit:23-29    │
├──────────────────────────────────────────────────────────────────────┤
│  vapiWebhookAuth                                 vapi-auth:32-80     │
│    1. HMAC-SHA256 (x-vapi-signature + timestamp, 5min replay)        │
│    2. API Key fallback (x-api-key header)                            │
│    3. Bearer token fallback (Authorization header)                   │
│    4. Dev mode: skip if no secret configured                         │
├──────────────────────────────────────────────────────────────────────┤
│  MESSAGE TYPE DISPATCH                       vapi-webhook:409+       │
│                                                                      │
│    tool-calls ──────────► handleToolCall()         (line 410)        │
│    assistant-request ───► resolveAssistant()       (line 503)        │
│    status-update ───────► handleStatusUpdate()     (line 510)        │
│    end-of-call-report ──► saveCallLog()            (line 517)        │
│    transfer-destination ► handleTransferDest()     (line 544)        │
└──────────────────────────────────────────────────────────────────────┘
                             │  tool-calls path (primary flow)
┌──────────────────────────────────────────────────────────────────────┐
│  1. CLINIC RESOLUTION                        vapi-webhook:317-343    │
│     vapiPhone cache → Clinic lookup by phone number                  │
│     Fallback: Vapi API phone number fetch + 1h cache                 │
│                                                                      │
│  2. voiceAgentEnabled CHECK                                          │
│     If disabled → reject with "Voice agent is not active"            │
│                                                                      │
│  3. PARAMETER EXTRACTION                                             │
│     Parse toolCallList[0].function.arguments                         │
│     Extract call metadata (call.customer.number, assistantId)        │
│                                                                      │
│  4. TOOL DISPATCH                            vapi-webhook:620-1400+  │
│     Route to handler by function name (see §4)                       │
│                                                                      │
│  5. SMS TRIGGER (if applicable)              vapi-webhook:259-305    │
│     create/cancel/update → fireSmsBehindWebhook()                    │
│     Uses setImmediate() to not block webhook response                │
│                                                                      │
│  6. RESPONSE FORMAT                                                  │
│     { results: [{ toolCallId, result: "natural language string" }] } │
└──────────────────────────────────────────────────────────────────────┘

Key Design Decisions

Decision Rationale Source
Caller phone from call.customer.number Telnyx metadata is authoritative; LLM input unreliable vapi-webhook.ts:755-778
Past-date clamping to today GPT-4o unreliable with dates; server enforces safety vapi-webhook.ts
Single-slot returns Concise for voice interactions (caller can't review a list) vapi-webhook.ts
Non-numeric providerId → "any" Only /^\d+$/ treated as specific provider vapi-webhook.ts
SMS via setImmediate Fire-and-forget so webhook responds within Vapi's 5s timeout vapi-webhook.ts:274

4. Tool Call Dispatch

14 tools available via the Vapi webhook. Each tool maps to a handler function in vapi-webhook.ts.

Tool Name Handler Lines EMR Method SMS Trigger Description
search_patient 627-753 searchPatient(query) Search patient by name
search_patient_by_phone 755-778 searchPatientByPhone(phone) Search by caller's phone (uses call.customer.number)
get_patient 780-795 getPatient(id) Get patient demographics by ID
get_clinic_info 797+ — (DB lookup) Return clinic info, hours, providers
get_providers 818-846 getProviders() List clinic providers
find_earliest_appointment 850-883 findEarliestSlot() Find next available slot
check_appointments 887-1011 getAppointments() Check existing appointments
create_appointment 887-1011 bookAppointment() Yes Book appointment (advisory lock + collision check)
cancel_appointment 1013-1048 cancelAppointment() Yes Cancel by appointment ID
update_appointment 1050-1170 book new + cancel old Yes Reschedule (book-then-cancel pattern)
register_new_patient 1174-1272 registerPatient() Register new patient in OSCAR
transfer_call 1276-1287 Transfer to clinic phone
log_call_metadata 1290+ — (cache write) Write to callMetadataCache

SMS Trigger Flow

When create_appointment, cancel_appointment, or update_appointment succeeds and the caller provided smsConsent: true:

Tool handler completes
fireSmsBehindWebhook()                          vapi-webhook:259-305
        │  setImmediate()                        (line 274)
        │  (non-blocking — webhook response already sent)
sms.service.ts guard chain (6 checks)           sms.service:174-218
Telnyx POST /v2/messages                         sms.service:243-250
CallLog.smsSentAt = now()                        (DB update)

5. In-Memory Caches

5 in-memory caches operate in the server process. None are shared across PM2 instances.

Cache Type TTL Cleanup Source
adapterCache Map<string, {adapter, created}> 5 min 10 min interval (.unref()) EmrAdapterFactory.ts:24-33
callMetadataCache Map<string, {data, ts}> 30 min Lazy 1% sweep per access vapi-webhook.ts:228-250
vapiPhoneCache Map<string, string> 1 hour Weekly refresh from Vapi API vapi-webhook.ts:60-99
templateCodes Map<string, code[]> Per adapter instance Never (lives with adapter) OscarSoapAdapter.ts:318
seenFaxIds Set<string> Unbounded Never cleaned fax-poller.service.ts:25
pendingRequests (OAuth) Map<string, request> 10 min 5 min interval oscar-oauth.ts

seenFaxIds Memory Leak

seenFaxIds grows without bound. Acceptable for demo/pilot but needs TTL or periodic clearing before production scale.


6. Logging Architecture

5 layers of logging, each with a distinct purpose.

┌─────────────────────────────────────────────────────────────────┐
│                    LOGGING ARCHITECTURE                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 1: REQUEST LOGGER (pino-http)     request-logger:14-32   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ Every HTTP req/res logged with:                         │    │
│  │   - Correlation ID (x-request-id or randomUUID)         │    │
│  │   - Method, URL, status, response time                  │    │
│  │   - Redacted: authorization, x-api-key, cookie          │    │
│  │   - Level: 5xx→error, 4xx→warn, else→info               │    │
│  │   - Skips: /health                                      │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  Layer 2: STRUCTURED LOGGER (Pino)             logger:13-64     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ Production: JSON to stdout (for log aggregation)        │    │
│  │ Development: pino-pretty (color, timestamps)            │    │
│  │ Level: LOG_LEVEL env || prod ? 'info' : 'debug'         │    │
│  │ Dynamic: setLogLevel() changes at runtime               │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  Layer 3: DEBUG LOGGER (PHI-aware)             logger:48-62     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ debugLog(msg, data) — only when VITARA_DEBUG active     │    │
│  │ Level: trace | Prefix: [PHI-DEBUG]                      │    │
│  │ Contains: patient names, DOB, phones, health cards      │    │
│  │ Auto-expires after 4 hours                              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  Layer 4: PHI REDACTION (webhook)        vapi-webhook:195-220   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ redactPhi() strips 15 field types from webhook logs:    │    │
│  │   patient names, phone, DOB, HIN, address, etc.         │    │
│  │ Applied to all Vapi webhook log entries                  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  Layer 5: AUDIT LOG (DB-persisted)              audit:1-100     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ All POST/PUT/PATCH/DELETE → AuditLog table              │    │
│  │ Captures: userId, action, resource, IP, userAgent       │    │
│  │ Redacts: 14 sensitive keys (passwords, tokens, secrets) │    │
│  │ Immutable: no UPDATE/DELETE at application layer         │    │
│  │ Retention: 7 years (PIPEDA 4.1.4)                       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  QUERY LOGGING (conditional)                 prisma:19-27       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ Prisma query logging enabled when debugManager active   │    │
│  │ Shows raw SQL queries in dev/debug mode                 │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Debug Mode (VITARA_DEBUG)

Aspect Detail Source
Activation (env) VITARA_DEBUG=true in .env (requires restart) debug-manager:51
Activation (API) POST /api/admin/debug {enabled: true} (no restart) debug-manager:61
TTL 4 hours auto-expiry (prevents accidental production PHI exposure) debug-manager:17
What it enables Verbose logging including PHI (names, DOB, phones, health cards) logger:48-62
What it does NOT disable HMAC, CORS, rate limiting — only enables verbose logging
Timer setTimeout with .unref() (doesn't block graceful shutdown) debug-manager:117-121

7. Circuit Breaker

The server uses opossum circuit breakers to protect against slow or failing OSCAR connections. Configuration from OscarSoapAdapter.ts:478-496.

                         success
                    ┌───────────────┐
                    │               │
                    ▼               │
              ┌───────────┐        │
     ────────>│  CLOSED   │────────┘
              │           │
              │ Normal    │
              │ operation │
              └─────┬─────┘
                    │  50% failure threshold
                    │  exceeded
              ┌───────────┐
              │   OPEN    │
              │           │
              │ All calls │──────────────────┐
              │ fail-fast │                  │
              └─────┬─────┘                  │
                    │                        │
                    │  30s cooldown          │  call attempt
                    │  timer expires         │  (while open)
                    ▼                        │
              ┌───────────┐                  │
              │ HALF_OPEN │                  │
              │           │                  │
              │ Next call │                  │
              │ is a test │                  │
              └─────┬─────┘                  │
                    │                        │
               ┌────┴────┐                   │
               │         │                   │
           success    failure                │
               │         │                   │
               ▼         ▼                   ▼
           ┌───────┐ ┌───────┐      ┌──────────────┐
           │CLOSED │ │ OPEN  │      │ Immediate    │
           │(reset)│ │(reset │      │ rejection    │
           │       │ │ timer)│      │ (no network) │
           └───────┘ └───────┘      └──────────────┘

Configuration:

Parameter Value Source
Timeout 4 seconds OscarSoapAdapter.ts:479
Failure threshold 50% OscarSoapAdapter.ts:480
Reset timeout 30 seconds OscarSoapAdapter.ts:481
Rolling count buckets 10 OscarSoapAdapter.ts:482
Rolling count timeout 10s OscarSoapAdapter.ts:483

Events logged:

Event Level Source
Circuit opens warn OscarSoapAdapter.ts:485-487
Circuit half-open info OscarSoapAdapter.ts:488-490
Circuit closes info OscarSoapAdapter.ts:491-493

OAuth Expiry Pre-check

Before making OSCAR calls, the adapter checks oscarOauthTokenExpiresAt. If the token is expired, the call is rejected immediately without hitting the circuit breaker (OscarSoapAdapter.ts:526-529).


8. Scheduled Jobs

Two cron jobs run via node-cron in jobs/scheduler.ts.

Job Cron Schedule Handler Source
Data Retention 0 3 * * * Daily 3:00 AM UTC runDataRetention() scheduler:16-22
OAuth Token Monitor 0 6,18 * * * 6 AM + 6 PM UTC checkOAuthTokenExpiry() scheduler:25-31

Data Retention (data-retention.ts)

Per-clinic configurable retention policy:

Data Type Default Action Config Field
Call transcripts 90 days NULL transcript column (preserve row + metadata) transcriptRetentionDays
Call log rows 365 days DELETE entire row callLogRetentionDays
Audit logs 7 years No automatic deletion (compliance) N/A

Returns: { transcriptsNulled, callLogsDeleted, clinicsProcessed }.

OAuth Token Monitor (scheduler.ts:25-31)

  • Warning threshold: 48 hours before expiry
  • Finds all clinics with oscarOauthTokenExpiresAt < (now + 48h)
  • If hoursLeft <= 0: ERROR "EXPIRED" — patient operations will fail
  • If hoursLeft > 0: WARN "EXPIRING SOON" — re-run OAuth flow needed
  • Logs: clinicId, clinicName, expiresAt, hoursLeft

9. Health Check

GET /health runs 3 parallel service checks (from health.service.ts:31-119).

GET /health
     ├──────────────┬──────────────────┬─────────────────────┐
     │              │                  │                     │
     ▼              ▼                  ▼                     │
┌──────────┐  ┌──────────────┐  ┌──────────────┐            │
│ Database │  │ OSCAR Bridge │  │  Vapi API    │            │
│          │  │              │  │              │            │
│ SELECT 1 │  │ GET /health  │  │ GET          │  5s timeout│
│ via      │  │ + X-API-Key  │  │ /assistant   │  each      │
│ Prisma   │  │              │  │ + Bearer     │            │
└────┬─────┘  └──────┬───────┘  └──────┬───────┘            │
     │               │                │                     │
     ▼               ▼                ▼                     │
 latencyMs       latencyMs        latencyMs                 │
     │               │                │                     │
     └───────────────┴────────────────┘                     │
                     │                                      │
                     ▼                                      │
              ┌──────────────┐                              │
              │ Status Logic │                              │
              │              │                              │
              │ all healthy  │──► 'healthy'                 │
              │ db/oscar down│──► 'down'                    │
              │ otherwise    │──► 'degraded'                │
              └──────────────┘                              │

Success conditions:

Service Healthy If Source
Database SELECT 1 succeeds health.service.ts:50-59
OSCAR Bridge HTTP status < 500 (includes 401, 404) health.service.ts:61-84
Vapi API HTTP status OK or 401 (reachable) health.service.ts:86-115

10. Ops Service

Administrative operations via ops.service.ts:152-529. Available to vitara_admin role.

Method Timeout Purpose Source
getSystemStatus() PM2, disk, memory, OS, node version, uptime ops:158-178
deploy() 60s build + 15s PM2 TypeScript build (tsc) + PM2 graceful restart ops:183-234
vapiGitOpsPush() 45s Push Vapi GitOps config (npm run push:dev) ops:239-257
pm2Action(action) 15s restart OR reload vitara-admin-api ops:262-270
warmSoapClients() 8s/clinic Warm SOAP WSDL for all soap-type clinics ops:275-335
getLogs(lines) 15s Tail PM2 logs (10-500 lines, default 100) ops:340-347
getBackups() List backup files (sorted newest first) ops:352-374
deployDocs() 60s MkDocs build + copy to site/ ops:379-398
getEnvConfig() sync Audit env vars (presence only, never values) ops:403-409
testSoap() Test OSCAR SOAP connectivity ops:414-447
getGitStatus() 15s Branch, commit hash, message, date, dirty flag ops:452-468

Deploy Lock

A deployLock flag (ops.service.ts:153) prevents concurrent deploy operations. If a deploy is in progress, subsequent deploy requests are rejected.

Hardcoded paths:

Constant Path
SERVER_DIR /home/ubuntu/vitara-platform/admin-dashboard/server
PLATFORM_DIR /home/ubuntu/vitara-platform
VAPI_GITOPS_DIR /home/ubuntu/vitara-platform/vapi-gitops
BACKUPS_DIR /home/ubuntu/vitara-platform/backups

11. External API Calls

Every outbound HTTP call the server makes.

Service Method URL Auth Trigger
OSCAR (SOAP) POST {oscarUrl}/ws/XxxService?wsdl WS-Security UsernameToken EMR adapter calls
OSCAR (REST) GET/POST {oscarUrl}/ws/services/* OAuth 1.0a HMAC-SHA1 EMR adapter calls (preferRest)
OSCAR (OAuth) GET/POST {oscarUrl}/oauth/* OAuth 1.0a Token exchange flow
Vapi API GET https://api.vapi.ai/assistant Bearer token Health check
Vapi API GET https://api.vapi.ai/phone-number Bearer token Phone cache refresh
Telnyx POST https://api.telnyx.com/v2/messages API key in header SMS confirmation
OSCAR Bridge GET {bridgeUrl}/health X-API-Key header Health check

12. Error Handling

Vapi Tool Error Format

Tool call errors are returned as natural language strings (the LLM reads them and speaks to the patient):

{
  "results": [{
    "toolCallId": "abc123",
    "result": "I was unable to search for patients at this time. The clinic system is temporarily unavailable."
  }]
}

HTTP Error Response Format

Dashboard API errors follow a consistent structure:

{
  "status": 400,
  "error": "Human-readable error message",
  "code": "MACHINE_READABLE_CODE",
  "details": {}
}
HTTP Status Meaning Common Causes
400 Bad Request Zod validation failure, missing parameters
401 Unauthorized Missing or expired JWT, invalid HMAC signature
403 Forbidden Insufficient role, clinic access mismatch
404 Not Found Clinic/resource not found
429 Too Many Requests Rate limit exceeded (auth: 5/min, API: 100/min, webhook: 300/min)
500 Internal Server Error OSCAR adapter failure, database error
503 Service Unavailable Circuit breaker OPEN for target clinic

Circuit Breaker Error

When the circuit is OPEN, the adapter returns immediately without making a network call. The webhook handler wraps this as a natural language error for the voice agent.

Graceful Shutdown (index.ts:174-188)

On SIGTERM/SIGINT:

  1. Stop accepting new connections
  2. Drain existing connections for up to 10 seconds
  3. Log warning if forced exit after timeout
  4. The 10s drain time is critical for update_appointment which books-then-cancels (two sequential EMR calls)

Server Startup Sequence

index.ts — startup order
├─ 1. Express app creation + middleware mount       (lines 33-79)
├─ 2. Route registration                            (lines 55-79)
├─ 3. HTTP server.listen(PORT)                       (line 82)
├─ 4. DebugManager.initialize()                      (line 93)
│     └─ Checks VITARA_DEBUG env var
├─ 5. Scheduler.start()                              (line 145)
│     └─ Registers cron jobs (data retention, OAuth monitor)
├─ 6. EMR Adapter pre-warmup (background)           (lines 148-166)
│     └─ For each clinic with oscarUrl:
│        getEmrAdapter(clinicId) — warms SOAP WSDL or REST TLS
│        Non-blocking, errors logged per-clinic (non-fatal)
└─ 7. Startup complete — accepting requests

Authentication Flows

Dashboard JWT (Stateless)

┌──────────┐         ┌──────────┐         ┌──────────┐
│  Client  │         │  Server  │         │   DB     │
└────┬─────┘         └────┬─────┘         └────┬─────┘
     │                    │                    │
     │  POST /auth/login  │                    │
     │  {email, password} │                    │
     │───────────────────>│                    │
     │                    │  Lookup user       │
     │                    │───────────────────>│
     │                    │  User record       │
     │                    │<───────────────────│
     │                    │                    │
     │                    │  bcrypt.compare()  │
     │                    │                    │
     │  { accessToken,    │                    │
     │    refreshToken }  │  (both are JWTs,   │
     │<───────────────────│   no DB session)   │
     │                    │                    │
     │  GET /api/clinic/* │                    │
     │  Authorization:    │                    │
     │  Bearer <token>    │                    │
     │───────────────────>│                    │
     │                    │  jwt.verify()      │
     │                    │  → {userId, role,  │
     │                    │     clinicId}      │
     │                    │                    │
     │  200 OK + data     │                    │
     │<───────────────────│                    │

Stateless JWT

There is no Session table. JWTs are self-contained. POST /auth/logout returns success but does not invalidate the token server-side. Token expiry is the only revocation mechanism.

Vapi Webhook Auth (Multi-method)

POST /api/vapi/*
┌─────────────────────────────┐
│ 1. HMAC-SHA256              │──► Valid? ──► AUTHENTICATED
│    x-vapi-signature header  │
│    Payload: timestamp +     │
│      '.' + JSON.stringify   │
│    5-min replay window      │
│    crypto.timingSafeEqual   │
└────────────┬────────────────┘
             │ No match
┌─────────────────────────────┐
│ 2. API Key                  │──► Match? ──► AUTHENTICATED
│    x-api-key header         │
└────────────┬────────────────┘
             │ No match
┌─────────────────────────────┐
│ 3. Bearer Token             │──► Match? ──► AUTHENTICATED
│    Authorization: Bearer    │
└────────────┬────────────────┘
             │ No match
         401 Unauthorized

Production: REJECTS ALL if no VAPI_WEBHOOK_SECRET configured (500)
Development: SKIPS auth if no secret configured

Onboarding Checks

When POST /api/clinic/onboarding/validate is called, the server runs pre-launch validation:

# Check Blocking Description
1 OSCAR connectivity Yes Can the server reach the OSCAR instance?
2 Authentication Yes Do SOAP/OAuth credentials work?
3 Patient search Yes Does searchPatient return results?
4 Provider sync Yes Can providers be fetched and stored?
5 Schedule read Yes Can schedule slots be retrieved?
6 Appointment read Yes Can existing appointments be listed?
7 Appointment write Yes Can a test appointment be created?
8 Appointment cancel Yes Can the test appointment be cancelled?
9 Schedule data flow No End-to-end data flow validation (informational)