Skip to content

Database Schema

Complete reference for every MongoDB collection, field, index, and relationship in Voicex.

All interfaces are defined in backend/src/db/schema.ts. Indexes are created in backend/src/db/client.ts.


Overview

Voicex uses MongoDB as its primary database. There are 9 collections:

CollectionPurposeKey relationships
plansSubscription tiers with model access and limitsReferenced by organizations.planId
organizationsTenant accountsReferences plans, owns users, agents, providers, calls
usersLogin accountsBelongs to an organization
providersLLM/TTS/STT provider configs (global + client)Referenced by agents via FK
provider_registryRead-only catalog of supported provider typesNot referenced by other collections
agentsAI voice agent configurationsReferences providers (3 FKs), belongs to organization
api_keysAPI keys for external integrationsBelongs to organization
callsVoice call records with transcriptsBelongs to organization + agent
daily_usageAggregated daily usage metricsBelongs to organization

Entity Relationship Diagram


Collection Details

plans

Defines subscription tiers. Each plan controls which models are accessible, how many agents can be created, and whether custom providers are allowed.

typescript
interface Plan {
  _id?: ObjectId;
  slug: string;             // "free" | "starter" | "pro" | "enterprise"
  name: string;             // "Free" | "Starter" | "Pro" | "Enterprise"
  description: string;
  custom: boolean;          // true = custom plan for specific client
  public: boolean;          // true = shown on pricing page
  pricing: {
    monthly: number;        // e.g. 0, 29, 99, 499
    annual: number;         // e.g. 0, 290, 990, 4990
    maxDiscountPercent: number;
  };
  limits: {
    maxAgents: number;
    maxConcurrentCalls: number;
    maxCallDurationSec: number;
    maxMonthlyMinutes: number;
  };
  models: {
    llm: string[];          // e.g. ["ollama/llama3.2:3b", "groq/llama-3.3-70b-versatile"]
    tts: string[];          // e.g. ["edge/en-US-AriaNeural"]
    stt: string[];          // e.g. ["deepgram/nova-2"]
  };
  features: {
    customProviders: boolean;     // can client add own provider keys?
    maxCustomProviders: number;   // how many?
  };
  createdAt: Date;
  updatedAt: Date;
}

Model string format: providerKey/modelId (e.g., groq/llama-3.3-70b-versatile, elevenlabs/21m00Tcm4TlvDq8ikWAM).

Indexes:

FieldsTypePurpose
slugUniqueLookup by slug
public, customCompoundList public non-custom plans for pricing page

Standard plans:

PlanAgentsConcurrentDurationMonthly MinCustom Providers
free125 min60No
starter51030 min1,0001
pro255060 min10,0005
enterprise999500120 min100,000100

organizations

A tenant account. Every user, agent, provider, and call belongs to exactly one organization.

typescript
interface Organization {
  _id?: ObjectId;
  name: string;
  planId: ObjectId;        // FK → plans._id
  status: 'pending' | 'active';
  ownerEmail: string;
  createdAt: Date;
  updatedAt: Date;
}

Lifecycle:

  1. User signs up → org created with status: 'pending'
  2. Admin activates → status: 'active'
  3. User can now sign in and access dashboard
  4. If set back to pending, all API access is blocked

Indexes:

FieldsPurpose
nameSearch by name
planIdFind orgs on a specific plan
statusFilter active/pending

users

Login accounts. Each user belongs to one organization.

typescript
interface User {
  _id?: ObjectId;
  orgId: ObjectId;           // FK → organizations._id
  email: string;             // unique across all orgs
  passwordHash: string;      // scrypt (salt:hash format)
  name: string;
  role: 'admin' | 'member';
  createdAt: Date;
}

Password hashing: Uses Node.js crypto.scryptSync with 16-byte random salt. Format: salt_hex:hash_hex. Timing-safe comparison via crypto.timingSafeEqual.

Indexes:

FieldsTypePurpose
emailUniqueLogin lookup, prevent duplicates
orgIdStandardList users in org

providers

Unified collection for both global (platform-managed) and client-owned providers. This is the core of the relational provider architecture.

typescript
interface ProviderModel {
  modelId: string;           // e.g. "llama-3.3-70b-versatile"
  label: string;             // e.g. "Llama 3.3 70B"
  description?: string;
}

