Virtual Care Service — Data Model
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD
1. ID Prefix Registry
| Prefix | Entity |
|---|---|
vsn_ | VirtualSession |
vsp_ | VirtualSessionParticipant |
avs_ | AsyncVisit |
tvc_ | TenantVirtualCareConfig |
All IDs are ULIDs with the prefix above.
2. TypeScript Interfaces
// virtual-session.ts
export type VirtualSessionId = Branded<string, 'VirtualSessionId'>;
export type SessionStatus =
| 'scheduled' | 'waiting' | 'active'
| 'ended' | 'cancelled' | 'failed';
export type VideoBackend =
| 'jitsi' | 'mediasoup' | 'zoom' | 'webex' | 'teams';
export interface VirtualSession {
id: VirtualSessionId;
tenantId: string;
nodeId: string;
patientId: string;
appointmentId: string | null;
encounterId: string | null;
initiatorId: string;
videoBackend: VideoBackend;
roomName: string;
roomUrl: string;
status: SessionStatus;
scheduledStart: Date;
scheduledEnd: Date;
actualStart: Date | null;
actualEnd: Date | null;
recordingEnabled: boolean;
recordingRef: string | null;
failureReason: string | null;
messagingThreadId: string | null;
version: number;
createdAt: Date;
updatedAt: Date;
}
// session-participant.ts
export type ParticipantRole = 'provider' | 'patient' | 'interpreter' | 'observer';
export type ParticipantStatus =
| 'invited' | 'waiting' | 'admitted' | 'active' | 'disconnected' | 'removed';
export interface VirtualSessionParticipant {
id: string;
sessionId: string;
tenantId: string;
userId: string;
role: ParticipantRole;
status: ParticipantStatus;
joinedAt: Date | null;
leftAt: Date | null;
removedAt: Date | null;
removedReason: string | null;
}
// async-visit.ts
export type AsyncVisitStatus =
| 'draft' | 'submitted' | 'under_review' | 'responded' | 'closed';
export interface AsyncVisit {
id: string;
tenantId: string;
patientId: string;
appointmentId: string | null;
providerId: string | null;
chiefComplaint: string;
attachmentRefs: string[];
status: AsyncVisitStatus;
clientMutationId: string;
submittedAt: Date | null;
respondedAt: Date | null;
encounterId: string | null;
createdAt: Date;
updatedAt: Date;
}
// tenant-virtual-care-config.ts
export interface TenantVirtualCareConfig {
tenantId: string;
defaultVideoBackend: VideoBackend;
jitsiServerUrl: string | null;
jitsiJwtAppId: string | null;
jitsiJwtSecretEncrypted: string | null; // KMS-encrypted; never returned in GET
mediasoupWsUrl: string | null;
zoomApiKeyEncrypted: string | null;
webexClientIdEncrypted: string | null;
teamsClientIdEncrypted: string | null;
brandingLogoUrl: string | null;
brandingPrimaryColor: string | null;
roomSlugPrefix: string | null;
recordingEnabled: boolean;
sessionGraceMinutesBefore: number;
sessionGraceMinutesAfter: number;
maxParticipants: number;
fallbackVideoBackend: VideoBackend | null;
updatedAt: Date;
}
3. PostgreSQL Schema
-- virtual_sessions
CREATE TABLE virtual_sessions (
id TEXT PRIMARY KEY, -- vsn_ULID
tenant_id UUID NOT NULL,
node_id UUID NOT NULL,
patient_id UUID NOT NULL,
appointment_id UUID,
encounter_id UUID,
initiator_id UUID NOT NULL,
video_backend TEXT NOT NULL DEFAULT 'jitsi',
room_name TEXT NOT NULL,
room_url TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled'
CHECK (status IN
('scheduled','waiting','active','ended','cancelled','failed')),
scheduled_start TIMESTAMPTZ NOT NULL,
scheduled_end TIMESTAMPTZ NOT NULL,
actual_start TIMESTAMPTZ,
actual_end TIMESTAMPTZ,
recording_enabled BOOLEAN NOT NULL DEFAULT FALSE,
recording_ref TEXT,
failure_reason TEXT,
messaging_thread_id UUID,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_virtual_sessions_tenant_status
ON virtual_sessions (tenant_id, status);
CREATE INDEX ix_virtual_sessions_patient_id
ON virtual_sessions (tenant_id, patient_id);
CREATE INDEX ix_virtual_sessions_scheduled_start
ON virtual_sessions (tenant_id, scheduled_start);
ALTER TABLE virtual_sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY virtual_sessions_tenant_isolation
ON virtual_sessions
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- virtual_session_participants
CREATE TABLE virtual_session_participants (
id TEXT PRIMARY KEY, -- vsp_ULID
session_id TEXT NOT NULL
REFERENCES virtual_sessions(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
role TEXT NOT NULL
CHECK (role IN ('provider','patient','interpreter','observer')),
status TEXT NOT NULL DEFAULT 'invited'
CHECK (status IN
('invited','waiting','admitted','active','disconnected','removed')),
joined_at TIMESTAMPTZ,
left_at TIMESTAMPTZ,
removed_at TIMESTAMPTZ,
removed_reason TEXT
);
CREATE INDEX ix_virtual_session_participants_session
ON virtual_session_participants (session_id);
CREATE INDEX ix_virtual_session_participants_user
ON virtual_session_participants (tenant_id, user_id);
ALTER TABLE virtual_session_participants ENABLE ROW LEVEL SECURITY;
CREATE POLICY virtual_session_participants_tenant_isolation
ON virtual_session_participants
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- async_visits
CREATE TABLE async_visits (
id TEXT PRIMARY KEY, -- avs_ULID
tenant_id UUID NOT NULL,
patient_id UUID NOT NULL,
appointment_id UUID,
provider_id UUID,
chief_complaint TEXT NOT NULL,
attachment_refs TEXT[] NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'submitted',
client_mutation_id TEXT NOT NULL,
submitted_at TIMESTAMPTZ,
responded_at TIMESTAMPTZ,
encounter_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, client_mutation_id)
);
ALTER TABLE async_visits ENABLE ROW LEVEL SECURITY;
CREATE POLICY async_visits_tenant_isolation
ON async_visits
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- tenant_virtual_care_config
CREATE TABLE tenant_virtual_care_config (
tenant_id UUID PRIMARY KEY,
default_video_backend TEXT NOT NULL DEFAULT 'jitsi',
jitsi_server_url TEXT,
jitsi_jwt_app_id TEXT,
jitsi_jwt_secret_encrypted TEXT, -- KMS-encrypted
mediasoup_ws_url TEXT,
zoom_api_key_encrypted TEXT,
webex_client_id_encrypted TEXT,
teams_client_id_encrypted TEXT,
branding_logo_url TEXT,
branding_primary_color TEXT,
room_slug_prefix TEXT,
recording_enabled BOOLEAN NOT NULL DEFAULT FALSE,
session_grace_minutes_before INTEGER NOT NULL DEFAULT 15,
session_grace_minutes_after INTEGER NOT NULL DEFAULT 30,
max_participants INTEGER NOT NULL DEFAULT 8,
fallback_video_backend TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- outbox
CREATE TABLE virtual_care_outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_virtual_care_outbox_unpublished
ON virtual_care_outbox (published, created_at) WHERE published = FALSE;
-- inbox
CREATE TABLE virtual_care_inbox (
event_id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);