pipedog is an unofficial community port of Pipecat, not affiliated with Daily.
pipedog

Overview

Mount a pipedog voice agent in SvelteKit, Hono, Bun, Next.js, TanStack Start, Cloudflare Workers, Express, Elysia, Nuxt, Deno or Vercel.

pipedog's voice endpoints are fetch handlers: a function from a Request to a Response. Each framework only has to route a request to them. Every snippet in this section is in examples/framework-snippets, and the test suite there mounts each one and checks it.

The two endpoints

  • offer: WebRTC signalling for SmallWebRTC. The browser POSTs its SDP offer and gets the answer. The media then runs over UDP, so the server has to be a long-lived process, not a serverless function.
  • socket: a WebSocket with protobuf audio frames, for clients that stream audio over a socket (see WebSocket). It works wherever the runtime can upgrade a request from fetch. Where it can't, the framework's page says where to mount it instead.

The agent behind both is the same: speech in, OpenAI transcription, chat and speech synthesis, speech out, as in the voice bot guide. It needs OPENAI_API_KEY in the environment, and it reads OPENAI_BASE_URL if you set one. Silero detects speech, and the default turn strategies end the turn with Smart Turn (see Turn detection).

Every framework page imports the endpoints from this module:

shared/voice.ts
// The two voice endpoints, as fetch handlers. Each framework's snippet mounts
// them; see the framework folders.

import { createTask } from "@pipedog/asyncio";
import { TransportParams } from "@pipedog/core/transports/base_transport";
import { ProtobufFrameSerializer } from "@pipedog/serializer-protobuf/protobuf";
import { smallWebRTCOfferHandler } from "@pipedog/transport-smallwebrtc/http";
import { SmallWebRTCRequestHandler } from "@pipedog/transport-smallwebrtc/request_handler";
import { SmallWebRTCTransport } from "@pipedog/transport-smallwebrtc/transport";
import {
  FastAPIWebsocketParams,
  FastAPIWebsocketTransport,
} from "@pipedog/transport-websocket/fastapi";
import { websocketFetchHandler } from "@pipedog/transport-websocket/fetch";
import type { WebsocketConnection } from "@pipedog/transport-websocket/socket";
import { runAgent } from "./agent.ts";

/**
 * WebRTC signalling: POST an SDP offer, get the answer. The media runs over UDP,
 * so this needs a long-lived server, not serverless functions.
 */
export const offer = smallWebRTCOfferHandler(new SmallWebRTCRequestHandler(), (connection) => {
  createTask(
    () =>
      runAgent(
        new SmallWebRTCTransport(
          connection,
          new TransportParams({ audioInEnabled: true, audioOutEnabled: true }),
        ),
      ),
    "voice-agent",
  );
});

/** Runs one agent on a WebSocket connection, with protobuf audio frames. */
export function runSocketAgent(connection: WebsocketConnection): Promise<void> {
  return runAgent(
    new FastAPIWebsocketTransport(
      connection,
      new FastAPIWebsocketParams({
        audioInEnabled: true,
        audioOutEnabled: true,
        serializer: new ProtobufFrameSerializer(),
      }),
    ),
  );
}

/** A WebSocket endpoint as a fetch handler, where the runtime upgrades from fetch. */
export const socket = websocketFetchHandler(runSocketAgent);

The module builds the agent with runAgent from shared/agent.ts, which is the pipeline above.

Authentication

pipedog's endpoints don't authenticate: anyone who can reach them can start a session, on your provider keys. That's the same split as Pipecat, whose transports don't authenticate either; its development runner mints one-time tokens for its WebSocket endpoints, and production auth comes from the application or Pipecat Cloud. Put your own check in front of each endpoint, in the framework, so it runs before a session starts:

/** Rejects requests without a valid session token before the endpoint runs. */
function requireSession(endpoint: (request: Request) => Promise<Response>) {
  return async (request: Request): Promise<Response> => {
    const url = new URL(request.url);
    // Browsers can't set headers on a WebSocket, so a socket brings its token in the URL.
    const token =
      request.headers.get("authorization")?.replace(/^Bearer /, "") ??
      url.searchParams.get("token");
    if (!token || !(await verifySessionToken(token))) {
      return new Response("unauthorized", { status: 401 });
    }
    return endpoint(request);
  };
}

export const authedOffer = requireSession(offer);
export const authedSocket = requireSession(socket);

verifySessionToken is yours: a signed token your app issues when it creates the session, short-lived and used once, as Pipecat's runner does. Add a cap on concurrent sessions and on session length there too, since each session spends provider credits.

Compare

FrameworkRuntime(s)offer (WebRTC)socket (WebSocket)Serverless?Tested how
SvelteKitNode, Bun, Deno (adapter-node or similar)YesNot in the snippetNoRoute handler called directly
HonoBun (Node and Workers with other adapters)YesYes, with hono/bunNoServed with Bun.serve, both routes
BunBunYesYesNoServed with Bun.serve, both routes
Next.jsNodeYesNo, mount it elsewhereNoRoute handler called directly
TanStack StartNode, BunYesNo, mount it elsewhereNoServer route handler called directly
Cloudflare WorkersworkerdNo, no UDPYes, in a Durable ObjectYesIts own example, run against workerd
ExpressNode, BunYesNot in the snippetNoapp.listen, then fetch
ElysiaBunYesNot in the snippetNoapp.handle
NuxtNode, Bun, Deno (Nitro presets)YesNot in the snippetNoHandler mounted on h3
DenoDenoYesYesNodeno run, both routes, when Deno is installed
VercelBun runtime on Vercel FunctionsRoute mounted, UDP media untestedYesYesServed with Bun.serve, both routes

What is tested

The test suite runs on Bun, and runs the Deno snippet with Deno when it's installed. It checks two things for each snippet: the offer route reaches pipedog's handler (a framework's own 404 wouldn't return pipedog's message), and the WebSocket route accepts an upgrade where the snippet mounts one. The Next.js, TanStack Start, Nuxt and SvelteKit route files are called as the handlers they export; the others run as servers. The Cloudflare Workers example has its own tests, which run against workerd through wrangler dev.

Not tested here: a deployment on any of these hosts, a full TanStack Start, Next.js, Nuxt or SvelteKit build, and the WebRTC media on the serverless hosts (Vercel, Next.js on Vercel, Nuxt's serverless presets), which the UDP requirement rules out.

Frameworks

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).

On this page