-
Notifications
You must be signed in to change notification settings - Fork 0
Home
forge-voice is a small self-hosted telephony sidecar for Forge. It wraps Asterisk behind a handful of HTTP endpoints so a Forge install can place calls on its own SIP trunk and get finished calls logged as communication records — without the main application ever carrying a SIP stack, RTP, codec negotiation or voicemail routing.
It is deliberately the smallest thing that can be called a bridge: one Node file, four Asterisk config files, a Compose stack of two containers. It is not part of the Forge deployment stack, it holds no Forge data, and nothing in Forge requires it.
The name causes a recurring misunderstanding, so state it plainly: this is call control, not speech. There is no speech recognition here, no transcription, no text-to-speech, no voice command surface, no IVR beyond a two-line dialplan. Forge's voice-assistant and TTS features are unrelated code living in forge-api.
| State | |
|---|---|
| Outbound call placement | Implemented — an HTTP POST becomes an ARI Originate on the trunk |
| Inbound call reporting | Implemented — a call arriving on the trunk enters the Stasis app and is reported to Forge |
| Call-state callback to Forge | Implemented — authenticated by a shared secret, terminal states become communication records |
| Call recording |
Off, and off deliberately. asterisk/extensions.conf carries a commented MixMonitor template with the disclosure playback above it. Consent law varies by jurisdiction; enabling it is a per-install legal decision, not a toggle |
| Transcription / STT / TTS | Not here at all. Forge's ingestion path accepts a transcript field, but nothing in this repo produces one |
| Voicemail drop, power dialer, IVR menus | Not implemented |
src/server.js is the shape of a bridge and says so in its own header comment. The Originate endpoint string and the dialplan usually need tuning for a given trunk's endpoint naming and caller-ID rules. Treat it as a starting point you adapt, not a finished product you configure.
forge-ui (browser) ──POST /api/voice/call──▶ forge-voice ──ARI──▶ Asterisk ──SIP──▶ trunk ──▶ PSTN
│
forge-api ◀──POST /api/v1/voice/webhook────────────┘ (call-state events)
The split is the point. Call control is open source and self-hosted; the SIP trunk is a commodity you buy per minute. Any provider that speaks plain SIP works, and changing provider is a credentials change rather than a rewrite. That is the whole argument against wiring Forge to a proprietary voice API — Forge does ship a Twilio webhook path, but that is Twilio's Programmable Voice, a different product from the SIP Trunking one that would sit perfectly well under this repo.
One endpoint on the API side carries the whole integration: POST /api/v1/voice/webhook, in VoiceController.cs. The bridge posts call-state JSON there with an X-Forge-Voice-Secret header; the controller compares the secret in fixed time, ignores non-terminal states, and hands terminal ones to a channel-agnostic ingestion handler that also serves inbound email and the Twilio path. A call and an email land as the same kind of record — the party is resolved from the phone number, the duration is stored, and a transcript, if one ever exists, is hashed as its own artifact separately from any audio.
Two things gate that endpoint from the API side, and both are off on a fresh install:
-
CAP-EXT-VOIP-SYNCmust be enabled. The controller carries a capability attribute, so with the capability off the route refuses the call. See Capability Gating on the hub for what that means and how the refusal looks. -
Voice:WebhookSecretmust be configured on the API. Unset, the endpoint answers 503 rather than defaulting open — an install that never configured a secret has not opted into accepting call events, and anything that could post there unauthenticated could fabricate a record the system treats as evidence.
The bridge mirrors that posture: with no secret configured it logs an error and posts nothing, rather than hammering a rejecting endpoint.
This is the most important honest caveat in this wiki, and it contradicts this repo's own README.
forge-ui ships an AsteriskOutboundService that posts to this bridge's /api/voice/call, alongside the default TelLinkOutboundService that just emits a tel: link and places no call. The README tells you to point the OutboundCallService alias at the Asterisk implementation. That alias is not what the application injects. The one shipped consumer — the leads queue's click-to-dial button — imports and injects TelLinkOutboundService by its concrete class, so flipping the alias changes nothing. Two further gaps sit behind that one: voiceServiceUrl does not exist in any environment*.ts file (the service defaults its base URL to an empty string and reports itself unavailable), and the bridge runs no CORS middleware, so a cross-origin browser POST to it would not survive preflight in any case.
What that adds up to: inbound call logging works end to end; browser-initiated outbound dialling does not, without changes on both sides. Outbound origination itself works fine when driven server-side or by hand against the bridge's endpoint. If you intend to wire the SPA path, plan on a small forge-ui change plus reverse-proxying the bridge under the SPA's own origin — see Setup.
Advertised capability flags on the UI service overstate the bridge too: AsteriskOutboundService declares recording: true and voicemailDrop: true, and neither exists here.
The bridge listens on 3030 and holds call state in a plain in-memory map, so a restart drops tracking for anything in flight.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/voice/call |
Place a call. Body { phone, callerId?, context? }; returns { ok, callId, channel } once Asterisk accepts the Originate |
POST |
/api/voice/hangup/:callId |
Hang up a tracked call |
GET |
/api/voice/status/:callId |
Channel state for a tracked call |
GET |
/health |
Liveness, plus whether the ARI connection is up |
None of these endpoints authenticate. Anything that can reach port 3030 can place a call on your trunk and spend your money. Treat that as a deployment requirement, not a detail — Setup covers the exposure posture.
-
Only two states are ever emitted. The bridge posts on
StasisStart(asUp) andStasisEnd(asHangup). It never emitsBusy,NoAnswerorFailed, even though the API understands all three as terminal. -
An unanswered outbound call is never logged. If the Originate times out without the channel entering Stasis, no
StasisEndfires, so no event is posted — and the entry stays in the in-memory map for the life of the process. A dial-heavy install should expect that map to grow. - Duration is measured from the Originate, not from answer, so outbound durations include ring time. The API rounds seconds up to whole minutes.
-
An outbound call with no
callerIdposts a literal string as itsfromvalue. The API resolves the external party from the to number for outbound calls, so this is usually harmless, but it is not a phone number and it will look odd in logs. - No tests exist. CI installs dependencies, syntax-checks every JS file and builds the image. That is the whole gate, and the workflow file says as much.
- Setup — what to run, how to configure it, and the trunk, registration and networking decisions.
- Configuration and Integrations on the hub — how Forge settings work generally, and how to tell whether an integration on your install is actually connected.
- Hardening a Production Install — the posture this sidecar has to fit into.
- Limits and Non-Goals — the honest edges of the product as a whole.
- forge-api wiki — the ingestion side of the seam. · forge-ui wiki — the click-to-dial surface. · forge-deploy wiki — the main stack, which does not include this service.
Issues and pull requests belong on this repo; bugs in the main application go to the Forge repo. Contribution mechanics are on the hub's Contributing page.
forge-voice · Apache 2.0 · built by Armory Works — a spoke of the Forge wiki, which carries the product-level answers.
This repo
Hub pages you'll want
- Capability Gating
- Configuration and Integrations
- Hardening a Production Install
- Limits and Non-Goals
- Contributing
Sibling wikis
Repo · Install Forge · Site