A developer has released an open-source alternative to proprietary call center software, built from scratch to avoid hidden fees and vendor lock-in. The system, now available on GitHub under the AGPL-3.0 license, combines VoIP routing, media handling, and campaign management into a self-hostable stack. Instead of documenting only the final design, the creator focuses on the unexpected bugs that derailed testing—and how each was resolved.
The architecture: decoupling signaling from media
The dialer splits call control and audio processing between two specialized tools. At the edge, Kamailio handles SIP signaling, routing, and high call-per-second (CPS) throughput. It uses the secsipid module to support STIR/SHAKEN caller ID verification, a requirement for most carriers today.
Behind Kamailio, FreeSWITCH manages audio streams, call origination, and advanced detection. The system fires calls via bgapi originate and monitors answering machine beeps with mod_avmd. Each call runs a lightweight Lua script that tracks state—dialing, greeting, digit collection, bridging—and reacts to real-time ESL (Event Socket Library) events from FreeSWITCH.
The core engine running pacing logic, lead claiming, and state transitions is written in Go. It uses database-level locking with SELECT FOR UPDATE SKIP LOCKED to assign leads atomically across campaigns without race conditions. Tenant isolation is enforced at the database layer using PostgreSQL Row-Level Security (RLS), ensuring data from one business never leaks into another’s dashboard or reports.
For agents and supervisors, a React web console provides dashboards for campaigns, leads, scripts, and analytics. The frontend communicates with the Go backend over a REST API, which in turn talks to FreeSWITCH and Kamailio via ESL and SIP.
Why Kamailio sits in front of FreeSWITCH
Separating signaling from media may seem like over-engineering for small deployments. But Kamailio excels at high-throughput SIP routing and policy enforcement, while FreeSWITCH specializes in real-time audio and media handling. Keeping them distinct allows horizontal scaling of the media tier—adding more FreeSWITCH servers—without touching the SIP edge. This separation also simplifies monitoring and troubleshooting in production environments.
Trusting tenant isolation with PostgreSQL RLS
Historically, multi-tenancy in telephony systems often relied on vigilant developers adding WHERE tenant_id = ? to every query. After years of chasing missing filters, the developer adopted PostgreSQL RLS policies. Every row includes a tenant_id, and the API sets a session variable (SET app.current_tenant = ?) at login. The database then automatically filters all subsequent queries to that tenant. Even a typo like SELECT * FROM leads cannot accidentally expose another customer’s data—PostgreSQL enforces the rule. This change provided more peace of mind than any code review or checklist ever did.
The bugs that burned midnight hours
Press-1 campaigns stayed silent. The developer had replaced internal agents with external bridging but forgot to update the press-1 pacing logic. It still waited for “available agents,” which were now always zero. The fix was to treat press-1 like broadcast: fill to capacity, then cap concurrency via the operator dashboard.
MP3 greetings sounded like dead air. FreeSWITCH relies on mod_sndfile for audio playback, but it cannot decode MP3 files without mod_shout loaded. Enabling mod_shout fixed playback, but audio quality was abysmal—44.1kHz stereo files were downsampled to 8kHz mono on every call. The real solution was preprocessing: uploads are now transcoded to 8kHz mono PCM WAV using ffmpeg at ingestion time. This lets mod_sndfile play files natively, avoiding resampling on every call.
Pressing 1 did nothing—and the night vanished. In Lua, session:execute("playback", file) consumes incoming DTMF digits. A greeting played with this method swallowed the caller’s “1” before the subsequent session:read could detect it. The call would proceed directly to timeout, making it appear as if pressing 1 hung up the call. The fix was switching to session:playAndGetDigits(), a single function that both plays a prompt and collects a digit without eating it.
Answering machine detection without SaaS fees. The legacy mod_amd module is no longer maintained, and the successor mod_com_amd moved to a paid license. The developer built a workaround using mod_avmd (beep detection) in broadcast mode. The Lua script starts avmd, plays the message, and on detecting a beep, replays the message after the tone. This creates a clean recording for voicemail without licensing costs.
Where the project stands today
The system is pre-release but functional. Press-1 and broadcast flows—origination, AMD, greeting playback, digit collection, bridging, and recording—are all wired end to end. Testing has been conducted over a Linphone gateway, which introduced its own DTMF quirks. STIR/SHAKEN signing is configured in Kamailio, but it has not yet been validated against a real carrier. There is no agent softphone app, nor is a production carrier trunk connected.
The developer invites telephony veterans to scrutinize the architecture before real traffic hits the system. The code is available on GitHub under AGPL-3.0 and designed to be self-hosted—no licenses, no subscriptions, just the dialer the creator wished existed.
This project shows that even a well-planned stack can stumble over edge cases, but the right architecture and discipline in implementation can turn debugging nightmares into reliable infrastructure.
AI summary
Açık kaynaklı bir ViciDial alternatifi inşa eden geliştirici, kullandığı yığını ve geceyarılarını uyutmayan kritik hataları paylaşıyor. PostgreSQL RLS, FreeSWITCH ve Go tabanlı mimariyi keşfedin.