CallMineAI Docs
Home
  • Introduction
  • Architecture
  • Installation
  • Configuration
  • Deployment
  • Customer panel
  • Admin panel
  • Agents & Voices
  • Campaigns & Calls
  • Billing & Credits
  • Telephony (Twilio)
  • Voice & AI
  • SIP trunks
  • Messaging
  • REST API
Home
  • Introduction
  • Architecture
  • Installation
  • Configuration
  • Deployment
  • Customer panel
  • Admin panel
  • Agents & Voices
  • Campaigns & Calls
  • Billing & Credits
  • Telephony (Twilio)
  • Voice & AI
  • SIP trunks
  • Messaging
  • REST API
  • Getting started

    • Introduction
    • Architecture
    • Installation
    • Configuration
    • Deployment
  • Using CallMineAI

    • Customer Guide
    • Agents & Voices
    • Campaigns & Calls
    • Billing & Credits
  • Administration

    • Admin Guide
    • Roles & permissions
    • Localization
  • Integrations

    • Telephony (Twilio)
    • Voice & AI
    • SIP trunks
    • Messaging
    • REST API
  • Help

    • FAQ & troubleshooting

Architecture

CallMineAI is a multi-tenant AI voice-calling and lead-intelligence SaaS. This page explains how the pieces fit together: the tech stack, the two authentication panels, the tenancy model, the end-to-end calling pipeline, and the core data model.

Tech stack

LayerTechnology
BackendNext.js 15 App Router (Node 20+), React Server Components + route handlers
FrontendReact 19, server-driven pages (props are rendered by the server component, no separate SPA/API for the UI)
StylingTailwindCSS (Space Grotesk display · Inter body · JetBrains Mono metrics)
BuildNext.js / Turbopack
DatabaseMySQL, accessed through Prisma 6
SessionsSigned JWT cookies (jose) — no session store to run
Cache / locksThe cache table (rate limits, cron overlap locks)
Voice stackTwilio (telephony) + ElevenLabs (voice synthesis) + OpenAI (gpt-4o-mini agent brain)

Only MySQL is required

There is no Redis, no queue broker and no session store to install — cache entries, rate limits and scheduler locks all live in the cache table. See Installation.

Two panels, two guards

There are two completely separate authentication contexts. They never share sessions, users, or roles.

PanelSession cookieModelURL prefixLogin
Customer workspacecm_sessionUser (role = client)/app/*/login
Super Admincm_adminAdminUser/admin/*/admin/login

