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.