durable-objects
Durable Objects is a Cloudflare Workers skill for building stateful per-key actors where each instance maintains isolated in-memory state and persistent transactional KV storage across requests. Use this skill when implementing chat rooms, multiplayer spaces, rate limiters, long-running agents, or leaderboards that require state to survive and be accessible across multiple HTTP calls or WebSocket connections to the same key identifier.
git clone --depth 1 https://github.com/butterbase-ai/butterbase-skills /tmp/durable-objects && cp -r /tmp/durable-objects/skills/durable-objects ~/.claude/skills/durable-objectsSKILL.md
# Butterbase Durable Objects
Durable Objects (DOs) are **stateful per-key actors** running on Cloudflare Workers. Each instance has its own in-memory state and a built-in transactional KV store. Use one when state must survive across requests for a single room/user/agent. For stateless work, use a serverless function instead (`butterbase-skills:function-dev`).
One tool: **`manage_durable_objects`**.
---
## 1. The mental model
```
Class: ChatRoom (deployed once)
│
├── instance "lobby" ─► in-memory state + state.storage + WebSockets
├── instance "general" ─► separate state, separate sockets
└── instance "user-123" ─► separate again
Each URL https://<app>.butterbase.dev/_do/chat-room/<instance-id>
gets routed to the instance with that id. State is isolated per id.
```
A class is shared code; an **instance** is a unique key (`/lobby`, `/general`, `/user-123`). Different ids = different state. There is no shared cross-instance state.
---
## 2. Constraints (read these first)
- **One TypeScript file per class.** No npm imports. Only `import { ... } from 'cloudflare:workers'` is allowed.
- **Exactly one exported class.** `export class Foo { ... }` — no extra exports, no helpers re-exported.
- **PascalCase class name** in source; **kebab-case** for the URL name (e.g. `ChatRoom` ↔ `chat-room`).
- File size: ≤ 5 MB. Total of all DO classes per app: ≤ 10 MB compressed.
- ≤ 5 DO classes per app (v1).
- **No service bindings yet.** Functions reach DOs over HTTP, not via env binding.
- `state.storage` keys/values capped at 128 KB. Larger blobs → Butterbase Storage.
- **WebSockets need `access_mode: "public"`** because browsers can't send custom headers on WS upgrade. Validate auth tokens inside `fetch()` instead.
---
## 3. The class skeleton
```typescript
export class ChatRoom {
constructor(public state: DurableObjectState, public env: Env) {}
async fetch(req: Request): Promise<Response> {
if (req.headers.get("Upgrade") === "websocket") {
const pair = new WebSocketPair();
this.state.acceptWebSocket(pair[1]);
return new Response(null, { status: 101, webSocket: pair[0] });
}
if (req.method === "POST") {
// handle plain HTTP
}
return Response.json({ ok: true });
}
// Optional WebSocket lifecycle hooks — called by the runtime
async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {
if (typeof msg !== "string") return; // guard binary
for (const peer of this.state.getWebSockets()) {
try { peer.send(msg); } catch {}
}
}
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {}
async webSocketError(ws: WebSocket, err: Error) {}
}
```
Key APIs:
| API | Purpose |
|-----|---------|
| `state.storage.get/put/delete/deleteAll/list` | Async transactional KV store |
| `state.acceptWebSocket(ws)` | Hold a WS connection; runtime routes messages to `webSocketMessage` |
| `state.getWebSockets()` | All active WS connections for this instance |
| `new WebSocketPair()` | Returns `[client, server]` — return `client` to browser, accept `server` |
| `this.env.KEY` | Read DO env vars (set via `set_env`) |
---
## 4. Deploy
```js
manage_durable_objects({
app_id: "app_abc123",
action: "deploy",
name: "chat-room", // kebab-case URL name
code: "<single TypeScript file>",
access_mode: "authenticated" // "public" | "authenticated" (default) | "service_key"
})
// → { id, name, class_name, status: "READY", access_mode, last_deployed_at }
```
Re-deploying with the same `name` updates the class; old in-memory state is evicted on next request. Storage persists across redeploys (same instance id = same `state.storage`).
### Access modes
| Mode | Auth required |
|------|---------------|
| `public` | None — validate tokens inside `fetch()` if you need any |
| `authenticated` (default) | End-user JWT in `Authorization: Bearer <token>` |
| `service_key` | Butterbase service key — backend-to-backend |
> The dispatcher only checks header **shape**, not validity. For real auth on production DOs, validate the token inside `fetch()`.
---
## 5. Address an instance
```
https://<your-subdomain>.butterbase.dev/_do/<name>/<instance-id>
```
- `<name>` = kebab-case DO name from deploy
- `<instance-id>` = anything you choose (`/lobby`, `/user-123`, `/main`)
Both HTTP and WebSocket upgrade work on the same URL.
```js
// HTTP
fetch("https://app.butterbase.dev/_do/chat-room/lobby", {
method: "POST",
body: JSON.stringify({ user: "alice", text: "hi" })
});
// WebSocket
const ws = new WebSocket("wss://app.butterbase.dev/_do/chat-room/lobby");
```
Different instance ids → completely separate state. There is no shared global view; if you need one, build it yourself (e.g. a `/registry` instance that other instances report into).
---
## 6. Env vars
Env vars are app-wide across all DO classes. Setting one redeploys the DO Worker — existing in-memory state is evicted, active WS connections drop.
```js
manage_durable_objects({ app_id, action: "list_env" }) // keys only, never values
manage_durable_objects({ app_id, action: "set_env", key: "AI_API_KEY", value: "sk-..." })
manage_durable_objects({ app_id, action: "delete_env", key: "AI_API_KEY" })
```
- Keys must match `^[A-Z_][A-Z0-9_]*$` (UPPER_SNAKE).
- A key can't collide with a DO class binding (e.g. `chat-room` reserves `CHAT_ROOM`).
- Read in code as `this.env.KEY_NAME`.
---
## 7. Lifecycle, listing, deletion
```js
manage_durable_objects({ app_id, action: "list" })
manage_durable_objects({ app_id, action: "get", name: "chat-room" }) // includes full source + status + error_message
manage_durable_objects({ app_id, action: "delete", name: "chat-room" }) // IRREVERSIBLE: purges all instances + storage
manage_durable_objects({ app_id, action: "usage", name: "chat-room" }) // do_requests, do_cpu_ms (refreshed every 15 minClaude Code plugin for Butterbase — 30+ guided skills and auto-configured MCP for the AI-native backend-as-a-service.
Use when calling the app's AI gateway from agent tools — chat completions, embeddings, listing models, configuring defaults or BYOK, reading token/cost usage
Configure OAuth providers, auth hooks, JWT lifetimes, and service keys for a Butterbase app
Use when building a new Butterbase app from scratch, creating a full-stack application, or when the user asks to set up a complete backend with database, auth, and deployment
Use when users report access denied errors, see wrong data, RLS policies are not working, or when troubleshooting Row-Level Security issues in Butterbase
Deploy a frontend (React, Next.js, or static HTML) to a live URL on Butterbase
Develop, deploy, or debug a Butterbase serverless function
Stage 1 of the journey: concrete one-question-at-a-time idea brainstorm with inline Butterbase capability tagging.