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
| Layer | Technology |
|---|---|
| Backend | Next.js 15 App Router (Node 20+), React Server Components + route handlers |
| Frontend | React 19, server-driven pages (props are rendered by the server component, no separate SPA/API for the UI) |
| Styling | TailwindCSS (Space Grotesk display · Inter body · JetBrains Mono metrics) |
| Build | Next.js / Turbopack |
| Database | MySQL, accessed through Prisma 6 |
| Sessions | Signed JWT cookies (jose) — no session store to run |
| Cache / locks | The cache table (rate limits, cron overlap locks) |
| Voice stack | Twilio (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.
| Panel | Session cookie | Model | URL prefix | Login |
|---|---|---|---|---|
| Customer workspace | cm_session | User (role = client) | /app/* | /login |
| Super Admin | cm_admin | AdminUser | /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 callsrequireClientPage(), which enforces auth, theclientrole, 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 insrc/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.
| Domain | Tables |
|---|---|
| Tenancy & users | clients, users |
| Plans & billing | plans, subscriptions, credit_ledger, credit_packages |
| Agents & voice | agents, voices, elevenlabs_keys |
| Telephony | phone_numbers, sip_trunks |
| Campaigns & contacts | campaigns, contacts |
| Calling engine | calls, call_queue_jobs |
| Compliance | kyc_documents |
| Messaging add-on | messaging settings, message/WhatsApp templates, WhatsApp accounts, conversations, messages |
| Developer API add-on | api_keys (+ API audit logs) |
| Team RBAC add-on | client_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.