Node, Bun and Deno servers
Run a pipedog voice agent as a long-lived server process, with SmallWebRTC or WebSocket sessions.
A server process is the most flexible host: it runs both transports, local turn detection models, and any framework. The voice bot guide builds one with SmallWebRTC, and Frameworks mounts the same endpoints in SvelteKit, Hono, Next.js and others.
The process
Each session runs in the same process, as its own pipeline and PipelineWorker. Start one per
connection, from the endpoint handler, and end it when the client disconnects:
transport.eventHandler("on_client_disconnected", async () => {
await runner.cancel();
});The shared agent in
examples/framework-snippets/shared/agent.ts
does exactly this. Pass handleSigint: false and handleSigterm: false to each session's
WorkerRunner, so one session doesn't take the process's signals; handle shutdown in the server.
Runtimes
- Bun runs the agent and upgrades WebSockets in
Bun.serve: pass the server Bun givesfetchtowebsocketFetchHandler's handler and serve withbunWebSocket. See Bun. - Node.js 22 or later. Use a framework's Node adapter (Express, Hono's
@hono/node-server, SvelteKit'sadapter-node), ornodeRequestListenerfrom@pipedog/transport-smallwebrtc/httpfor a plainnode:httpserver. For WebSocket endpoints,serveFetch()from@pipedog/transport-websocket/serveserves a fetch handler with upgrades (with thewspackage). - Deno serves both endpoints with
Deno.serve. See Deno.
serveFetch(handler, { port }) listens with the same fetch handler on all three runtimes, so one
server.ts runs on Bun, Node and Deno; examples/twilio-phone-bot is served this way.
The development runner
@pipedog/runner is Pipecat's development runner. main() serves your bot()
with the prebuilt browser client, SmallWebRTC signalling and the telephony webhooks and WebSockets,
from flags (bun bot.ts -t twilio --proxy ...). For a server you deploy, build the same app with
createRunnerApp() and listen with serve(), or mount its fetch in your own server:
import { createRunnerApp } from "@pipedog/runner/run";
import { serve } from "@pipedog/runner/serve";
const runner = await createRunnerApp({ bot, transport: "twilio", proxy: "bot.example.com" });
await serve(runner, { host: "0.0.0.0", port: 7860 });serve() runs on Bun, Node (with ws) and Deno. The runner's routes don't authenticate, as
upstream's don't; put your checks in front of them. See
Mounting the runner.
SmallWebRTC in production
SmallWebRTC is a peer-to-peer WebRTC connection between the browser and your server. The media runs over UDP, so:
- the server needs UDP reachable from clients, on the ports the WebRTC stack picks;
- behind NAT, give the transport ICE servers (STUN, and TURN for restrictive networks);
- one server holds every session's media, so scale by adding servers and routing sessions to them.
See SmallWebRTC for the transport's options.
WebSocket in production
WebSocket sessions need only TCP, so they pass through ordinary load balancers and proxies. Keep idle timeouts on the proxy longer than a call, or send keepalives. Each socket stays on the server that accepted it for its lifetime. See WebSocket.
Containers
A container image needs the runtime and your app. If you use local turn detection
(@pipedog/audio-silero, @pipedog/audio-smart-turn), the ONNX models load at startup from a
path or URL you give them; bake them into the image or download them on start. See
Turn detection.
Sizing
Each session decodes, resamples and encodes audio, and keeps provider connections open. Measure CPU and memory per session under real load, and cap concurrent sessions per process. Provider rate limits often bind before CPU does.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).