-
Notifications
You must be signed in to change notification settings - Fork 0
Setup
Running forge-voice is a Compose bring-up plus three decisions that have nothing to do with this repo: which SIP trunk you buy, how that trunk authenticates you, and how the bridge reaches both Asterisk and the Forge API across whatever network topology you have. The bring-up is easy. The three decisions are where installs actually get stuck, so most of this page is about them.
This service is not shipped by forge-deploy and is not part of the main stack. Nothing in Installation brings it up, and no forge-deploy script or Compose file references it. You clone it and run it yourself, next to Forge.
Two containers. Asterisk itself, from a stock upstream image, with this repo's four config files mounted read-only over /etc/asterisk; and the Node bridge, built from the repo's own Dockerfile, which connects to Asterisk over ARI, the Asterisk REST Interface, and exposes the HTTP endpoints described on Home.
| File | What it decides |
|---|---|
asterisk/pjsip.conf |
The trunk: transport, endpoint, auth, AOR, registration, and the context inbound calls land in |
asterisk/extensions.conf |
The dialplan. Both directions hand off to the forge-voice Stasis app; the recording template lives here, commented |
asterisk/ari.conf |
The ARI user the bridge authenticates as, and the origin allowlist |
asterisk/http.conf |
Asterisk's HTTP listener, which ARI rides on |
src/server.js |
Endpoints, in-memory call tracking, and the webhook post |
Everything Asterisk-side is intentionally minimal. Production installs are expected to grow the dialplan — DID routing, business-hours handling, voicemail — and that work belongs in extensions.conf, not in the bridge.
Docker with Compose, a SIP trunk account, and — if you want calls logged — a reachable Forge API. Without the API the bridge still places calls; it simply reports nothing. To run the bridge outside Docker instead, you need a current Node runtime (the Dockerfile and CI pin the major version; follow those rather than a number written in prose) and an Asterisk instance with ARI enabled somewhere it can reach.
You do not need a Twilio account, carrier licensing, or a kernel-mode SIP stack. Asterisk does all of that inside its container.
Copy .env.example to .env and fill it in. Nothing here belongs in a committed file — .env is gitignored, keep it that way.
| Variable | Purpose |
|---|---|
SIP_TRUNK_HOST |
Your provider's hostname, used for the endpoint, the AOR contact and the registration URIs |
SIP_TRUNK_USER / SIP_TRUNK_PASS
|
Trunk credentials, for a registration-based trunk |
ARI_PASS |
The password the bridge authenticates to Asterisk's ARI with. ARI_USER defaults to forge-voice
|
FORGE_API_WEBHOOK |
Where call-state events are posted. Defaults to the API's voice webhook route on the Compose service name |
FORGE_WEBHOOK_SECRET |
Shared secret sent as X-Forge-Voice-Secret. Unset, the bridge logs an error and posts nothing |
QB_API_WEBHOOK and QB_WEBHOOK_SECRET are read as fallbacks for installs configured before the project was renamed. New installs should ignore them.
Always set ARI_PASS explicitly. The bridge falls back to a placeholder value when it is unset rather than refusing to start, which produces a service that looks up but cannot authenticate — or, worse, one that can be authenticated against by anyone who read the repo.
The README's "Known issue" section is stale. It warns that
docker-compose.ymlforwards only the legacyQB_*names, so theFORGE_*values never reach the container. That was true; the commit that wrote the warning also fixed the Compose file, and the warning survived the rewrite. The current Compose file forwardsFORGE_API_WEBHOOKandFORGE_WEBHOOK_SECRETdirectly, with the legacy pair passed through only when set. Ignore the warning; it is a documentation bug worth a PR.
pjsip.conf and ari.conf are written with ${SIP_TRUNK_USER}-style placeholders, and the Compose file passes the matching variables into the Asterisk container. Nothing renders them. Asterisk's static configuration parser does not expand shell-style variables out of the process environment, there is no entrypoint or envsubst step anywhere in this repo, and the files are mounted read-only so nothing could rewrite them in place. The values arrive at Asterisk literally, character for character.
So before your first bring-up, render those files yourself: template them into a generated directory and mount that, or bake real values into a copy that is not committed. A trunk that never registers, and an ARI login that fails with the right password in .env, are both this trap.
Three settings on the main application, none of them in this repo:
-
Voice:WebhookSecretonforge-api, set to the same value asFORGE_WEBHOOK_SECREThere. There is no entry for it in the shippedappsettings.jsonand no variable for it in the deploy stack's environment file, so supply it as an environment variable using .NET's section separator —Voice__WebhookSecret— on the API container. Unset, the webhook answers 503. -
CAP-EXT-VOIP-SYNC, enabled. It gates the whole voice controller. Off by default on purpose, because call handling is legally fraught. See Capability Gating. -
The
forge-uioutbound-call provider, if and only if you want the browser click-to-dial path. Read the last section of Home first — that path needs code changes, not configuration.
pjsip.conf as shipped describes one trunk, authenticating by username and password, registering outbound. That covers most retail SIP providers. Three things about it are worth understanding rather than copying.
Registration versus IP authentication. A registering trunk sends credentials to the provider and is matched on inbound calls by the From header. An IP-authenticated trunk does not register at all; the provider recognises you by source address, and Asterisk has to recognise the provider the same way. For that you need the [trunk-identify] block — present in the file, commented, with placeholder addresses — set to your provider's signalling addresses. Without it, inbound calls on an IP-authenticated trunk match no endpoint and are refused. Providers publish these addresses and change them occasionally; treat the list as something to re-check, not set once.
An endpoint's context is where calls from it are routed — that is, inbound. This is the single most common misconfiguration in a file this small, and this repo shipped it wrong once: the endpoint pointed at outbound, so every incoming call entered the outbound dialplan and was treated as a dial attempt. It now reads context=inbound. Outbound calls do not use the endpoint context at all — the bridge originates them straight into the Stasis app over ARI.
Every call has to reach Stasis. That is what makes it visible to the bridge: StasisStart fires for a channel the bridge did not originate, which is how an inbound call becomes a communication record in Forge. If you extend the dialplan, whatever you add must still end at Stasis(forge-voice) or the call happens and Forge never hears about it. The shipped [inbound] context also carries a catch-all so an unrouted DID is visible in the log instead of being dropped silently.
Caller ID and endpoint naming are provider-specific. The Originate builds an endpoint string of the form PJSIP/<number>@trunk, and passes a caller-ID string straight through. Providers differ on whether they want E.164, a leading 1, or a stripped national number, and most will reject or rewrite a caller ID you have not proven you own. Expect to adjust both the number formatting and the caller ID for your trunk — this is the tuning the code's own header comment warns about.
Codecs. The endpoint allows ulaw, alaw and g722 and disallows everything else. If your provider offers only a compressed codec, or you want one, that list is where you change it, and transcoding costs the Asterisk container CPU.
Asterisk runs with host networking, deliberately: RTP media needs a wide UDP port range and predictable addressing, and NAT traversal through a Docker bridge is a fight not worth having. That one choice creates the rest of the topology problems.
The bridge reaching Asterisk. The Compose file points ARI_URL at host.docker.internal. On Docker Desktop that resolves. On Linux it does not, unless you add a host-gateway mapping to the voice-bridge service's extra_hosts, run the bridge on host networking too, or point ARI_URL at the host's bridge-gateway address. Since a self-hosted Forge box is almost always Linux, assume this needs fixing on your first bring-up. The symptom is honest at least: the bridge logs a failed ARI connection, retries, and /health reports the ARI connection down.
The bridge reaching the Forge API. The default webhook URL uses the API's Compose service name, which only resolves if the bridge is on the same Docker network as the Forge stack. This repo's Compose file declares no networks at all, so out of the box it is not. Either join the bridge to the Forge stack's network as an external network, or set FORGE_API_WEBHOOK to an address the bridge can actually reach.
The browser reaching the bridge. AsteriskOutboundService is Angular code running in the user's browser, not server-side code, so its base URL must be resolvable from the browser — a Docker service name never is. And a different host or port is a different origin, which the bridge's Express app does not answer preflight for. If you pursue the browser dialling path, the clean answer is to reverse-proxy the bridge under the SPA's own origin at a path prefix; that removes the CORS problem instead of configuring around it.
Exposure. The bridge publishes 3030 and authenticates nothing; Asterisk's HTTP listener binds all interfaces on the host. Both are fine on a loopback-only or private-network install and are not fine on anything reachable from outside. Bind the published port to loopback, keep the SIP and ARI ports behind the host firewall, and put any externally reachable path through an authenticating reverse proxy. The hub's Hardening a Production Install sets the posture the rest of the stack expects, and this sidecar should not be the hole in it.
The allowed_origins list in ari.conf names a single origin. A Node ARI client sends no Origin header, so that list only matters if you ever point a browser-based ARI tool at the same instance — do not read it as the thing protecting ARI.
Bring the stack up and check /health: it reports liveness and, separately, whether ARI is connected. Those are different failures and the endpoint distinguishes them for a reason. Then confirm the trunk registered from the Asterisk console before you place anything.
The end-to-end check that actually proves the seam is an inbound one, because it exercises the whole path without touching the unwired browser side: call your DID, let it complete, and look for a Communication row against the caller in Forge. If the call connects but nothing lands, work the chain in order — did the call reach Stasis, did the bridge log the inbound channel, did the webhook post, did the API accept it. A 503 from the API means Voice:WebhookSecret is unset; a 401 means the two secrets differ; a capability refusal means CAP-EXT-VOIP-SYNC is off. The API logs each of those distinctly.
The dialplan carries a commented MixMonitor line with a disclosure playback above it, and the pair is commented as a set on purpose. Recording is a legal decision before it is a technical one — consent requirements differ by state and country, and several jurisdictions require every party to agree — so this repo will not make it for you. If you enable it, enable the disclosure with it, and know that the ingestion handler on the API side already stores audio and any transcript as separately hashed artifacts, so that a later dispute can tell a recording apart from a transcription of it. Retention of that audio then becomes your problem under Data Ownership and Export.
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