Local Mode
Local mode runs an embedded Express server on your infrastructure. It handles telephony webhooks, manages WebSocket audio streams, and connects to AI providers directly — no external service required.Starting the Server
ServeOptions
Webhook Endpoints
The embedded server exposes these HTTP endpoints:Health Check
{ "status": "ok", "mode": "local" }.
Twilio Endpoints
Telnyx endpoints are fully supported with feature parity to Twilio (DTMF, transfer, recording).
Telnyx Endpoints
WebSocket Streams
Audio streams are handled over WebSocket. The server upgrades HTTP connections to WebSocket on the/ws/stream/ path and each call gets its own connection. How caller and callee reach the handler depends on the carrier:
- Twilio — the
wss://{webhookUrl}/ws/stream/{callId}URL has no query string. Twilio strips query-string params during the WS upgrade handshake, so the inbound TwiML emits the caller / callee as<Parameter name="caller" value="..."/>/<Parameter name="callee" value="..."/>children of<Stream>. The values are then surfaced in the WSstartframe asstart.customParametersand applied byStreamHandler.handleCallStart. - Telnyx —
wss://{webhookUrl}/ws/stream/{callControlId}?caller={caller}&callee={callee}. The Call Control flow includes the metadata in the answer command and the SDK reads the query string on the WS upgrade.
/webhooks/twilio/voice route emit it), use TwilioAdapter.generateStreamTwiml(streamUrl, { caller, callee }) — the parameters argument is forwarded as <Parameter> children of <Stream> so it lands on start.customParameters.
Rate Limiting
WebSocket connections are rate-limited to 10 concurrent connections per IP address. Connections exceeding the limit receive a429 Too Many Requests response. This protects against DoS attacks while being generous enough for legitimate telephony provider traffic (which only opens 1 connection per call).
Security
Twilio Signature Validation
When a Twilio Auth Token is configured (either passed tonew Twilio({ authToken }) or via TWILIO_AUTH_TOKEN), all Twilio webhook requests are validated using HMAC-SHA1 signature verification:
- The SDK reconstructs the URL from the webhook hostname and request path
- Parameters are sorted and concatenated
- An HMAC-SHA1 digest is computed using the Twilio Auth Token
- The result is compared with the
X-Twilio-Signatureheader using timing-safe comparison
Telnyx Signature Validation
When a Telnyx public key is configured (either passed tonew Telnyx({ publicKey }) or via TELNYX_PUBLIC_KEY), Telnyx webhook requests are verified using Ed25519 signatures:
- The raw request body is captured before JSON parsing
- The signed payload is:
{timestamp}|{rawBody} - The Ed25519 signature is verified against the Telnyx public key
- Requests older than 5 minutes are rejected (replay protection)
Architecture
Audio Transcoding
Graceful Shutdown
Callphone.disconnect() to gracefully stop the embedded server. The shutdown sequence:
- Stop accepting new connections — the HTTP server stops listening.
- Hang up active calls — each active call is terminated via the telephony provider API (Twilio, Telnyx, or Plivo).
- Close WebSocket connections — a close frame (
1001 Server shutting down) is sent to all active WebSocket connections. - Wait for drain — the server waits up to 10 seconds for active connections to close cleanly.
- Force-terminate — any connections still open after the drain timeout are forcibly terminated.
- Close HTTP server — the underlying HTTP server is fully closed.
If a cloudflared tunnel was started with
tunnel: true, it is also stopped when disconnect() is called.Binding Address
The server binds to0.0.0.0 by default, which means it listens on all network interfaces. This is necessary for the server to accept connections from telephony providers and tunnels, but it also means the server is accessible from other machines on your network.
Use a tunnel (Cloudflare Tunnel, ngrok) to expose the server to the internet for telephony provider webhooks.