Both cookies are signed JWTs (src/lib/auth/), issued and verified independently — one never grants access to the other panel. src/middleware.js runs the edge guard for /admin and /app.

  • The customer workspace is where organizations build agents, launch campaigns and place calls. Pages live under src/app/app/**/page.jsx; every one calls requireClientPage(), which enforces auth, the client role, tenant scoping and demo mode.
  • The Super Admin panel operates the whole platform (plans, customers, credits, engines, locales, currencies). Pages live under src/app/admin/** and check permissions through the admin guards in src/lib/guards-admin.js. See the Admin panel guide.

Writes are separate route handlers under src/app/api/**: a page renders at GET /app/agents, and its mutations run at POST /api/app/agents. Each handler opens with gateClient() / gateAdmin(), the equivalent of the middleware stack on the page.

Role and permission details for both panels are in Roles & permissions.

Multi-tenancy model

Each customer organization is one row in the clients table. Every calling artifact — agents, campaigns, contacts, calls, phone numbers, and so on — carries a client_id foreign key. Scoping is explicit: resolveClientId(user) (src/lib/guards.js) resolves the active tenant once per request — honouring admin impersonation — and every query filters on that client_id. Writes call assertOwned(record, clientId) before touching a row, which throws a 403 on a foreign record.

Tenant isolation

Prisma has no global scopes, so nothing filters for you. Any new query over customer data must pass client_id in its where, and any write must pass the loaded record through assertOwned(). Skipping either leaks data across organizations.

The calling pipeline

Outbound calling is an asynchronous, queue-table-driven pipeline. Launching a campaign fans work out into per-contact rows in call_queue_jobs; a scheduled dispatcher feeds them to the dialer at a rate the ElevenLabs key pool can sustain; each placed call runs a speech loop driven by the agent's LLM; results are then charged, analyzed and pushed to the customer's webhooks.

1. Campaign launch
   └─ POST /api/app/campaigns/{id}/launch fans out one call_queue_jobs row
      (status=pending) per contact

2. POST /api/cron/dispatch  (every minute, TTL lock in the `cache` table)
   └─ dispatchDue() computes free slots = pool.capacity − pool.currentLoad
      (capped by CALL_DIALER_CONCURRENCY), claims that many due rows via
      claimDue(), and runs placeCall() for each

3. placeCall()   — src/lib/services/dialer.js
   ├─ acquires an ElevenLabs key from the pool (requeues the row if none free)
   ├─ verifies the customer has credits
   ├─ creates a Call row (status=ringing)
   └─ the telephony driver (Twilio/Plivo) dials; the webhook URL points back
      at /webhooks/{provider}/{kind}?c={callId}

4. Live call — TwiML <Gather input="speech"> loop
   ├─ the webhook route handler streams caller speech in
   ├─ agent-conversation.js builds the system prompt (agent + tone + goal +
   │  knowledge base + contact vars) and gets the next reply from the LLM
   │  (ai.js → OpenAI), spoken back with <Say> or ElevenLabs audio
   └─ loop continues until [END_CALL] or CALL_MAX_TURNS is reached

5. processCallResult()  (fired by the carrier status webhook, idempotent)
   ├─ maps carrier status → Call status, sets duration/ended_at
   ├─ releases the ElevenLabs key back to the pool
   ├─ charges credits for connected calls  (ceil(seconds / 60) ≈ 1 credit/min)
   ├─ runs analyzeCall()
   └─ refreshCampaign() recomputes stats; auto-completes when the queue drains

6. analyzeCall()
   ├─ the LLM produces {summary, sentiment, qualification}
   │  (Whisper transcribe fallback if a recording exists; heuristic fallback
   │  with no LLM)
   └─ fires outbound webhooks: call.completed and lead.qualified (hot/warm)

The scheduled tasks are required

Nothing above runs unless something POSTs /api/cron/dispatch every minute. Run the bundled worker:

npm run worker

or point a crontab / hosted scheduler at the same endpoint with the X-Cron-Secret header. Overlap is prevented by a TTL lock row in the cache table, so several app instances are safe.

Real calls additionally need valid Twilio/ElevenLabs/OpenAI credentials and a public HTTPS URL so the carrier can reach the webhooks. See Installation, Deployment and Billing & Credits.

Data model (key tables)

The schema is centered on the clients tenant table; almost everything below is scoped by client_id.

DomainTables
Tenancy & usersclients, users
Plans & billingplans, subscriptions, credit_ledger, credit_packages
Agents & voiceagents, voices, elevenlabs_keys
Telephonyphone_numbers, sip_trunks
Campaigns & contactscampaigns, contacts
Calling enginecalls, call_queue_jobs
Compliancekyc_documents
Messaging add-onmessaging settings, message/WhatsApp templates, WhatsApp accounts, conversations, messages
Developer API add-onapi_keys (+ API audit logs)
Team RBAC add-onclient_roles, client_permissions (+ role/permission pivots)

credit_ledger is the single source of truth for a customer's balance — the latest row's balance_after is the current balance, and 1 credit is roughly one call minute. The four add-ons (Messaging, REST API, Team RBAC, SIP Trunk) are built-in modules that are plan-gated: each adds its own columns to plans and is toggled per customer by their plan's feature flags.

Last Updated: 8/15/26, 10:14 AM
Prev
Introduction
Next
Installation