interface Provider {
  _id?: ObjectId;
  orgId: ObjectId | null;    // null = global (platform-owned)
  category: 'llm' | 'tts' | 'stt';
  providerKey: string;       // "groq", "openai", "ollama", "elevenlabs", "edge", "deepgram"
  name: string;              // Display name, e.g. "Groq Cloud"
  credentials: string;       // AES-256-GCM encrypted JSON string
  models: ProviderModel[];   // Models available through this provider
  settings: Record<string, unknown>;  // e.g. { ollamaBaseUrl: "..." }
  active: boolean;
  createdAt: Date;
  updatedAt: Date;
}

Two types of providers:

TypeorgId valueCreated byExample
Globalnullseed-global-providers.tsPlatform's Groq account
Client<ObjectId>Client via dashboardClient's own OpenAI API key

Credential encryption:

  • Algorithm: AES-256-GCM
  • Key: ENCRYPTION_KEY env var (64-char hex = 32 bytes)
  • Stored format: iv_hex:authTag_hex:ciphertext_hex
  • Credentials are a JSON object like {"apiKey": "gsk_..."} encrypted at rest
  • Decrypted only at runtime when starting a voice session

Indexes:

FieldsTypePurpose
orgId, categoryCompoundList providers for an org by type
orgId, category, providerKey, nameUniquePrevent duplicate provider names per org
orgId, activeCompoundFilter active providers

provider_registry

Read-only catalog of all supported provider types and their available models. Used by the frontend to render the "Add Provider" form.

typescript
interface ProviderRegistryEntry {
  _id?: ObjectId;
  category: 'llm' | 'tts' | 'stt';
  providerKey: string;
  displayName: string;
  requiresKey: boolean;
  models: { modelId: string; label: string; description?: string }[];
  settings: { key: string; label: string; type: string; required: boolean }[];
  createdAt: Date;
}

Indexes:

FieldsTypePurpose
category, providerKeyUniquePrevent duplicates

agents

AI voice agent configurations. Agents reference providers via foreign keys — they do NOT embed provider configs.

typescript
interface AgentPersona {
  systemPrompt: string;
  greeting: string;
  personality: string;
  language: string;
  guardrails: string;
}

interface AgentLLMConfig {
  temperature: number;    // 0.0 - 2.0, default 0.4
  maxTokens: number;      // default 200
}

interface AgentTTSConfig {
  speed: number;          // default 1.0
}

interface AgentThresholds {
  silenceTimeoutMs: number;            // default 700
  maxCallDurationSec: number;          // default 1800 (30 min)
  interruptionSensitivity: 'low' | 'medium' | 'high';  // default 'medium'
  endpointingMs: number;               // default 200
}

interface Agent {
  _id?: ObjectId;
  orgId: ObjectId;                     // FK → organizations._id
  name: string;
  persona: AgentPersona;

  // LLM configuration (relational)
  llmProviderId: ObjectId;             // FK → providers._id
  llmModelId: string;                  // e.g. "llama-3.3-70b-versatile"
  llmConfig: AgentLLMConfig;

  // TTS configuration (relational)
  ttsProviderId: ObjectId;             // FK → providers._id
  ttsModelId: string;                  // e.g. "21m00Tcm4TlvDq8ikWAM"
  ttsConfig: AgentTTSConfig;

  // STT configuration (relational)
  sttProviderId: ObjectId;             // FK → providers._id

  thresholds: AgentThresholds;
  active: boolean;
  createdAt: Date;
  updatedAt: Date;
}

Agent status (computed server-side, not stored):

typescript
type AgentStatus = 'active' | 'inactive' | 'paused_provider' | 'paused_plan';

interface AgentWithStatus extends Agent {
  status: AgentStatus;
  pauseReason?: string;
}

Defaults:

typescript
const DEFAULT_AGENT_PERSONA = {
  systemPrompt: "You are a helpful voice assistant...",
  greeting: "Hello! How can I help you today?",
  personality: "friendly and professional",
  language: "en-US",
  guardrails: "Keep responses concise (1-3 sentences)..."
};

const DEFAULT_LLM_CONFIG = { temperature: 0.4, maxTokens: 200 };
const DEFAULT_TTS_CONFIG = { speed: 1.0 };
const DEFAULT_AGENT_THRESHOLDS = {
  silenceTimeoutMs: 700,
  maxCallDurationSec: 1800,
  interruptionSensitivity: 'medium',
  endpointingMs: 200
};

