A contact-center operations platform
A custom platform that runs an entire call-center operation from one place — automatically capturing every call from cloud telephony, turning it into a trackable record, and rolling calls, tickets, campaigns and follow-ups into real-time dashboards and reporting.
- Discipline
- Design & engineering
- Sector
- Contact center · Support operations
- Type
- Custom platform
- Core stack
- Next.js · NestJS · PostgreSQL

The challenge
The operation ran inbound and outbound phone campaigns — onboarding and collections — across dozens of client sites on behalf of their owners. Calls, SMS, support tickets, manual notes and follow-ups lived in different places. Phone activity was logged by hand, there was no single record of a customer, and no live view of what the floor was doing right now.
The solution
We designed and built one system of record for the whole phone operation. Cloud telephony feeds calls in automatically; each interaction becomes a record an agent can classify and follow up; and everything the operation touches — customers, campaigns, sites, tickets — rolls up into real-time dashboards and per-site reporting the team can export on demand.
How it works
From a ringing phone to a report
A call happens
Inbound or outbound, every call is placed through cloud telephony — agents dial and answer from an embedded softphone right inside the platform.
The platform captures it automatically
Telephony posts each event to a verified webhook. The system turns it into a call record, matches or creates the customer, and provisions the agent — with no manual data entry.
The agent works it in one view
In the Contact Center, calls, tickets and manual records share a single list. The agent sets a disposition, a campaign outcome, notes and a follow-up.
Everyone sees it live
Changes push to every screen over WebSockets — live-call indicators, ticket assignments, due callbacks — with no page refresh and no polling.
Nothing falls through
Background jobs and schedulers watch for overdue callbacks, follow-ups and scheduled calls, and notify the right agent the moment something comes due.
It all rolls up
Calls, tickets and outcomes consolidate into per-site and per-campaign dashboards — and into PDF and Excel exports generated on demand.

Architecture
Two services, one front door
The browser only ever talks to a Next.js backend-for-frontend (BFF), which holds the session and forwards every request, server-to-server, to a NestJS API. That API owns the data in PostgreSQL, with Redis for queues and rate limiting and object storage for call recordings and attachments. One proxy sits in front of the whole backend — the browser never holds a token, and every call passes through a single, hardened path.
Real-time & integrations
Built to stay live
Real-time over WebSockets
A Socket.IO channel pushes live-call changes, ticket assignments and due callbacks to the exact agent who needs them. The client collapses bursts of events into a single debounced refresh, so the UI stays current without polling the server.
Verified telephony webhooks
Every telephony event arrives on a webhook authenticated with a constant-time token check. An idempotent ingestion engine absorbs the messiness of real phone traffic — duplicate events, transfers, voicemail vs. missed, bridge legs — and normalizes numbers so a customer is never duplicated.
Background jobs & scheduling
A Redis-backed job queue (BullMQ) and cron safety-nets flip overdue items, deduplicate notifications at the database level, and reach the right agent — so a promised callback is never quietly missed.
Integrations
Cloud telephony for calls and SMS with an embedded softphone; a transactional email service for account and reset messages; and object storage for recordings and attachments, served through short-lived presigned URLs.
Technology
The full stack
| Layer | Technologies |
|---|---|
| Frontend |
|
| Backend |
|
| Data & real-time |
|
| Infrastructure |
|
| Quality |
|
Security
Hardened at every layer
| Control | Layer | What it protects |
|---|---|---|
| HttpOnly BFF session | Session | The auth token lives in an HttpOnly, Secure cookie the browser’s JavaScript can never read; a backend-for-frontend attaches it server-to-server on every request. |
| JWT + bcrypt | Auth | Stateless JWT authentication with bcrypt-hashed passwords. The API refuses to start without its signing secret. |
| Role-based access | Access | Three roles — agent, admin, dev — enforced both at the API with guards and at the edge with route gates. |
| Scoped socket tokens | Access | WebSockets use a separate ~2-minute token the REST API explicitly rejects, so a leaked socket token can never reach your data. |
| Distributed rate limiting | Rate limits | A Redis-backed limiter shared across server replicas, keyed per user, with tighter limits on login and password reset. |
| Constant-time webhooks | Webhooks | Inbound telephony webhooks are authenticated with a constant-time token comparison that resists timing attacks. |
| Strict input validation | Validation | Every request body is validated and stripped to an allow-list before it reaches any business logic. |
| Hardened transport | Transport | Helmet security headers with a content-security policy, CORS locked to the frontend origin, and per-request timeouts. |
| Safe file handling | Storage | Uploads are size-capped and stored privately; files are served only through short-lived presigned URLs — never public objects. |
| Careful secrets & data | Secrets | Secrets read from the environment and masked in logs, destructive schema sync disabled by default, sensitive fields excluded from responses, and soft deletes throughout. |
| Scoped API keys | API keys | Programmatic access uses hashed API keys with explicit scopes, expiry and last-used tracking. |
What it manages
One system, the whole operation
- Calls
- Tickets
- Manual records
- Scheduled calls
- Customers
- Campaigns
- Sites
- Owners
- Phone lines
- SMS analytics
- Notifications
- Users & agents
- Reports & exports
- API keys
- Configurable dictionaries
The system
More of the platform


Thinking about a system like this?
Every build starts with a conversation about how your operation actually runs — not a template.