FusionPBX works. That is not faint praise. It has run serious telephony for over a decade on top of FreeSWITCH, which is one of the more impressive pieces of open-source infrastructure anyone has written. But it is a PHP application from a different era of web development, and every time I went into the admin interface I found myself thinking about what it would look like if you started now. Eventually I stopped thinking and started writing. That became GOPBX.
GOPBX keeps FreeSWITCH for media and signalling and replaces everything above it: a Go 1.25 control plane, a SvelteKit and shadcn-svelte admin, PostgreSQL as the system of record, and a REST API that the web interface uses like any other client. Dialplan, directory and gateway XML is generated on demand, so those paths never go stale. Open source under MIT.
What I actually kept
The first decision was the one that mattered most: FreeSWITCH stays. There is no reason to reimplement a media engine and a SIP stack that already work, and any attempt to do so would be a multi-year project that ends up worse than what it replaced. FreeSWITCH handles what it is good at, media, signalling, codecs, the hard real-time parts, and GOPBX replaces the layer above it.
That layer is bigger than it first appears. A PBX platform is mostly configuration management with a real-time view attached: extensions, dialplans, gateways, time conditions, IVR trees, ring groups, queues. All of it has to be stored, validated, rendered into the XML that FreeSWITCH consumes, and then applied to a running system without dropping calls. That is where PHP was doing the work, and that is what I moved to Go.
Why Go for the control plane
Telephony control planes have a specific shape. They are long-running processes that hold persistent connections, react to a steady stream of events, and have to stay responsive while doing several things at once. The event socket connection to FreeSWITCH is a firehose: registrations, channel state changes, DTMF, hangups, all arriving continuously and all needing to be fanned out to whoever is watching.
Go handles that shape well. A goroutine per connection with channels between them is close to the natural way to describe the problem, and it stays readable when you come back to it six months later. The static binary matters too, deploying a PBX admin layer becomes copying one file rather than provisioning a PHP-FPM pool and hoping the extension versions line up.
The best argument for the rewrite was never performance. It was that the concurrency model matched the problem.
| Layer | Traditional stack | GOPBX |
|---|---|---|
| Media and signalling | FreeSWITCH | FreeSWITCH, unchanged |
| Control plane | PHP application | Go 1.25, single static binary |
| Admin interface | Server-rendered PHP pages | SvelteKit and shadcn-svelte |
| Configuration store | PostgreSQL | PostgreSQL |
| FreeSWITCH config | Database-backed, files or on demand | On demand for dialplan, directory, gateways |
| Automation path | Bolt-on, partial | Same REST API the UI uses |
| Deployment | PHP-FPM pool and dependencies | Copy one binary |
Only the rows marked in blue are the actual rewrite. Dynamic XML is not a GOPBX invention, FusionPBX serves configuration from its database too; what changed is the language and the interface above it. The engine below and the database beside it stayed where they were, which is the reason the project was finishable at all.
The API-first decision
The rule I set early was that the web interface gets no privileged path into the system. Everything, creating an extension, editing a dialplan, provisioning a device, reading call detail records, goes through the same REST API that anything else would use. The frontend is just the first client.
This is more disciplined than it sounds, and it pays for itself immediately. Automation stops being a separate integration project. Provisioning a hundred extensions from a CSV is the same set of calls the UI makes one at a time. Validation lives in one place, so the UI and a script cannot drift apart in what they accept. And when you want a second interface later, a mobile view, an operator panel, a script that reconciles state against a CRM, it already works.
XML generation is the interesting part
FreeSWITCH is configured through XML, and it can request that XML dynamically at the moment it needs it. That is the hinge the whole system turns on. GOPBX stores the canonical configuration in PostgreSQL, and renders XML on demand when FreeSWITCH asks for a dialplan, a directory entry or a gateway definition.
For the paths served dynamically, dialplan, directory and gateway lookups, this removes the synchronisation step: the database is the truth and FreeSWITCH reads it at the moment it matters, so a change applies to the next call without interrupting current ones. Configuration that FreeSWITCH only reads at startup still needs a restart, and the documentation is explicit about which is which. Getting this right removed an entire category of stale-configuration bugs that plague file-based setups.
The frontend: SvelteKit and shadcn-svelte
The admin interface is SvelteKit with Svelte 5 runes and a shadcn-svelte component library. I have written about the performance reasoning for that stack in more detail elsewhere, but the short version applies here too: Svelte compiles the framework away, which means less JavaScript arrives and less work happens at runtime, and the runes reactivity model makes fine-grained live updates straightforward rather than an optimisation exercise.
That last point is not cosmetic in a PBX. An operator panel showing live channels, registrations and queue state is a page where individual values change several times a second while the rest of the DOM stays still. A model that updates exactly the values that changed is doing the natural thing, not a clever thing.
Using shadcn-svelte rather than a bespoke design system was a deliberate shortcut. The components are accessible, consistent and unremarkable in the good sense, and the time saved went into the parts that actually differentiate the product.
Multi-tenancy from the beginning
Domain isolation is one of those properties that is nearly free at the start and nearly impossible to retrofit. Every object in GOPBX carries a domain, every query is scoped, and permissions are evaluated with the domain as part of the check rather than as an afterthought. If a service provider is running fifty customers on one installation, that isolation has to be structural, not a convention that a future contributor can accidentally break.
What is done and what is not
Here is the honest part. GOPBX is an active reimplementation of the FusionPBX workflows I needed, not a certified drop-in replacement for a production cluster. The core is real: REST CRUD across the configuration objects, XML generation, ESL control, the SvelteKit admin, multi-tenant domains. Other surfaces are scaffolded and are being filled in.
I am deliberately not publishing a feature-completeness percentage, because that number is meaningless without knowing which features you personally need. The repository is the accurate answer, and it is public under MIT, read the code, run it, and judge it against your own requirements rather than against a marketing table.
Would I do it again
Yes, with one caveat. Rewriting working software is usually a bad idea, and most rewrites fail because they underestimate how much accumulated knowledge is encoded in the code they are replacing. The reason this one has worked so far is that I did not rewrite the hard part. FreeSWITCH still does the telephony. What I replaced was the configuration layer above it, where the accumulated knowledge is mostly about data shapes rather than about protocol edge cases learned over years of production traffic.
The practical test before any rewrite: work out which part of the system holds knowledge you cannot reconstruct, and leave that part alone.
Frequently asked questions
What is GOPBX?
GOPBX is an open-source PBX platform with a Go 1.25 control plane, a SvelteKit and shadcn-svelte admin frontend, and FreeSWITCH handling media and signalling. It provides REST CRUD across configuration objects, dynamic XML generation for FreeSWITCH, and ESL-based control. It is licensed MIT.
Is GOPBX a fork of FusionPBX?
No. It is a functional clone rather than a fork, and it shares no code with FusionPBX. It targets the same problem space and a comparable feature surface, but the control plane is written from scratch in Go and the interface is a SvelteKit application rather than PHP.
Does GOPBX replace FreeSWITCH?
No, and it deliberately does not try to. FreeSWITCH remains the media and signalling engine. GOPBX replaces the configuration, provisioning and administration layer that sits above it, driving FreeSWITCH through the event socket and generated XML.
Why Go instead of PHP or Node?
Because a control plane is a long-running, event-driven, concurrent process, and Go's goroutine and channel model maps onto that shape directly. The single static binary also makes deployment substantially simpler than maintaining a PHP-FPM environment.
Is GOPBX production-ready?
It is in active development. The core, REST API, XML generation, ESL control, multi-tenant domains and the admin UI, is functional, while other surfaces are still being completed. The GitHub repository is the accurate source of truth for current status.