Configuration Guide
How to configure Verani for Cloudflare Workers.
The Three-Way Match
These three must match for every Durable Object:
- Export name in
src/index.ts:export const UserConnection = ... - Class name in
wrangler.jsonc:"class_name": "UserConnection" - Migration in
wrangler.jsonc:"new_sqlite_classes": ["UserConnection"]
Basic Setup
Verani uses a per-connection architecture: each user gets their own ConnectionDO, and separate RoomDOs handle coordination. Both need explicit binding configuration.
1. Define Your Connection
// src/actors/connection.ts
import { defineConnection } from "verani";
const connection = defineConnection({
name: "UserConnection",
websocketPath: "/ws",
// Map logical room names → wrangler.jsonc binding names
rooms: {
chat: "ChatRoom",
},
// Binding name for this connection DO (must match wrangler.jsonc)
connectionBinding: "UserConnection",
extractMeta(req) {
const url = new URL(req.url);
return {
userId: url.searchParams.get("userId") ?? crypto.randomUUID(),
clientId: crypto.randomUUID(),
channels: ["default"],
};
},
async onConnect(ctx) {
await ctx.actor.joinRoom("chat");
ctx.emit("welcome", { message: "Connected!" });
},
});
2. Export Handlers
// src/index.ts
import { createConnectionHandler, createRoomHandler } from "verani";
import { connection } from "./actors/connection";
export const UserConnection = createConnectionHandler(connection);
export const ChatRoom = createRoomHandler({
name: "ChatRoom",
connectionBinding: "UserConnection",
});
3. Configure Wrangler
// wrangler.jsonc
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2024-01-01",
"durable_objects": {
"bindings": [
{ "class_name": "UserConnection", "name": "UserConnection" },
{ "class_name": "ChatRoom", "name": "ChatRoom" }
]
},
"migrations": [
{
"new_sqlite_classes": ["UserConnection", "ChatRoom"],
"tag": "v1"
}
]
}
4. Route WebSocket Connections
// src/index.ts
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/ws")) {
const userId = url.searchParams.get("userId") ?? crypto.randomUUID();
const id = env.UserConnection.idFromName(userId);
const stub = env.UserConnection.get(id);
return stub.fetch(request);
}
return new Response("Not Found", { status: 404 });
}
};
Binding Configuration
Verani requires explicit binding names so ConnectionDOs and RoomDOs can find each other via RPC.
rooms (ConnectionDefinition)
Maps logical room names to wrangler.jsonc binding names:
defineConnection({
rooms: {
presence: "PresenceRoom", // "presence" → env.PresenceRoom
chat: "ChatRoom", // "chat" → env.ChatRoom
},
});
When you call ctx.actor.joinRoom("presence"), Verani looks up "PresenceRoom" from this map and uses env.PresenceRoom to reach the RoomDO.
You can also use namespace prefixes for dynamic room names. Verani checks for an exact room key first, then falls back to the prefix before :.
defineConnection({
rooms: {
conversation: "ChatRoom",
"conversation:staff": "StaffRoom",
},
});
await ctx.actor.joinRoom("conversation:123"); // uses ChatRoom
await ctx.actor.joinRoom("conversation:staff"); // uses StaffRoom (exact match wins)
await ctx.emit.toRoom("conversation:456").emit("chat.message", data);
Use this pattern for room families like conversation:123, game:abc, or document:xyz.
connectionBinding (both Connection and Room)
Tells DOs how to find the ConnectionDO binding:
// Connection side
defineConnection({ connectionBinding: "UserConnection" });
// Room side — needed so RoomDOs can deliver messages back to connections
createRoomHandler({ connectionBinding: "UserConnection" });
Multiple Rooms
// src/index.ts
export const UserConnection = createConnectionHandler(connection);
export const ChatRoom = createRoomHandler({
name: "ChatRoom",
connectionBinding: "UserConnection",
});
export const PresenceRoom = createRoomHandler({
name: "PresenceRoom",
connectionBinding: "UserConnection",
});
{
"durable_objects": {
"bindings": [
{ "class_name": "UserConnection", "name": "UserConnection" },
{ "class_name": "ChatRoom", "name": "ChatRoom" },
{ "class_name": "PresenceRoom", "name": "PresenceRoom" }
]
},
"migrations": [
{
"new_sqlite_classes": ["UserConnection", "ChatRoom", "PresenceRoom"],
"tag": "v1"
}
]
}
Actor Routing
Choose which Actor instance handles requests by picking the Actor ID:
// Per-user (recommended — each user gets their own ConnectionDO)
const id = env.UserConnection.idFromName(userId);
env.UserConnection.get(id);
// Room-based (each room gets its own RoomDO)
const id = env.ChatRoom.idFromName(`room:${roomId}`);
env.ChatRoom.get(id);
Common Errors
"no such Durable Object class is exported"
- Fix: Export name doesn't match
class_namein wrangler.jsonc
"Cannot find name 'UserConnection'"
- Fix: Missing export:
export { UserConnection };
"RoomDO binding not found"
- Fix:
roomsmap indefineConnectiondoesn't include the room name or namespace prefix, or the binding name doesn't match wrangler.jsonc
"Connection binding not found"
- Fix:
connectionBindingdoesn't match the binding name in wrangler.jsonc