Skip to main content

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

Returns { "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 WS start frame as start.customParameters and applied by StreamHandler.handleCallStart.
  • Telnyxwss://{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.
If you construct the inbound TwiML yourself (rather than letting Patter’s /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 a 429 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 to new Twilio({ authToken }) or via TWILIO_AUTH_TOKEN), all Twilio webhook requests are validated using HMAC-SHA1 signature verification:
  1. The SDK reconstructs the URL from the webhook hostname and request path
  2. Parameters are sorted and concatenated
  3. An HMAC-SHA1 digest is computed using the Twilio Auth Token
  4. The result is compared with the X-Twilio-Signature header using timing-safe comparison
Requests with invalid signatures are rejected with HTTP 403.

Telnyx Signature Validation

When a Telnyx public key is configured (either passed to new Telnyx({ publicKey }) or via TELNYX_PUBLIC_KEY), Telnyx webhook requests are verified using Ed25519 signatures:
  1. The raw request body is captured before JSON parsing
  2. The signed payload is: {timestamp}|{rawBody}
  3. The Ed25519 signature is verified against the Telnyx public key
  4. Requests older than 5 minutes are rejected (replay protection)
Requests with invalid signatures are rejected with HTTP 403.

Architecture

Audio Transcoding

Graceful Shutdown

Call phone.disconnect() to gracefully stop the embedded server. The shutdown sequence:
  1. Stop accepting new connections — the HTTP server stops listening.
  2. Hang up active calls — each active call is terminated via the telephony provider API (Twilio, Telnyx, or Plivo).
  3. Close WebSocket connections — a close frame (1001 Server shutting down) is sent to all active WebSocket connections.
  4. Wait for drain — the server waits up to 10 seconds for active connections to close cleanly.
  5. Force-terminate — any connections still open after the drain timeout are forcibly terminated.
  6. 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 to 0.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.
Because the server binds to 0.0.0.0, it is reachable from any network interface. In production, use a firewall or reverse proxy to restrict access. Always configure the Auth Token / public key so webhook signature verification is enforced.
Use a tunnel (Cloudflare Tunnel, ngrok) to expose the server to the internet for telephony provider webhooks.