Skip to content
cable
Esc
navigateopen⌘Jpreview
On this page

Cloudflare Durable Objects

Run each cable channel host in a Cloudflare Durable Object with hibernatable WebSockets, durable storage, edge authentication, and signed private grants.

@cablejs/cloudflare runs one cable channel host in a Durable Object. Export the generated class from the Worker module, bind its class name in Wrangler, and register that namespace in the edge handler.

import { cloudflareHost, createHandler, type CloudflareHostInstance } from "@cablejs/cloudflare";

interface Env {
  readonly CHAT_HOSTS: DurableObjectNamespace<CloudflareHostInstance<Env>>;
  readonly CABLE_GRANT_SECRET: string;
}

export const ChatHost = cloudflareHost(api.chat, chatImplementation, {
  grantSecret: (env: Env) => env.CABLE_GRANT_SECRET,
  peer: (env: Env, key) => env.CHAT_HOSTS.getByName(key),
});

const handler = createHandler(api, procedures, {
  authenticate: (request, env) => authenticate(request, env),
  context: ({ identity, hosts }) => ({ identity, hosts }),
  credentials: { mode: "bearer" },
  grantSecret: (env) => env.CABLE_GRANT_SECRET,
  grants: (identity) => grantsFor(identity),
  hosts: (env) => [{ channel: api.chat, namespace: env.CHAT_HOSTS }],
  uid: (identity) => identity.userId,
});

export default { fetch: handler.fetch };
{
  "durable_objects": { "bindings": [{ "name": "CHAT_HOSTS", "class_name": "ChatHost" }] },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatHost"] }],
}

class_name must equal the exported class name. Add a migration when introducing a class. Do not rename or remove a deployed class without Cloudflare’s migration process.

Request flow

The edge handler authenticates RPC, host-fallback, and WebSocket requests. For an accepted upgrade, it strips caller credentials, signs a private grant, and forwards the request to the Durable Object named for the channel key. The host verifies that grant before accepting the socket. grantSecret must return the same secret in both places.

In cookie mode, set credentials: { mode: "cookie", origins: ["https://app.example.com"] } and authenticate the session cookie. In bearer mode, set credentials: { mode: "bearer" } and validate the Authorization header or browser token. grants derives short-lived channel capabilities. It does not replace a channel’s authorize check.

The adapter uses hibernatable WebSockets, serialized attachments, Durable Object storage and alarms, and RPC for peer calls. Keep application state in context.storage, not object fields, because hibernation creates a new engine instance.

Run the example

The local example needs both the Worker and Vite processes. Create the Worker secret and start Wrangler in one terminal:

(cd examples/chat-cloudflare && cp .dev.vars.example .dev.vars)
(cd examples/chat-cloudflare && bunx wrangler dev --local)

Set CABLE_GRANT_SECRET in examples/chat-cloudflare/.dev.vars to at least 32 random characters. Then start Vite in a second terminal:

bun --filter @cablejs/example-chat-cloudflare dev

The local browser identity is only a development bearer token. Replace it with application authentication before deployment. The contract, Worker, and client code live in examples/chat-cloudflare/src/.

Was this page helpful?