This document explains how the transcriber forwards transcriptions to an external dispatcher service for further processing (webhooks, storage, analytics, etc.).
The transcriber is platform-agnostic and can connect to any WebSocket-compatible dispatcher. The dispatcher implementation is separate and must be provided by you.
┌─────────────────────────────────────────────────────────────┐
│ Transcriber │
│ (Node.js server or Cloudflare Worker with Container) │
│ │
│ Client WS ←→ [Proxy] ←→ Backend (OpenAI/Deepgram/Gemini) │
│ │ │
│ │ (final transcriptions) │
│ ↓ │
│ Dispatcher Connection (WebSocket) │
└────────────────────┬────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ Your Dispatcher Implementation │
│ │
│ - Receives transcription messages │
│ - Fans out to webhooks, databases, analytics, etc. │
└─────────────────────────────────────────────────────────────┘
When running as a standalone Node.js server, the transcriber connects to the dispatcher via a configurable WebSocket URL.
Configuration:
| Variable | Description | Default |
|---|---|---|
DISPATCHER_WS_URL |
WebSocket URL of your dispatcher | (empty - disabled) |
DISPATCHER_HEADERS |
JSON object with auth headers | {} |
Example:
DISPATCHER_WS_URL=wss://your-dispatcher.example.com/ws
DISPATCHER_HEADERS='{"Authorization": "Bearer your-token"}'When deployed as a Cloudflare Worker, the transcriber connects to a Dispatcher Durable Object via WebSocket.
Why WebSocket to DO? Cloudflare Workers have a subrequest limit per invocation (1000 on enterprise, lower on other plans). For long-running sessions with many transcriptions, RPC calls and queue pushes count against this limit. The WebSocket-to-Durable-Object approach avoids this because each incoming WebSocket message grants fresh subrequest quota to the DO.
Add the DO binding to your wrangler.jsonc:
Option A: Environment Variable (recommended)
{
"vars": {
"USE_DISPATCHER": "true"
}
}Option B: Query Parameter (per-request)
wss://your-worker.workers.dev/transcribe?sessionId=test&useDispatcher=true
Precedence: Query parameter overrides the environment variable.
The transcriber sends messages in this format:
interface DispatcherTranscriptionMessage {
sessionId: string; // Session identifier
endpointId: string; // Participant ID
text: string; // Transcription text
timestamp: number; // Unix timestamp in milliseconds
language?: string; // Optional language code
}Implement a Durable Object that accepts WebSocket connections:
import { DurableObject } from 'cloudflare:workers';
export class TranscriptionDispatcherDO extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
if (request.headers.get('Upgrade') !== 'websocket') {
return new Response('Expected WebSocket', { status: 426 });
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
server.addEventListener('message', async (event) => {
const message = JSON.parse(event.data as string);
// Forward to webhooks, databases, etc.
await this.fanOut(message);
});
return new Response(null, { status: 101, webSocket: client });
}
private async fanOut(message: DispatcherTranscriptionMessage) {
// Your fan-out logic here
// Each WebSocket message grants fresh subrequest quota
}
}For Node.js or other platforms, implement a WebSocket server:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
const sessionId = new URL(req.url, 'http://localhost').searchParams.get('sessionId');
ws.on('message', async (data) => {
const message = JSON.parse(data.toString());
// Forward to webhooks, databases, etc.
});
});# Transcripts returned to client only (dispatcher disabled)
wscat -c "wss://your-endpoint/transcribe?sessionId=test&sendBack=true"# Transcripts returned to client + forwarded to dispatcher
wscat -c "wss://your-endpoint/transcribe?sessionId=test&sendBack=true&useDispatcher=true"wrangler tail --name=your-dispatcher-worker-nameView Worker logs to see dispatcher calls:
cd worker
npm run tailLook for:
"Using dispatcher for sessionId: XXX"- Dispatcher enabled"Dispatcher error:"- Dispatcher RPC failures"Error dispatching transcription:"- Parse or call errors
The dispatcher integration is implemented in worker/index.ts with WebSocket interception, making it:
- More maintainable (separated concerns)
- More flexible (easy to add consumers)
- More testable (clear interfaces) Look for these log messages:
Connected to Dispatcher DO via WebSocket- WebSocket connection establishedDispatcher connection closed- Connection lostFailed to connect to Dispatcher DO- DO connection error
{ "durable_objects": { "bindings": [ { "name": "DISPATCHER_DO", "class_name": "TranscriptionDispatcherDO", "script_name": "your-dispatcher-worker" } ] }, "vars": { "USE_DISPATCHER": "true" } }