Skip to content

Commit 44886e2

Browse files
committed
docs: add Durable Objects guide
Covers config, SQLite storage, WebSockets, Agents SDK, and what jack auto-fixes at deploy time.
1 parent f8c1c78 commit 44886e2

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# Durable Objects
2+
3+
Durable Objects give your worker persistent state and real-time coordination — think chat rooms, game sessions, collaborative editing, or any stateful backend that needs to survive between requests.
4+
5+
Each Durable Object instance has its own SQLite database and runs in a single location, so you get strong consistency without managing infrastructure.
6+
7+
## Quick Start
8+
9+
Add a Durable Object to your `wrangler.jsonc`:
10+
11+
```jsonc
12+
{
13+
"compatibility_flags": ["nodejs_compat"],
14+
"durable_objects": {
15+
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
16+
},
17+
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }]
18+
}
19+
```
20+
21+
Create and export your class:
22+
23+
```typescript
24+
// src/counter.ts
25+
import { DurableObject } from "cloudflare:workers";
26+
27+
export class Counter extends DurableObject<Env> {
28+
async fetch(request: Request): Promise<Response> {
29+
const count = (this.ctx.storage.sql
30+
.exec("SELECT count FROM hits LIMIT 1")
31+
.one()?.count as number) ?? 0;
32+
33+
this.ctx.storage.sql.exec(
34+
"CREATE TABLE IF NOT EXISTS hits (count INTEGER)"
35+
);
36+
this.ctx.storage.sql.exec(
37+
"INSERT OR REPLACE INTO hits (rowid, count) VALUES (1, ?)",
38+
count + 1
39+
);
40+
41+
return Response.json({ count: count + 1 });
42+
}
43+
}
44+
```
45+
46+
Re-export it from your entrypoint:
47+
48+
```typescript
49+
// src/index.ts
50+
export { Counter } from "./counter";
51+
52+
interface Env {
53+
COUNTER: DurableObjectNamespace;
54+
}
55+
56+
export default {
57+
async fetch(request: Request, env: Env) {
58+
const id = env.COUNTER.idFromName("global");
59+
const stub = env.COUNTER.get(id);
60+
return stub.fetch(request);
61+
},
62+
};
63+
```
64+
65+
Deploy:
66+
67+
```bash
68+
jack ship
69+
```
70+
71+
## Configuration
72+
73+
### Bindings
74+
75+
Each Durable Object class needs a binding in `wrangler.jsonc`:
76+
77+
```jsonc
78+
{
79+
"durable_objects": {
80+
"bindings": [
81+
{ "name": "COUNTER", "class_name": "Counter" },
82+
{ "name": "ROOM", "class_name": "ChatRoom" }
83+
]
84+
}
85+
}
86+
```
87+
88+
- `name` — the variable name in your `Env` type
89+
- `class_name` — must match the exported class name exactly
90+
91+
### Migrations
92+
93+
Durable Objects with SQLite storage need a migration entry:
94+
95+
```jsonc
96+
{
97+
"migrations": [
98+
{ "tag": "v1", "new_sqlite_classes": ["Counter", "ChatRoom"] }
99+
]
100+
}
101+
```
102+
103+
When you add a new class later, add a new migration step:
104+
105+
```jsonc
106+
{
107+
"migrations": [
108+
{ "tag": "v1", "new_sqlite_classes": ["Counter", "ChatRoom"] },
109+
{ "tag": "v2", "new_sqlite_classes": ["GameSession"] }
110+
]
111+
}
112+
```
113+
114+
Don't edit existing migration entries — always append.
115+
116+
### Compatibility Flags
117+
118+
Durable Objects require the `nodejs_compat` flag:
119+
120+
```jsonc
121+
{
122+
"compatibility_flags": ["nodejs_compat"]
123+
}
124+
```
125+
126+
Jack auto-adds this if you forget, but it's better to be explicit.
127+
128+
## Usage Patterns
129+
130+
### Named instances
131+
132+
Use `idFromName()` when you want a predictable, shared instance:
133+
134+
```typescript
135+
// Everyone hitting /room/lobby gets the same DO
136+
const id = env.ROOM.idFromName("lobby");
137+
const room = env.ROOM.get(id);
138+
```
139+
140+
### Unique instances
141+
142+
Use `newUniqueId()` for per-session or per-user state:
143+
144+
```typescript
145+
const id = env.SESSION.newUniqueId();
146+
const session = env.SESSION.get(id);
147+
148+
// Store the ID somewhere so you can reconnect
149+
const sessionId = id.toString();
150+
```
151+
152+
### SQLite storage
153+
154+
Every DO instance gets its own SQLite database via `this.ctx.storage.sql`:
155+
156+
```typescript
157+
export class Notes extends DurableObject<Env> {
158+
async fetch(request: Request): Promise<Response> {
159+
if (request.method === "POST") {
160+
const { title, body } = await request.json();
161+
this.ctx.storage.sql.exec(
162+
"CREATE TABLE IF NOT EXISTS notes (title TEXT, body TEXT, created_at TEXT)"
163+
);
164+
this.ctx.storage.sql.exec(
165+
"INSERT INTO notes (title, body, created_at) VALUES (?, ?, datetime('now'))",
166+
title, body
167+
);
168+
return Response.json({ ok: true });
169+
}
170+
171+
const notes = this.ctx.storage.sql
172+
.exec("SELECT * FROM notes ORDER BY created_at DESC")
173+
.toArray();
174+
return Response.json(notes);
175+
}
176+
}
177+
```
178+
179+
### WebSockets
180+
181+
Durable Objects are the standard way to handle WebSockets:
182+
183+
```typescript
184+
export class ChatRoom extends DurableObject<Env> {
185+
async fetch(request: Request): Promise<Response> {
186+
if (request.headers.get("Upgrade") === "websocket") {
187+
const [client, server] = Object.values(new WebSocketPair());
188+
this.ctx.acceptWebSocket(server);
189+
return new Response(null, { status: 101, webSocket: client });
190+
}
191+
return new Response("Expected WebSocket", { status: 400 });
192+
}
193+
194+
async webSocketMessage(ws: WebSocket, message: string) {
195+
// Broadcast to all connected clients
196+
for (const client of this.ctx.getWebSockets()) {
197+
if (client !== ws) {
198+
client.send(message);
199+
}
200+
}
201+
}
202+
203+
async webSocketClose(ws: WebSocket) {
204+
ws.close();
205+
}
206+
}
207+
```
208+
209+
## Agents SDK
210+
211+
For AI chat applications, the [Agents SDK](https://developers.cloudflare.com/agents/) provides `AIChatAgent` — a higher-level abstraction built on Durable Objects that handles message history, real-time sync, and streaming out of the box.
212+
213+
The **ai-chat** template uses this pattern:
214+
215+
```bash
216+
jack new my-chat -t ai-chat
217+
```
218+
219+
```typescript
220+
import { AIChatAgent } from "@cloudflare/ai-chat";
221+
import { streamText } from "ai";
222+
223+
export class Chat extends AIChatAgent<Env> {
224+
async onChatMessage(onFinish) {
225+
const result = streamText({
226+
model: provider("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
227+
messages: await convertToModelMessages(this.messages),
228+
onFinish,
229+
});
230+
return result.toUIMessageStreamResponse();
231+
}
232+
}
233+
```
234+
235+
Rooms, persistence, and WebSocket sync are all handled automatically.
236+
237+
## What Jack Does For You
238+
239+
When you run `jack ship` with Durable Objects configured:
240+
241+
1. **Auto-fixes prerequisites** — adds `nodejs_compat` flag and migration entries if missing
242+
2. **Validates exports** — checks your built code actually exports the declared classes (catches typos before deploy)
243+
3. **Handles deployment** — configures dispatch hints and DO bindings on the Cloudflare side
244+
4. **Tracks resources** — registers your DOs so they show up in `jack info`
245+
246+
You don't need a Cloudflare account or API tokens — jack cloud manages everything.
247+
248+
## Local Development
249+
250+
For local development with `wrangler dev`, Durable Objects work out of the box — no extra setup needed. State is stored in `.wrangler/state/`.
251+
252+
```bash
253+
wrangler dev
254+
```
255+
256+
## Resources
257+
258+
- [Cloudflare Durable Objects Docs](https://developers.cloudflare.com/durable-objects/)
259+
- [Agents SDK](https://developers.cloudflare.com/agents/)
260+
- [SQLite in Durable Objects](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/)

vocs.config.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export default defineConfig({
7070
items: [
7171
{ text: "OpenClaw / Claude Code", link: "/guides/openclaw" },
7272
{ text: "AI & Vectorize Bindings", link: "/guides/ai-vectorize" },
73+
{ text: "Durable Objects", link: "/guides/durable-objects" },
7374
{ text: "Using with AI Agents", link: "/guides/ai-agents" },
7475
{ text: "Troubleshooting", link: "/guides/troubleshooting" },
7576
],

0 commit comments

Comments
 (0)