Database Schema¶
PostgreSQL 16 Multi-Tenant Schema
Database Configuration¶
| Setting | Value |
|---|---|
| Database | vitara |
| User | vitara |
| Port | 5432 (internal) |
| Version | PostgreSQL 16 Alpine |
| Volume | vitara-db-data |
Schema Overview¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ VITARA DATABASE SCHEMA │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ clinics │──────────────────┐ │
│ │ (UUID PK) │ │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ │ 1:1 │ 1:N │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ clinic_config │ │ │
│ │ (clinic_id FK) │ │ │
│ └─────────────────┘ │ │
│ │ │
│ ┌─────────────────┐ ┌────────────────┴─────┐ ┌─────────────────┐ │
│ │ clinic_hours │ │ clinic_providers │ │ clinic_holidays │ │
│ │ (clinic_id FK) │ │ (clinic_id FK) │ │ (clinic_id FK) │ │
│ └─────────────────┘ └──────────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ waitlist │ │ call_logs │ │
│ │ (clinic_id FK) │ │ (clinic_id FK) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ users │ │ audit_logs │ │
│ │ (clinic_id FK) │ │ (user_id FK) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Table Definitions¶
clinics¶
Primary clinic registration table.
CREATE TABLE clinics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
phone VARCHAR(20),
vapi_phone VARCHAR(20),
address TEXT,
city VARCHAR(100),
province VARCHAR(50) DEFAULT 'BC',
postal_code VARCHAR(10),
timezone VARCHAR(50) DEFAULT 'America/Vancouver',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_clinics_slug ON clinics(slug);
CREATE INDEX idx_clinics_vapi_phone ON clinics(vapi_phone);
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
name |
VARCHAR(255) | Clinic display name |
slug |
VARCHAR(100) | URL-safe identifier |
phone |
VARCHAR(20) | Main clinic phone |
vapi_phone |
VARCHAR(20) | Vapi phone number |
address |
TEXT | Street address |
city |
VARCHAR(100) | City |
province |
VARCHAR(50) | Province (default: BC) |
postal_code |
VARCHAR(10) | Postal code |
timezone |
VARCHAR(50) | IANA timezone |
is_active |
BOOLEAN | Active status |
clinic_config¶
Per-clinic configuration including OSCAR credentials and EMR live status (enhanced in v1.5.0).
CREATE TABLE clinic_config (
clinic_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clinic_name VARCHAR(255) NOT NULL,
clinic_slug VARCHAR(100) UNIQUE NOT NULL,
clinic_status VARCHAR(50) DEFAULT 'pending_approval',
clinic_timezone VARCHAR(50) DEFAULT 'America/Vancouver',
clinic_created_at TIMESTAMP DEFAULT NOW(),
clinic_updated_at TIMESTAMP DEFAULT NOW(),
-- Contact Information
contact_email VARCHAR(255),
contact_phone VARCHAR(20),
contact_address TEXT,
-- Registration Settings
accepting_new_patients BOOLEAN DEFAULT true,
waitlist_enabled BOOLEAN DEFAULT true,
max_new_patients_per_week INTEGER,
-- EMR Configuration (v1.5.0+)
emr_type VARCHAR(50) DEFAULT 'oscar',
emr_live BOOLEAN DEFAULT false,
emr_config JSONB DEFAULT '{}',
-- OSCAR Connection (encrypted at application layer)
oscar_url VARCHAR(500),
oscar_api_key VARCHAR(255),
oscar_consumer_key VARCHAR(255),
oscar_consumer_secret_encrypted TEXT,
oscar_token_key VARCHAR(255),
oscar_token_secret_encrypted TEXT,
oscar_connection_verified BOOLEAN DEFAULT false,
oscar_last_verified_at TIMESTAMP,
oscar_default_provider VARCHAR(50),
-- Voice Settings
default_language VARCHAR(10) DEFAULT 'en',
supported_languages TEXT[] DEFAULT ARRAY['en', 'zh'],
-- Preferences (JSONB)
preferences JSONB DEFAULT '{}'
);
CREATE INDEX idx_clinic_config_slug ON clinic_config(clinic_slug);
CREATE INDEX idx_clinic_config_status ON clinic_config(clinic_status);
| Column | Type | Description |
|---|---|---|
clinic_id |
UUID | Primary key |
clinic_name |
VARCHAR(255) | Clinic display name |
clinic_slug |
VARCHAR(100) | URL-safe identifier |
clinic_status |
VARCHAR(50) | active, pending_approval, suspended |
emr_type |
VARCHAR(50) | EMR type (e.g., oscar, telus) |
emr_live |
BOOLEAN | When true, uses real EMR; when false, demo mode |
emr_config |
JSONB | Per-clinic EMR configuration |
oscar_url |
VARCHAR(500) | OSCAR REST Bridge URL |
oscar_api_key |
VARCHAR(255) | API key for OSCAR |
oscar_default_provider |
VARCHAR(50) | Default provider number |
preferences |
JSONB | Booking rules, reminder hours, etc. |
Status Values:
| Status | Description |
|---|---|
pending_approval |
New clinic awaiting setup |
active |
Active but EMR not live |
live |
EMR integration enabled |
suspended |
Temporarily disabled |
clinic_hours¶
Working hours per day of week.
CREATE TABLE clinic_hours (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
day_of_week INTEGER NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
open_time TIME,
close_time TIME,
is_closed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(clinic_id, day_of_week)
);
CREATE INDEX idx_clinic_hours_clinic ON clinic_hours(clinic_id);
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
clinic_id |
UUID (FK) | References clinics |
day_of_week |
INTEGER | 0=Sunday, 6=Saturday |
open_time |
TIME | Opening time |
close_time |
TIME | Closing time |
is_closed |
BOOLEAN | Closed all day |
clinic_holidays¶
Holiday closures.
CREATE TABLE clinic_holidays (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
date DATE NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(clinic_id, date)
);
CREATE INDEX idx_clinic_holidays_date ON clinic_holidays(clinic_id, date);
clinic_providers¶
Provider display names (overrides OSCAR names).
CREATE TABLE clinic_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
oscar_provider_id VARCHAR(50) NOT NULL,
display_name VARCHAR(255) NOT NULL,
specialty VARCHAR(100),
accepting_new_patients BOOLEAN DEFAULT true,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(clinic_id, oscar_provider_id)
);
waitlist¶
New patient waitlist entries.
CREATE TABLE waitlist (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR(255),
notes TEXT,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW(),
contacted_at TIMESTAMP,
registered_at TIMESTAMP
);
CREATE INDEX idx_waitlist_clinic ON waitlist(clinic_id);
CREATE INDEX idx_waitlist_status ON waitlist(status);
| Status Values | Description |
|---|---|
pending |
Awaiting contact |
contacted |
Staff has reached out |
registered |
Now a patient |
cancelled |
Removed from list |
call_logs¶
Call analytics and outcomes.
CREATE TABLE call_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clinic_id UUID REFERENCES clinics(id),
vapi_call_id VARCHAR(255),
caller_phone VARCHAR(20),
demographic_id INTEGER,
language VARCHAR(10),
intent VARCHAR(50),
outcome VARCHAR(50),
appointment_id INTEGER,
duration_seconds INTEGER,
transferred_to_staff BOOLEAN DEFAULT false,
transfer_reason VARCHAR(255),
error_message TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_call_logs_clinic ON call_logs(clinic_id);
CREATE INDEX idx_call_logs_created ON call_logs(created_at);
CREATE INDEX idx_call_logs_outcome ON call_logs(outcome);
| Intent Values | Description |
|---|---|
book |
Book appointment |
reschedule |
Reschedule existing |
cancel |
Cancel appointment |
register |
New patient registration |
info |
Clinic information |
other |
Other/unclear |
| Outcome Values | Description |
|---|---|
booked |
Appointment created |
rescheduled |
Appointment moved |
cancelled |
Appointment cancelled |
registered |
Patient registered |
waitlisted |
Added to waitlist |
transferred |
Transferred to staff |
abandoned |
Caller hung up |
error |
System error |
admin_users¶
Admin dashboard users (v1.5.0+).
CREATE TABLE admin_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
role VARCHAR(50) NOT NULL DEFAULT 'clinic_manager',
clinic_id UUID REFERENCES clinic_config(clinic_id),
is_active BOOLEAN DEFAULT true,
failed_login_attempts INTEGER DEFAULT 0,
locked_until TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_admin_users_email ON admin_users(email);
CREATE INDEX idx_admin_users_clinic ON admin_users(clinic_id);
CREATE INDEX idx_admin_users_role ON admin_users(role);
| Role Values | Description |
|---|---|
vitara_admin |
Platform administrators with full system access |
clinic_manager |
Single-clinic administrators for self-service |
audit_log¶
Immutable audit trail for admin actions (v1.5.0+).
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES admin_users(id),
user_email VARCHAR(255),
user_role VARCHAR(50),
action VARCHAR(100) NOT NULL,
resource_type VARCHAR(50),
resource_id UUID,
changes JSONB,
metadata JSONB,
ip_address INET,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_audit_log_user ON audit_log(user_id);
CREATE INDEX idx_audit_log_action ON audit_log(action);
CREATE INDEX idx_audit_log_resource ON audit_log(resource_type, resource_id);
CREATE INDEX idx_audit_log_created ON audit_log(created_at);
| Column | Type | Description |
|---|---|---|
user_id |
UUID | Acting user |
user_email |
VARCHAR(255) | Email at time of action (for display) |
user_role |
VARCHAR(50) | Role at time of action |
action |
VARCHAR(100) | e.g., auth.login, clinic.update |
resource_type |
VARCHAR(50) | e.g., clinic_config, admin_user |
resource_id |
UUID | Affected resource ID |
changes |
JSONB | Before/after values |
metadata |
JSONB | Additional context |
Note: JSONB columns are auto-parsed by the PostgreSQL pg driver. The backend uses safeJsonParse() helper for safe handling.
Demo Data¶
Initial seed data for development.
-- Demo clinic
INSERT INTO clinics (id, name, slug, phone, vapi_phone, address, city)
VALUES (
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'VitaraPlatform Demo Clinic',
'demo-clinic',
'604-555-0100',
'+16045550100',
'123 Demo Street',
'Vancouver'
);
-- Demo clinic config
INSERT INTO clinic_config (clinic_id, accepting_new_patients, waitlist_enabled)
VALUES ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', true, true);
-- Demo hours (Mon-Fri 9-5)
INSERT INTO clinic_hours (clinic_id, day_of_week, open_time, close_time, is_closed)
VALUES
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 0, NULL, NULL, true), -- Sunday
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 1, '09:00', '17:00', false), -- Monday
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 2, '09:00', '17:00', false), -- Tuesday
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 3, '09:00', '17:00', false), -- Wednesday
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 4, '09:00', '17:00', false), -- Thursday
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 5, '09:00', '17:00', false), -- Friday
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 6, NULL, NULL, true); -- Saturday
Migrations¶
Location¶
/opt/vitara-platform/voice-agent/migrations/
Files¶
| File | Description |
|---|---|
001_add_emr_columns.sql |
Add emr_type and emr_config columns (v1.4.0) |
001_add_emr_columns_rollback.sql |
Rollback script for EMR columns |
002_admin_system.sql |
Admin users, audit_log, enhanced clinic_config (v1.5.0) |
003_seed_data.sql |
Demo clinics, users, and audit log entries (v1.5.1) |
Running Migrations¶
# Connect to database
docker exec -it vitara-db psql -U vitara -d vitara
# Run migrations in order
docker exec vitara-db psql -U vitara -d vitara \
-f /migrations/001_add_emr_columns.sql
docker exec vitara-db psql -U vitara -d vitara \
-f /migrations/002_admin_system.sql
docker exec vitara-db psql -U vitara -d vitara \
-f /migrations/003_seed_data.sql
Backup & Recovery¶
Backup¶
# Full backup
docker exec vitara-db pg_dump -U vitara vitara > backup_$(date +%Y%m%d).sql
# Schema only
docker exec vitara-db pg_dump -U vitara -s vitara > schema.sql
# Data only
docker exec vitara-db pg_dump -U vitara -a vitara > data.sql