Full-stack call center MVP: NestJS backend (API + WebSocket), Next.js frontend, Supabase (PostgreSQL + Auth), RingCentral dialer, AssemblyAI real-time STT, and live compliance (rules, violations, alerts).
- Features
- Installation
- Environment
- Running the app
- Database & migrations
- Mic and sound permissions
- Live transcript & compliance & AI assistant
- Live transcript → compliance → AI assistant (detail)
- Call audio & live AI (RingOut, WebRTC, capture)
- AssemblyAI in this project (STT, not RAG)
- Database: calls, transcripts, violations, alert history
- Checking compliance (course / rules)
- Project structure
- Making changes
- Deployment
| Area | Description |
|---|---|
| Auth | Email/password signup & login, Supabase (OTP, magic link, reset), Google OAuth, RingCentral OAuth |
| Orgs & roles | Organizations, members, platform roles (rep / admin / super_admin), permission-based access (RBAC) |
| Rep Portal & dialer | RingCentral Embeddable widget (RingOut), dialpad, call flow (ready → in progress → ended), disposition (outcome, notes) |
| Mic & sound | Centralized permission request/check (see Mic and sound permissions); required for live transcription and call audio |
| Live transcript | Real-time speech-to-text via AssemblyAI streaming STT (server) or browser Speech Recognition (fallback); stored per call |
| Compliance | Org-level rules (Disclosure, Prohibited Language, etc.), real-time evaluation during calls, violation alerts, compliance score |
| AI Call Assistant | Same audio → AssemblyAI streaming STT → transcript → optional RAG/LLM layer (app uses a stub; replace with your embedding + vector DB + LLM) |
| Calls & DB | All calls stored with transcripts, compliance score, violations, and in-call compliance alert history |
| Compliance UI | Rules CRUD, violations list & verification, TPI/DNC, simulate transcript to test rules |
| Training / onboarding | Training modules, onboarding, skill development (dashboard routes) |
| Other | Call center outbound, AI coaching, campaigns, QA, analytics, knowledge base, settings, admin |
- Node.js 20+
- npm (or yarn/pnpm)
- Supabase account
- RingCentral developer app (for dialer)
- AssemblyAI API key (optional; for server-side live streaming transcription)
git clone <repo-url>
cd MVP
# Backend
cd backend && npm install && cd ..
# Frontend
cd frontend && npm install && cd ..- Create a project at supabase.com.
- In Project Settings → Database, copy the connection string (URI). Use the direct connection (port 5432) for migrations.
- Set
DATABASE_URLinbackend/.envto that URI (replace[YOUR-PASSWORD]with your DB password). - Run migrations (see Database & migrations).
cp backend/.env.example backend/.env
# Edit backend/.env: DATABASE_URL, SUPABASE_*, SESSION_SECRET, RINGCENTRAL_*, ASSEMBLYAI_API_KEY (optional), etc.cp frontend/.env.example frontend/.env
# Set NEXT_PUBLIC_API_URL (e.g. http://localhost:3001) and NEXT_PUBLIC_WS_URL (e.g. ws://localhost:3001).
# For RingCentral dialer: NEXT_PUBLIC_RINGCENTRAL_CLIENT_ID, NEXT_PUBLIC_RINGCENTRAL_SERVER (optional).- In Supabase Dashboard → Authentication → URL Configuration, add:
- Site URL and Redirect URLs:
http://localhost:3000,http://localhost:3000/auth/callback,http://localhost:3000/auth/reset-password.
- Site URL and Redirect URLs:
| Variable | Where | Purpose |
|---|---|---|
DATABASE_URL |
Backend | Supabase PostgreSQL (direct, port 5432) |
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_JWT_SECRET |
Backend | Supabase Auth & API |
FRONTEND_URL |
Backend | CORS and redirects (e.g. http://localhost:3000) |
SESSION_SECRET |
Backend | JWT signing |
RINGCENTRAL_* |
Backend | RingCentral OAuth (dialer) |
ASSEMBLYAI_API_KEY |
Backend | Live streaming STT (optional; fallback: browser Speech Recognition) |
NEXT_PUBLIC_API_URL |
Frontend | API base (e.g. http://localhost:3001) |
NEXT_PUBLIC_WS_URL |
Frontend | WebSocket (e.g. ws://localhost:3001) |
NEXT_PUBLIC_RINGCENTRAL_CLIENT_ID, NEXT_PUBLIC_RINGCENTRAL_SERVER |
Frontend | RingCentral Embeddable widget |
# Terminal 1 — Backend (API + WebSocket)
cd backend && npm run start:dev
# Terminal 2 — Frontend
cd frontend && npm run dev- Frontend: http://localhost:3000
- API: http://localhost:3001
- WebSocket:
ws://localhost:3001/ws?token=<JWT> - DB check:
GET http://localhost:3001/db-check - Integrations (AssemblyAI, etc.):
GET http://localhost:3001/integrations/status
Live transcript can use server-side STT (AssemblyAI) or browser Speech Recognition. To use AssemblyAI:
-
Get an API key
Sign up at AssemblyAI, create an API key, and add it tobackend/.env:ASSEMBLYAI_API_KEY=your_key_here
-
Verify the backend sees it
With the backend running (npm run start:devinbackend/), open:http://localhost:3001/integrations/statusYou should see:
{ "assemblyai": { "configured": true, "hint": "Live transcript (STT) and AI assistant will use AssemblyAI streaming when you are on a call." } }If
configuredisfalse, the key is missing or invalid in.env; fix it and restart the backend. -
Test on a call
Go to Rep Portal → Dialer, start a call, allow the microphone, and speak. Transcript should appear in the Live Transcript panel (and Compliance will update). If it still doesn’t:- Confirm the WebSocket is open (no errors in the browser console).
- Confirm the mic is allowed and unmuted (or that the “muted but still capturing” path is working).
- Without AssemblyAI, the app falls back to browser Speech Recognition (Chrome/Edge recommended).
- ORM: Prisma; schema in
backend/prisma/schema.prisma. - Migrations:
backend/prisma/migrations/.
Commands (from backend/):
npx prisma generate # Regenerate client after schema change
npm run db:migrate # Apply migrations (dev)
npm run db:migrate:deploy # Apply in production
npm run db:studio # Open Prisma StudioIf you previously applied manual SQL and now use Prisma, you may need to mark the initial migration as applied:
npx prisma migrate resolve --applied 20250211000000_init (adjust name to match).
The dialer needs microphone (for live transcription and optional “Include call audio”) and sound (to hear the call). The app uses a small permission layer so requests and errors are consistent.
- Frontend:
frontend/src/lib/mediaPermissions.tscheckMicPermission()— current mic state (if Permissions API available).requestMicPermission()— request mic from a user gesture; returns stream or a structured error (denied / not found / error) with a user-facing message.requestSoundPermission()— unlock playback viaAudioContexton user gesture; returns context to keep alive and a message.
- Usage: RingCentral “Connect” triggers a mic request so the browser prompt appears before redirect. On the call page, “Allow mic for live transcription” and “Allow sound for call audio” use the same helpers and show the same error copy (e.g. address bar → Site settings → Microphone / Sound).
- Live transcript
- Server (AssemblyAI): When
ASSEMBLYAI_API_KEYis set, client sends PCM audio over WebSocket; backend streams it to AssemblyAI’s real-time streaming STT API and broadcaststranscriptevents. AssemblyAI does speech-to-text only (no RAG). - Fallback: If the key is not set, the client uses browser Speech Recognition and sends
simulate-transcript; backend runs the same compliance logic and broadcasts transcript + compliance.
- Server (AssemblyAI): When
- Compliance
- On each transcript update (from server STT or simulate), the backend evaluates org compliance rules (disclosure + prohibited language), updates satisfied/violated rules and score, and broadcasts
compliance. Violations are shown in the UI in real time; they are persisted to the DB only when the user saves the call (PATCH/calls/:idwithviolations).
- On each transcript update (from server STT or simulate), the backend evaluates org compliance rules (disclosure + prohibited language), updates satisfied/violated rules and score, and broadcasts
- AI Call Assistant (RAG pipeline uses transcript)
- The same audio is sent to a second AssemblyAI streaming STT session for the “AI Call Assistant” panel. The backend receives transcript text and runs a stub suggestion (keyword-based). AssemblyAI does not perform RAG; for a real voice RAG pipeline you would take this transcript → embedding + vector DB → LLM (e.g. LangChain) and send the result as the suggestion.
See backend/src/ws/ws.gateway.ts (file-level comment) for the full flow.
AssemblyAI provides speech-to-text (live streaming and batch). It does not do RAG, embeddings, or LLM; it only turns audio into text.
| Feature | AssemblyAI | This project |
|---|---|---|
| Live transcription | ✅ Yes (streaming WebSocket API) | Used for live call transcript + compliance input |
| Batch / file transcription | ✅ Yes (not wired in this repo) | Could be added for recordings |
| RAG / LLM | ❌ No | Transcript is the input to your RAG/LLM pipeline |
Architecture (RingCentral + NestJS + voice AI):
Call audio (mic / RingCentral / WebRTC)
↓
AssemblyAI streaming STT ←── ASSEMBLYAI_API_KEY
↓
Live transcript text
↓
├──→ Live Transcript UI + Compliance rules
└──→ (Optional) RAG pipeline: transcript → embedding + vector DB → LLM → suggestion
In this codebase:
transcription/— one AssemblyAI streaming session per call for live transcript (and compliance).assemblyai/— a second streaming session per call for the AI Call Assistant panel; transcript is then passed to a stub (keyword-based). Replace that stub with your own RAG/LLM (e.g. LangChain + vector store) that consumes the transcript.
- Calls —
calls: one row per call (user, org, RingCentral id, status, duration, compliance score, outcome, notes, recording URL). - Live transcriptions —
transcripts: one row per segment (call, speakerrep/customer, text, timestamp). Saved when the call ends: client sends full transcript inPOST /calls/:id/end(replace), or if the client sends no transcript the server keeps what it persisted from the live STT buffer instopTranscription. - Compliance —
compliance_scoreon the call is set inPOST /calls/:id/end. Violations (violations) are created when the rep clicks Save Call (PATCH) with violation data. In-call alerts (compliance_alerts): one row per real-time violation alert during the call for auditing. - Assistant logs —
call_assistant_logs: one row per transcript turn + suggestion (callId, transcript, suggestion). Transcript comes from AssemblyAI streaming STT; suggestion is from the app's stub (replace with your RAG/LLM that consumes the transcript).
- Compliance dashboard:
/dashboard/compliance— overview, links to rules and violations. - Rules:
/dashboard/compliance/rules— create/edit rules (category, severity, detection pattern), simulate transcript to test rules before going live. - Violations:
/dashboard/compliance/violations— list violations, verify/filter. - TPI/DNC:
/dashboard/compliance/tpi-dnc— TPI and DNC list management. - During a call: Real-time compliance score and checklist (satisfied/violated) and violation alerts; all driven by the same rules and stored as above.
MVP/
├── backend/ # NestJS API + WebSocket
│ ├── src/
│ │ ├── admin/ # Platform admin
│ │ ├── assemblyai/ # AssemblyAI streaming STT for AI assistant; transcript fed to stub (replace with RAG/LLM)
│ │ ├── auth/ # JWT, Google, Supabase, RingCentral
│ │ ├── calls/ # Call CRUD, end call, transcripts, recording
│ │ ├── compliance/ # Rules, violations, evaluation
│ │ ├── config/ # Env, Supabase, RingCentral
│ │ ├── transcription/ # Live STT (AssemblyAI streaming), persist transcript
│ │ ├── ws/ # WebSocket gateway (dialer, transcript, compliance)
│ │ └── ...
│ └── prisma/ # Schema + migrations
├── frontend/ # Next.js App Router
│ └── src/
│ ├── app/dashboard/ # Dashboard, rep-portal/dialer, compliance, ...
│ ├── components/ # UI, sidebar, guards
│ ├── contexts/ # User, WebSocket
│ ├── hooks/ # useMicrophoneStream
│ └── lib/ # api, mediaPermissions, roles, websocket
└── README.md # This file
- Backend README:
backend/README.md— API table, WebSocket auth, RingCentral redirect notes. - Project guide:
PROJECT_GUIDE.md— project structure, features list, running locally, and deployment to Vercel (frontend) and Render (backend). - Frontend: Next.js default; no separate feature README.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Frontend (dialer) │
│ • Mic + optional tab audio → PCM → WebSocket "audio" (speaker: rep/customer)│
│ • Or: browser Speech Recognition → WebSocket "simulate-transcript" │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Backend WebSocket (ws.gateway.ts) │
│ • join-call: starts transcription (rep + customer) + AssemblyAI RAG │
│ • audio → TranscriptionService + AssemblyAIStreamingService │
│ • simulate-transcript → compliance eval + broadcast transcript │
└─────────────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ Live transcript │ │ Compliance │ │ AI assistant (RAG) │
│ • AssemblyAI v3 │ │ • Rep + customer │ │ • Same audio → │
│ • Two sessions │ │ text → rules │ │ AssemblyAI stream │
│ (rep, customer) │ │ • Satisfied/violated│ │ • Final turns → │
│ • Broadcast │ │ • Score 0–100% │ │ suggestion stub │
│ type: "transcript" │ │ • Broadcast │ │ • Broadcast │
│ │ │ type: "compliance" │ │ assistant-transcript│
│ │ │ • Alerts to DB │ │ assistant-suggestion│
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
Live transcript: AssemblyAI path (when USE_ASSEMBLYAI_LIVE_TRANSCRIPTION): frontend sends PCM with speaker: "rep" or "customer"; backend streams to AssemblyAI v3 (one session per speaker); gateway broadcasts { type: "transcript", speaker, text, timestamp, isFinal }. Browser STT path: frontend sends "simulate-transcript"; gateway broadcasts same shape and runs compliance.
Compliance: ComplianceRulesService.evaluateTranscript(orgId, repTranscript, customerTranscript); violations broadcast in real time; persisted on Save Call (PATCH /calls/:id). In-call alerts → compliance_alerts.
AI assistant: Same PCM → AssemblyAIStreamingService; on final turn, stub ragSuggestionFromTranscript; broadcast assistant-transcript / assistant-suggestion; persist to callAssistantLog.
WebSocket message types: transcript, compliance, assistant-transcript, assistant-suggestion (server→client); audio, simulate-transcript (client→server).
Files: Pipeline — ws/ws.gateway.ts, rep-portal/dialer/DemoDialerContent.tsx. STT — transcription/transcription.service.ts, hooks/useMicrophoneStream.ts. Compliance — compliance/compliance.service.ts, compliance-rules.service.ts. AI assistant — assemblyai/assemblyai-streaming.service.ts.
Why RingOut alone is not enough: With RingOut, your app never receives the audio → no live transcription or RAG. You must capture both sides and send to AssemblyAI.
How this app captures audio:
| Source | How we get it | Sent as |
|---|---|---|
| Rep speech | Browser microphone (getUserMedia) |
event: "audio", speaker: "rep" |
| Customer speech | Optional: “Include call audio (share tab)” — share RingCentral tab with tab audio (getDisplayMedia). Must check ☑ Share tab audio. |
event: "audio", speaker: "customer" |
Safari: Tab audio cannot be captured via getDisplayMedia({ audio: true }). Rep transcript works; customer transcript via tab share does not work in Safari. For rep + customer in Safari, use RingCentral Web Phone (WebRTC) or Call Media Streaming to get the remote track.
Backend pipeline: Mic PCM → WebSocket audio (rep) → TranscriptionService → AssemblyAI v3. Tab PCM (customer) → WebSocket audio (customer) → second session → same compliance/RAG path.
Summary:
| Setup | Rep transcript | Customer transcript | RAG assistant |
|---|---|---|---|
| RingOut, mic only | ✅ | ❌ | ✅ (rep only) |
| RingOut + “Include call audio” (tab share) | ✅ | ✅ (Chrome/Edge) | ✅ (both) |
| RingCentral WebRTC / Media Streaming | ✅ | ✅ (clean) | ✅ (both) |
-
Backend (NestJS)
- Code:
backend/src/. - After changing Prisma schema:
cd backend && npx prisma generate. - New migrations:
npx prisma migrate dev --name your_migration_name. - Run:
npm run start:dev(hot reload).
- Code:
-
Frontend (Next.js)
- Code:
frontend/src/. - Run:
npm run dev. - Env: copy
frontend/.env.exampletofrontend/.envand setNEXT_PUBLIC_API_URL,NEXT_PUBLIC_WS_URL, and RingCentral/other keys as needed.
- Code:
-
Environment
- Backend:
backend/.env(see Environment). - Never commit
.env; use.env.exampleas a template and document any new variables in the README.
- Backend:
-
Testing
- Backend:
cd backend && npm run test(if test script exists). - Frontend:
cd frontend && npm run buildto catch build errors; add E2E/unit as needed.
- Backend:
-
Today’s goals / stats
- Backend:
GET /calls/stats/todayreturns{ calls, connects, qualified, booked }for the current user (or org for admins). - Frontend: dashboard uses
api.getTodayGoals()and refetches on focus.
- Backend:
- Run
npx prisma generateif schema changed. - Run
npm run buildin both backend and frontend. - Ensure no secrets in code; use env vars only.
-
Build
cd backend && npm ci && npm run build
-
Database
- Set
DATABASE_URLto your production PostgreSQL (e.g. Supabase direct connection). - Run migrations:
npm run db:migrate:deploy(ornpx prisma migrate deploy). - Ensure Supabase (or your DB) allows connections from the deployment host.
- Set
-
Environment
- Configure all required env vars in the host (see Environment):
DATABASE_URL,SUPABASE_*,SESSION_SECRET,FRONTEND_URL,RINGCENTRAL_*,ASSEMBLYAI_API_KEY(optional),API_BASE_URLfor webhooks.
- Configure all required env vars in the host (see Environment):
-
Run
node dist/main.js
Or use a process manager (e.g. PM2, systemd) and reverse proxy (e.g. Nginx) for HTTPS.
- RingCentral webhooks: Set your RingCentral app’s webhook URLs to
https://your-api-domain/calls/webhook/ringcentraland.../calls/webhook/ringcentral/ringoutso call events and RingOut status are received.
- RingCentral webhooks: Set your RingCentral app’s webhook URLs to
-
Health
- Expose a health/readiness endpoint if needed;
GET /db-checkandGET /integrations/statuscan be used for basic checks.
- Expose a health/readiness endpoint if needed;
-
Build
cd frontend && npm ci && npm run build
-
Environment
- Set build-time env vars (e.g. in CI or host):
NEXT_PUBLIC_API_URL,NEXT_PUBLIC_WS_URL(your production API and WebSocket URLs),
NEXT_PUBLIC_RINGCENTRAL_CLIENT_ID,NEXT_PUBLIC_RINGCENTRAL_SERVER(if using dialer). - These are embedded at build time; redeploy after changing them.
- Set build-time env vars (e.g. in CI or host):
-
Run
npm run start
Or deploy the
.nextoutput to Vercel, Node host, or static export per Next.js docs.- Ensure CORS on the backend allows your frontend origin (
FRONTEND_URL).
- Ensure CORS on the backend allows your frontend origin (
-
Auth
- In Supabase Dashboard, add your production site URL and redirect URLs (e.g.
https://your-app.com,https://your-app.com/auth/callback,https://your-app.com/auth/reset-password). - RingCentral: set the production redirect URI in the RingCentral app to match your backend’s
RINGCENTRAL_CALLBACK_URL.
- In Supabase Dashboard, add your production site URL and redirect URLs (e.g.
- Backend:
DATABASE_URL, migrations, all env vars, webhook URLs. - Frontend:
NEXT_PUBLIC_*set for production, build succeeds. - Supabase Auth redirect URLs and RingCentral redirect URI updated for production.
- HTTPS and correct CORS/security headers in production.
- Supabase project created;
DATABASE_URLand Supabase keys inbackend/.env. - Backend migrations applied:
cd backend && npm run db:migrate. - Supabase Auth redirect URLs include your frontend URL and
/auth/callback,/auth/reset-password. - RingCentral app created; redirect URI matches
RINGCENTRAL_CALLBACK_URL; backend and (if used) frontend RingCentral env set. - Frontend
.env:NEXT_PUBLIC_API_URL,NEXT_PUBLIC_WS_URL. - Start backend and frontend; open Dashboard → Dialer; connect RingCentral; start a call and confirm mic/sound prompts and (if configured) live transcript and compliance.