Cloudflare Durable Objects state adapter for Chat SDK. Uses a SQLite-backed Durable Object for persistent subscriptions, distributed locking, and caching — with zero external dependencies beyond the Workers runtime.
npm install chat chat-state-cloudflare-doimport { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createCloudflareState, ChatStateDO } from "chat-state-cloudflare-do";
// Re-export the Durable Object class so Cloudflare can find it
export { ChatStateDO };
export default {
async fetch(request: Request, env: Env) {
const bot = new Chat({
userName: "my-bot",
adapters: { slack: createSlackAdapter() },
state: createCloudflareState({ namespace: env.CHAT_STATE }),
});
return bot.webhooks.slack(request);
},
};Add the Durable Object binding and migration to your wrangler.jsonc (recommended) or wrangler.toml:
wrangler.jsonc (recommended)
wrangler.toml
[durable_objects]
bindings = [
{ name = "CHAT_STATE", class_name = "ChatStateDO" }
]
[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatStateDO"]import type { ChatStateDO } from "chat-state-cloudflare-do";
interface Env {
CHAT_STATE: DurableObjectNamespace<ChatStateDO>;
}| Option | Type | Required | Default | Description |
|---|---|---|---|---|
namespace |
DurableObjectNamespace<ChatStateDO> |
Yes | — | Durable Object namespace binding from wrangler config |
name |
string |
No | "default" |
Name for the DO instance |
shardKey |
(threadId: string) => string |
No | — | Function to derive a shard name from a thread ID |
locationHint |
DurableObjectLocationHint |
No | — | Location hint for DO placement |
A single Durable Object handles approximately 500-1,000 requests per second. For high-traffic bots, use shardKey to distribute load across multiple DO instances:
const state = createCloudflareState({
namespace: env.CHAT_STATE,
shardKey: (threadId) => threadId.split(":")[0], // One DO per platform
});Locks and subscriptions are per-thread, so sharding by any prefix of the thread ID is safe. Cache operations (get/set/delete) always route to the default shard since their keys are not thread-scoped.
| Strategy | shardKey |
DOs created |
|---|---|---|
| No sharding (default) | — | 1 |
| Per platform | (id) => id.split(":")[0] |
1 per platform |
| Per channel | (id) => id.split(":").slice(0, 2).join(":") |
1 per channel |
The adapter uses a single Durable Object class (ChatStateDO) with three SQLite tables:
subscriptions— thread IDs the bot is subscribed tolocks— distributed locks with token-based ownership and TTLcache— key-value pairs with optional TTL
All operations are single-threaded within a DO instance, providing distributed locking via DO atomicity rather than Lua scripts. Expired entries are cleaned up automatically via the Alarms API.
Each method call creates a fresh DO stub. Stubs are cheap (just a JS object) and the Cloudflare docs recommend creating new stubs rather than reusing them after errors.
- Persistent subscriptions across deployments
- Distributed locking via single-threaded DO atomicity
- Key-value caching with TTL
- Automatic TTL cleanup via Alarms
- Optional sharding for high-traffic bots
- Location hints for latency optimization
- Zero external dependencies (no Redis, no database)
- Use Smart Placement to co-locate your Worker with the DO
- Monitor DO metrics in the Cloudflare dashboard
- Enable sharding if you expect >500 req/s to a single DO instance
- Use
locationHintto place the DO near your primary user base
MIT
{ "durable_objects": { "bindings": [ { "name": "CHAT_STATE", "class_name": "ChatStateDO" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["ChatStateDO"] } ] }