Indexes:

FieldsPurpose
orgId, activeList active agents for org
orgId, createdAtSort agents by creation
llmProviderIdFind agents using a specific LLM provider (delete guard)
ttsProviderIdFind agents using a specific TTS provider (delete guard)
sttProviderIdFind agents using a specific STT provider (delete guard)

api_keys

API keys for external integrations (client's app calling the voice WebSocket).

typescript
interface ApiKey {
  _id?: ObjectId;
  orgId: ObjectId;
  keyHash: string;           // SHA-256 hash of full key
  keyPrefix: string;         // "vx_" + first 10 hex chars (for display)
  name: string;
  scopes: string[];          // e.g. ["voice", "dashboard"]
  lastUsedAt: Date | null;
  createdAt: Date;
  revokedAt: Date | null;    // null = active, Date = revoked
}

Key format: vx_ followed by 48 random hex characters.

Security: Only the SHA-256 hash is stored. The raw key is returned exactly once at creation time. Resolution works by hashing the incoming key and matching against keyHash.

Indexes:

FieldsTypePurpose
orgIdStandardList keys for org
keyHashUniqueResolve incoming API key
keyPrefixStandardDisplay/identify keys

calls

Records of voice sessions with full transcripts and metrics.

typescript
interface Call {
  _id?: ObjectId;
  orgId: ObjectId;
  agentId: ObjectId;
  sessionId: string;          // UUID, unique
  channel: 'web' | 'phone';
  status: 'active' | 'completed' | 'failed';
  startedAt: Date;
  endedAt?: Date;
  durationSec?: number;
  metrics: {
    ttfbMs: number;           // Time to first byte (first audio)
    avgLatencyMs: number;
    totalTokens: number;      // LLM tokens consumed
    ttsChars: number;         // TTS characters generated
    interruptions: number;    // How many times user interrupted
    turnCount: number;        // Number of conversation turns
  };
  summary?: string;           // AI-generated post-call summary
  sentiment?: 'positive' | 'neutral' | 'negative';
  transcript: TranscriptMessage[];
  metadata?: Record<string, unknown>;
  createdAt: Date;
}

interface TranscriptMessage {
  role: 'user' | 'assistant';
  text: string;
  timestamp: number;
}

Indexes:

FieldsPurpose
orgId, createdAtList calls for org (sorted)
orgId, agentId, createdAtList calls for specific agent
sessionId (unique)Look up by session
statusFilter active/completed
orgId, statusActive calls for org

daily_usage

Aggregated daily metrics per organization. Updated atomically when calls end.

typescript
interface DailyUsage {
  _id?: ObjectId;
  orgId: ObjectId;
  date: string;              // "YYYY-MM-DD"
  calls: number;
  minutes: number;
  ttsChars: number;
  llmTokens: number;
  sttSeconds: number;
}

Indexes:

FieldsTypePurpose
orgId, dateUniqueOne record per org per day
orgId, date (desc)StandardQuery recent usage

Redis Data

Redis is used as an optional cache/session layer. If REDIS_URL is not set, the system falls back to in-memory Maps.

Key patternTTLPurpose
plan:{planId}10 minCached plan document (avoids DB hits on every request)
rl:{clientId}60 secRate limit counter (30 connections/min)
voice:history:{sessionKey}24 hoursConversation message history (max 20 messages)

Plan Cache Strategy

Request → L1 Cache (in-memory Map, 1 min TTL)
  ├── HIT → return cached plan
  └── MISS → L2 Cache (Redis, 10 min TTL)
              ├── HIT → populate L1, return
              └── MISS → MongoDB query
                          ├── populate L1 + L2, return

The cached plan includes pre-computed modelSets (JavaScript Set objects) for O(1) model access checks:

typescript
interface CachedPlan extends Plan {
  modelSets: {
    llm: Set<string>;   // e.g. Set(["ollama/llama3.2:3b", "groq/llama-3.3-70b-versatile"])
    tts: Set<string>;
    stt: Set<string>;
  };
}

Database Connection

Configured in backend/src/db/client.ts.

SettingValue
Max pool size100 (configurable via MONGODB_MAX_POOL_SIZE)
Min pool size10
Idle timeout60 seconds
Server selection timeout5 seconds
Connect timeout10 seconds

All indexes are created automatically on startup via initDb().

Built with Deepgram, Groq, and ElevenLabs.