- Industry
- Operators · MSPs · CCaaS product teams
- Type
- Multi-tenant IPPBX + CCaaS
- Role
- Solo — architecture, full-stack, ESL, deploy
- Keys
- Super-admin · tenant admin · agent
- Stack
- NestJS · Vue 3 · FreeSWITCH · PostgreSQL
- Proof
- 12 running screens · YouTube
The problem
Operators wanted to sell CCaaS seats — agents, queues, campaigns and supervisor reporting — without SSH into FreeSWITCH for every extension change. A PBX GUI alone does not give tenant isolation, CRM-shaped campaigns or an agent workspace.
Constraints
Delivery stayed inside the client's stack, tenancy, compliance and media-ownership boundaries. Where a choice was forced (suite vs owned plane, BYOC vs CPaaS, local vs cloud models), the architecture section below records the trade-off rather than a marketing rewrite.
Business requirements
Super-admin platform health, per-tenant PBX menus (extensions through campaigns), agent soft dialer with disposition, PostgreSQL as truth, and push-to-FreeSWITCH sync after admin edits.
Engineering challenges
- Three roles — super-admin, tenant admin and agent with JWT guards.
- Config sync — DB → dialplan/SIP without orphan XML on disk.
- Honest observability — UI admits when Prometheus integration is still placeholder.
The solution
NestJS + Vue 3 CCaaS: tenant lifecycle, FreeSWITCH sync screen, agent workspace, campaigns/leads and reporting groups — documented with twelve running screenshots and a walkthrough video.
My role
Solo architecture and full-stack delivery — API, Vue surfaces, ESL/sync path and deploy. Distinct from UnifiedPBX on Asterisk 22 and from AI receptionist platform receptionist.
Technology stack
Production considerations
- Sync jobs after trunk or extension changes — operators must not edit XML by hand.
- Agent state aligned with queue events before outbound campaigns scale.
- Metrics roadmap separated from demo KPI zeros in capture.
Evidence — one door, three keys
Email + password. POST /api/v1/auth/login returns access and refresh tokens. JWT plus RolesGuard decides the rest. Agents are sent to Agent Workspace. Super-admin sees Tenants, Diagnosis and FreeSWITCH Sync. A shared office-PC password is not a product.
Who does what
| Role | Job | Surfaces in this capture |
|---|---|---|
| Super-admin | Tenant lifecycle, box health, push DB → FreeSWITCH | Dashboard, Tenants, Diagnosis, FreeSWITCH Sync, Reports |
| Tenant admin / supervisor | One organisation’s PBX and campaigns | Extensions, Queues, IVR, DID, trunks, routes, Leads, Campaigns |
| Agent | Dial, status, next lead, disposition | Agent Workspace — Soft Dialer, CDR, Contacts, Reports |
This page is not UnifiedPBX on Asterisk 22 (Next.js + AMI/ARI). It is not Multi-tenant AI receptionist (missed-call receptionist). It is the seat-product CCaaS path.
Super-admin — is the platform healthy?
The question this screen answers: “Is the engine up, and which tenant needs a call from us?”
Left rail: Dashboard, Tenants, PBX, Reports, Diagnosis, FreeSWITCH Sync. Tiles: system status, active load, quality snapshot (this UI says metrics come in Phase 3), business pulse, call-flow / CCaaS health, incident feed. Zeros here mean a quiet capture — not a published SLA.
After an admin changes an extension or a trunk, this is the push: full sync (dialplan + SIP), dialplan only, or SIP reload. PostgreSQL is truth. FreeSWITCH executes the XML that this screen writes. Without it, every tenant is a snowflake file on disk.
Reports Overview groups the signals a supervisor can act on: Matrices, Call Flow, Call Statistics, Queue & CCaaS, Agent Reports, IVR Analytics. Copy on the screen: Prometheus tells you what the engine is doing; these groups turn that into something an operator can click.
Honest chrome: “Backend Prometheus integration is not yet complete. Placeholders shown below.” Hero KPIs, time series and operational cards are laid out. We do not pretend Grafana exists. A 404 capture of the Prometheus UI is not shown.
Tenant admin — the PBX is a menu, not XML
One organisation. The left rail is the product: Extensions, Queues, Ring Groups, Conferences, IVR, Voicemail, DID, SIP Trunk, outbound/inbound routes, Leads, Campaigns, Scripts, Voice Prompts, Feature Codes. That list is why operators stop SSH-ing into FreeSWITCH for a new queue.
Extensions: number, display name, status. Add Extension writes the directory that FreeSWITCH Sync pushes. Agents cannot dial until a row here is linked to their user — the Agent Workspace says so in plain language.
Campaigns: type (inbound / outbound / blended), status, script count, assigned agents. Empty in this capture. Add Campaign is the start of the CCaaS loop — not a CSV beside the PBX.
Edit Campaign is its own route. This capture is an empty form — the rail is the evidence: campaigns live next to trunks and IVR, not in a second app.
Leads: search, single add, bulk upload, groups. Name, phone, email. This is the pool the agent’s “next lead” reads. Runtime in this build is agent preview — not a claimed server-side predictive worker.
Agent — who do I call, and can I actually register?
Soft Dialer, CDR, Contacts, Reports. Campaign selector. Status chips: ready, break, lunch, meeting, not_ready. Dialpad. This capture is Offline with the banner: “No extension assigned. Ask your admin to link an extension to your user in Extensions.” That message is the product working. A silent SIP failure is how agents lose a morning.
When an extension is linked, SIP.js registers over WSS (5066). Originate can also go POST /calls/originate. Channel events flow ESL → Nest → Socket.IO so live screens move without a refresh.
Supervisor — live channels
Active Calls: UUID, direction, from, to — backed by ESL show channels / active-call APIs, not yesterday’s CDR. Empty here because nothing was up in the capture. Super-admin rail stays (Tenants, Diagnosis, Sync). An agent does not get that rail.
The contract the UI already uses
Swagger at /api/docs — IPPBX API 1.0, OAS 3.0, Bearer auth. Auth: login, refresh, profile. Tenants: POST /api/v1/tenants is labelled Super Admin only. That lock is the isolation story in one line. The rest of the tag list (extensions, campaigns, FreeSWITCH, metrics) is the same surface the Vue app calls.
Working-system video
The recording is the product walking — not a slide deck. Same stack as these screens.
Watch on YouTube → · Dedicated watch page →
How the system flows
Three roles share one NestJS API and one FreeSWITCH box. PostgreSQL is truth — the admin UI writes objects, FreeSWITCH Sync pushes XML, agents dial through SIP.js.
flowchart LR A[Agent selects campaign] --> B[Fetch next lead] B --> C[SIP.js register WSS] C --> D[Originate via API or softphone] D --> E[FreeSWITCH bridges call] E --> F[Disposition on lead] F --> B
Architecture
The rule: NestJS decides; FreeSWITCH executes.
flowchart TB
subgraph ui [Vue 3 SPA]
SA[Super-admin]
TA[Tenant admin]
AG[Agent workspace]
end
API[NestJS /api/v1 JWT RBAC]
PG[(PostgreSQL tenants PBX campaigns)]
SIO[Socket.IO live events]
FS[FreeSWITCH ESL]
SA --> API
TA --> API
AG --> API
API --> PG
API --> SIO
API --> FS
FS --> SIO
Twenty-four Nest modules behind one prefix. Multi-tenancy inside FreeSWITCH uses directory variable_tenant_id, not a native FreeSWITCH multi-tenant product. Redis/Bull appear in docs — not claimed as wired Nest workers here.
Engineering challenges
- Static XML drifting from the admin UI → DB as truth + directory / dialplan / Sofia sync
- Live channels invisible to the SPA → ESL events → Socket.IO
- Shared FreeSWITCH for many companies →
tenantId+ directory variables + Super-Admin-only tenant create - Desktop softphones only → SIP.js Agent Panel, with a visible “no extension” failure
- Campaign lists in a CSV beside the PBX → campaigns and leads on the same rail as trunks
My role
Solo engineer: architecture, NestJS modules, Vue SPA, schema, ESL services, campaign APIs, Docker / nginx / install scripts, and the documentation pack these screens were captured against.
Architecture NestJS Vue 3 FreeSWITCH ESL SIP.js RBAC
Engineering outcomes
No invented seat-price or ROI. What the running UI actually shows:
- One login that fans out by role
- Super-admin health dashboard and FreeSWITCH Sync
- Tenant-admin PBX catalogue (extensions through feature codes) plus campaigns and leads
- Agent Workspace that refuses to pretend SIP works without an extension
- Active-call table and report groups wired for ESL / Prometheus — with incomplete metrics labelled as such
- OpenAPI with Super-Admin-only tenant create
- A working-system video of the same product
Architectural insight
If FreeSWITCH is the source of truth, every tenant is a snowflake XML file. If the agent cannot see why register failed, you built a toy dialer. The CCaaS product is identity, tenancy and campaigns — SIP is the adapter. Related: multi-tenant IP PBX, AI call center when a model must sit on the same queue events, ZenTech for a two-van shop, not a seat reseller.
FAQ — buyer & architecture questions
Ten common questions about this case study, fit, and engagement.
Building a CCaaS you own?
Share tenants, carriers, who logs in as super-admin vs agent, and what already cannot pace or transfer. The first reply is architecture — not a suite quote.
Discuss Your Project