Looking for the chatbot template? It's now here.
GitHub

chat-state-cloudflare-do

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.

Installation

bash
npm install chat chat-state-cloudflare-do

Usage

typescript
import { 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 itexport { 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);  },};

Wrangler configuration

Add the Durable Object binding and migration to your wrangler.jsonc (recommended) or wrangler.toml:

wrangler.jsonc (recommended)

jsonc
{  "durable_objects": {    "bindings": [      { "name": "CHAT_STATE", "class_name": "ChatStateDO" }  },  "migrations": [    { "tag": "v1", "new_sqlite_classes": ["ChatStateDO"] }}

wrangler.toml

toml
[durable_objects]bindings = [  { name = "CHAT_STATE", class_name = "ChatStateDO" }[[migrations]]tag = "v1"new_sqlite_classes = ["ChatStateDO"]

Environment type

typescript
import type { ChatStateDO } from "chat-state-cloudflare-do";interface Env {  CHAT_STATE: DurableObjectNamespace<ChatStateDO>;}

Configuration

OptionTypeRequiredDefaultDescription
namespaceDurableObjectNamespace<ChatStateDO>YesDurable Object namespace binding from wrangler config
namestringNo"default"Name for the DO instance
shardKey(threadId: string) => stringNoFunction to derive a shard name from a thread ID
locationHintDurableObjectLocationHintNoLocation hint for DO placement

Sharding

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:

typescript
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.

StrategyshardKeyDOs 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

Architecture

The adapter uses a single Durable Object class (ChatStateDO) with three SQLite tables:

  • subscriptions — thread IDs the bot is subscribed to
  • locks — distributed locks with token-based ownership and TTL
  • cache — 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.

Features

  • 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)

Production recommendations

  • 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 locationHint to place the DO near your primary user base

Documentation

License

MIT