Quick Start Guide

Get up and running with Verani in 5 minutes.

Step 1: Install

npm install verani @cloudflare/actors
# or
bun add verani @cloudflare/actors

Don't have a Cloudflare Worker project? Create one:

npm create cloudflare@latest my-verani-app
cd my-verani-app

Step 2: Create a Connection Handler

Create src/actors/connection.ts:

import { defineConnection, createConnectionHandler, createRoomHandler } from "verani";

// Define connection handler (one WebSocket per user)
const userConnection = defineConnection({
  name: "UserConnection",

  extractMeta(req) {
    const url = new URL(req.url);
    const userId = url.searchParams.get("userId") || crypto.randomUUID();
    return {
      userId,
      clientId: crypto.randomUUID(),
      channels: ["default"]
    };
  },

  async onConnect(ctx) {
    console.log(`User ${ctx.meta.userId} connected`);
    // Join chat room (persisted across hibernation)
    await ctx.actor.joinRoom("chat");
  },

  async onDisconnect(ctx) {
    console.log(`User ${ctx.meta.userId} disconnected`);
    // Room leave is handled automatically
  }
});

// Handle messages (socket.io-like)
userConnection.on("chat.message", async (ctx, data) => {
  // Broadcast to everyone in the chat room
  await ctx.emit.toRoom("chat").emit("chat.message", {
    from: ctx.meta.userId,
    text: data.text,
    timestamp: Date.now()
  });
});

// Export handlers
export const UserConnection = createConnectionHandler(userConnection);
export const ChatRoom = createRoomHandler({ name: "ChatRoom" });

At this point you have defined two different Durable Object classes:

  • UserConnection (ConnectionDO): one instance per user, owns that user's single WebSocket and any per-user state. It calls ctx.actor.joinRoom("chat") and uses ctx.emit.toRoom("chat") to talk to rooms.
  • ChatRoom (RoomDO): one instance per room name (for example, "chat"), owns no WebSockets at all. It keeps track of which users are in the room and fans out messages to their UserConnection instances via RPC.

Step 3: Export the DO Classes

Update src/index.ts:

import { UserConnection, ChatRoom } from "./actors/connection";

// Export Durable Object classes
export { UserConnection, ChatRoom };

// Route WebSocket connections
export default {
  async fetch(request: Request) {
    const url = new URL(request.url);

    if (url.pathname.startsWith("/ws")) {
      // Extract userId and route to user-specific DO
      const userId = url.searchParams.get("userId") || crypto.randomUUID();
      const stub = UserConnection.get(userId);
      return stub.fetch(request);
    }

    return new Response("Not Found", { status: 404 });
  }
};

Important:

  • Export names must match class_name in wrangler.jsonc for both UserConnection and ChatRoom.
  • UserConnection.get(userId) is what you use in fetch() to route each WebSocket upgrade to a per-user ConnectionDO.
  • ChatRoom is exported so Wrangler can bind it as a separate Durable Object; you typically don't call ChatRoom.get("chat") directly in app code. Instead, ctx.actor.joinRoom("chat") and ctx.emit.toRoom("chat") use the RoomDO under the hood to manage membership and broadcasting.

Step 4: Configure Wrangler

Update wrangler.jsonc:

{
  "name": "my-verani-app",
  "main": "src/index.ts",
  "compatibility_date": "2024-01-01",

  "durable_objects": {
    "bindings": [
      {
        "class_name": "UserConnection",
        "name": "CONNECTION_DO"
      },
      {
        "class_name": "ChatRoom",
        "name": "ROOM_DO"
      }
    ]
  },

  "migrations": [
    {
      "new_sqlite_classes": ["UserConnection", "ChatRoom"],
      "tag": "v1"
    }
  ]
}

Step 5: Build a Client

import { VeraniClient } from "verani";

const client = new VeraniClient(
  "wss://your-worker.dev/ws?userId=alice"
);

// Listen for messages
client.on("chat.message", (data) => {
  console.log(`${data.from}: ${data.text}`);
});

client.on("user.joined", (data) => {
  console.log(`User ${data.userId} joined`);
});

// Send messages
client.emit("chat.message", { text: "Hello!" });

// Wait for connection
await client.waitForConnection();

Step 6: Deploy

npx wrangler deploy

Your WebSocket endpoint: wss://my-verani-app.your-subdomain.workers.dev/ws

That's It!

You now have a working realtime chat app. Open multiple browser tabs and watch messages sync in real-time.

Next Steps

Key Concepts

  • ConnectionDO = A Durable Object that owns ONE WebSocket per user
  • RoomDO = A Durable Object that coordinates room membership and broadcasts without owning sockets (it talks to ConnectionDOs via RPC)
  • joinRoom() = Join a room (membership persisted across hibernation)
  • Emit = Send messages (ctx.emit.toRoom("chat").emit("event", data))
  • on() = Listen for events (connection.on("event", handler))

For more details, see the Concepts section.