Durable Objects API
The @cloudwerk/durable-object package provides a convention-based system for creating Durable Objects with native RPC methods, SQLite/KV storage, WebSocket support, and alarms.
Installation
Section titled “Installation”pnpm add @cloudwerk/durable-objectdefineDurableObject()
Section titled “defineDurableObject()”Creates a Durable Object definition with typed state and methods.
import { defineDurableObject } from '@cloudwerk/durable-object'
export default defineDurableObject<TState>({ init?: (ctx: DurableObjectContext) => Awaitable<TState>, methods?: Record<string, DurableObjectMethod>, fetch?: (request: Request, ctx: DurableObjectContext) => Awaitable<Response>, webSocketMessage?: (ws: WebSocket, message: string | ArrayBuffer, ctx: DurableObjectContext) => Awaitable<void>, webSocketClose?: (ws: WebSocket, code: number, reason: string, ctx: DurableObjectContext) => Awaitable<void>, webSocketError?: (ws: WebSocket, error: Error, ctx: DurableObjectContext) => Awaitable<void>, alarm?: (ctx: DurableObjectContext) => Awaitable<void>,})Type Parameter
Section titled “Type Parameter”The generic type TState defines the shape of your Durable Object’s state.
interface CounterState { count: number lastUpdated: Date}
export default defineDurableObject<CounterState>({ // ...})Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
init | Function | Initialize state when DO is created |
methods | Record<string, Function> | RPC methods exposed to callers |
fetch | Function | Handle HTTP requests |
webSocketMessage | Function | Handle WebSocket messages |
webSocketClose | Function | Handle WebSocket close events |
webSocketError | Function | Handle WebSocket errors |
alarm | Function | Handle scheduled alarms |
DurableObjectContext
Section titled “DurableObjectContext”The context object passed to all handlers.
interface DurableObjectContext<TState = unknown> { // State management state: TState setState(updates: Partial<TState>): void getState(): TState
// Storage storage: DurableObjectStorage sql: SqlStorage
// WebSocket acceptWebSocket(ws: WebSocket, tags?: string[]): void getWebSockets(tag?: string): WebSocket[]
// Alarms setAlarm(time: Date | number): Promise<void> deleteAlarm(): Promise<void> getAlarm(): Promise<number | null>
// Environment env: Env
// Execution waitUntil(promise: Promise<unknown>): void blockConcurrencyWhile<T>(fn: () => Promise<T>): Promise<T>}Init Handler
Section titled “Init Handler”Initialize state when the Durable Object is first created.
export default defineDurableObject<CounterState>({ async init(ctx) { // Load from storage or return defaults const stored = await ctx.storage.get<CounterState>('state') return stored ?? { count: 0, lastUpdated: new Date() } },})RPC Methods
Section titled “RPC Methods”Methods are exposed as native Cloudflare RPC methods.
export default defineDurableObject<CounterState>({ init: async () => ({ count: 0, lastUpdated: new Date() }),
methods: { async increment(amount: number = 1) { this.state.count += amount this.state.lastUpdated = new Date() await this.ctx.storage.put('state', this.state) return this.state.count },
async decrement(amount: number = 1) { this.state.count -= amount this.state.lastUpdated = new Date() await this.ctx.storage.put('state', this.state) return this.state.count },
async getCount() { return this.state.count },
async reset() { this.state.count = 0 this.state.lastUpdated = new Date() await this.ctx.storage.put('state', this.state) return 0 }, },})Calling RPC Methods
Section titled “Calling RPC Methods”import { durableObjects } from '@cloudwerk/core/bindings'
// Get stub by nameconst counter = durableObjects.counter.get('user-123')
// Call methods directly (native RPC)const count = await counter.increment(5)const current = await counter.getCount()await counter.reset()Storage API
Section titled “Storage API”Key-Value Storage
Section titled “Key-Value Storage”interface DurableObjectStorage { // Single operations get<T>(key: string): Promise<T | undefined> put<T>(key: string, value: T): Promise<void> delete(key: string): Promise<boolean>
// Batch operations get<T>(keys: string[]): Promise<Map<string, T>> put(entries: Record<string, unknown>): Promise<void> delete(keys: string[]): Promise<number>
// List list<T>(options?: StorageListOptions): Promise<Map<string, T>>
// Transactions transaction<T>(fn: () => Promise<T>): Promise<T>
// Sync sync(): Promise<void>}
interface StorageListOptions { prefix?: string start?: string end?: string limit?: number reverse?: boolean}Storage Examples
Section titled “Storage Examples”methods: { async saveUser(ctx, user: User) { await ctx.storage.put(`user:${user.id}`, user) },
async getUser(ctx, id: string) { return ctx.storage.get<User>(`user:${id}`) },
async getAllUsers(ctx) { const users = await ctx.storage.list<User>({ prefix: 'user:' }) return Array.from(users.values()) },
async deleteUser(ctx, id: string) { return ctx.storage.delete(`user:${id}`) },
async batchSave(ctx, users: User[]) { const entries = Object.fromEntries( users.map(u => [`user:${u.id}`, u]) ) await ctx.storage.put(entries) },}SQL Storage
Section titled “SQL Storage”Durable Objects support SQLite for relational data.
interface SqlStorage { exec(query: string, ...bindings: unknown[]): SqlStorageResult prepare(query: string): SqlStatement}
interface SqlStatement { bind(...values: unknown[]): SqlStatement first<T>(): T | null all<T>(): T[] run(): SqlRunResult}
interface SqlRunResult { changes: number lastRowId: number}SQL Examples
Section titled “SQL Examples”export default defineDurableObject({ async init(ctx) { // Create tables on init ctx.sql.exec(` CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) `) return { initialized: true } },
methods: { async addMessage(ctx, userId: string, content: string) { const result = ctx.sql.exec( 'INSERT INTO messages (user_id, content) VALUES (?, ?)', userId, content ) return result.lastRowId },
async getMessages(ctx, userId: string, limit: number = 50) { return ctx.sql .prepare('SELECT * FROM messages WHERE user_id = ? ORDER BY created_at DESC LIMIT ?') .bind(userId, limit) .all() },
async deleteMessage(ctx, id: number) { const result = ctx.sql.exec('DELETE FROM messages WHERE id = ?', id) return result.changes > 0 }, },})Transactions
Section titled “Transactions”Ensure atomicity for multiple operations.
methods: { async transfer(ctx, fromId: string, toId: string, amount: number) { await ctx.storage.transaction(async () => { const from = await ctx.storage.get<Account>(`account:${fromId}`) const to = await ctx.storage.get<Account>(`account:${toId}`)
if (!from || !to) throw new Error('Account not found') if (from.balance < amount) throw new Error('Insufficient funds')
from.balance -= amount to.balance += amount
await ctx.storage.put({ [`account:${fromId}`]: from, [`account:${toId}`]: to, }) }) },}Fetch Handler
Section titled “Fetch Handler”Handle raw HTTP requests to the Durable Object.
export default defineDurableObject({ async fetch(request, ctx) { const url = new URL(request.url)
if (url.pathname === '/ws') { // Upgrade to WebSocket const pair = new WebSocketPair() ctx.acceptWebSocket(pair[1]) return new Response(null, { status: 101, webSocket: pair[0] }) }
if (url.pathname === '/state') { return Response.json(ctx.state) }
return new Response('Not found', { status: 404 }) },})WebSocket Handlers
Section titled “WebSocket Handlers”Handle WebSocket connections for real-time features.
interface ChatState { messages: Message[]}
export default defineDurableObject<ChatState>({ init: async () => ({ messages: [] }),
async fetch(request, ctx) { const url = new URL(request.url)
if (url.pathname === '/ws') { const pair = new WebSocketPair() const userId = url.searchParams.get('userId')
// Accept with tags for filtering ctx.acceptWebSocket(pair[1], [userId, 'all'])
return new Response(null, { status: 101, webSocket: pair[0] }) }
return new Response('Not found', { status: 404 }) },
async webSocketMessage(ws, message, ctx) { const data = JSON.parse(message as string)
if (data.type === 'chat') { const msg = { id: crypto.randomUUID(), userId: data.userId, content: data.content, timestamp: new Date(), }
ctx.state.messages.push(msg) await ctx.storage.put('messages', ctx.state.messages)
// Broadcast to all connected clients for (const client of ctx.getWebSockets('all')) { client.send(JSON.stringify({ type: 'message', data: msg })) } } },
async webSocketClose(ws, code, reason, ctx) { console.log(`WebSocket closed: ${code} ${reason}`) },
async webSocketError(ws, error, ctx) { console.error('WebSocket error:', error) },})WebSocket Methods
Section titled “WebSocket Methods”| Method | Description |
|---|---|
ctx.acceptWebSocket(ws, tags?) | Accept WebSocket with optional tags |
ctx.getWebSockets(tag?) | Get connected WebSockets, optionally filtered by tag |
Alarms
Section titled “Alarms”Schedule periodic tasks within the Durable Object.
export default defineDurableObject({ async init(ctx) { // Schedule first alarm await ctx.setAlarm(Date.now() + 60_000) // 1 minute return { processedCount: 0 } },
async alarm(ctx) { // Process pending work await processPendingTasks(ctx)
ctx.setState({ processedCount: ctx.state.processedCount + 1, })
// Schedule next alarm await ctx.setAlarm(Date.now() + 60_000) },})Alarm Methods
Section titled “Alarm Methods”| Method | Description |
|---|---|
ctx.setAlarm(time) | Schedule alarm (Date or timestamp) |
ctx.deleteAlarm() | Cancel scheduled alarm |
ctx.getAlarm() | Get scheduled alarm time (null if none) |
Accessing Durable Objects
Section titled “Accessing Durable Objects”durableObjects Proxy
Section titled “durableObjects Proxy”import { durableObjects } from '@cloudwerk/core/bindings'
// Get by name (deterministic ID)const counter = durableObjects.counter.get('user-123')
// Get by unique IDconst uniqueId = durableObjects.counter.newUniqueId()const counter = durableObjects.counter.getById(uniqueId)
// Call RPC methodsconst count = await counter.increment(5)getDurableObject()
Section titled “getDurableObject()”Get a typed Durable Object namespace.
import { getDurableObject } from '@cloudwerk/core/bindings'
interface CounterDO { increment(amount?: number): Promise<number> getCount(): Promise<number>}
const counterNs = getDurableObject<CounterDO>('counter')const counter = counterNs.get('user-123')const count = await counter.increment(5)hasDurableObject()
Section titled “hasDurableObject()”Check if a Durable Object namespace exists.
import { hasDurableObject } from '@cloudwerk/core/bindings'
if (hasDurableObject('counter')) { const counter = durableObjects.counter.get('user-123')}Error Handling
Section titled “Error Handling”DurableObjectError
Section titled “DurableObjectError”Base error class for Durable Object failures.
import { DurableObjectError } from '@cloudwerk/durable-object'
class DurableObjectError extends Error { readonly code: string readonly objectName: string readonly objectId?: string}DurableObjectNotFoundError
Section titled “DurableObjectNotFoundError”Thrown when accessing a non-existent Durable Object.
import { DurableObjectNotFoundError } from '@cloudwerk/durable-object'DurableObjectMethodError
Section titled “DurableObjectMethodError”Thrown when an RPC method fails.
import { DurableObjectMethodError } from '@cloudwerk/durable-object'
try { await counter.increment(-1)} catch (error) { if (error instanceof DurableObjectMethodError) { console.error('Method failed:', error.methodName, error.cause) }}CLI Commands
Section titled “CLI Commands”List Durable Objects
Section titled “List Durable Objects”cloudwerk objects listShow Object Details
Section titled “Show Object Details”cloudwerk objects info <name>Generate Types
Section titled “Generate Types”cloudwerk objects generate-typesType Definitions
Section titled “Type Definitions”DurableObjectDefinition
Section titled “DurableObjectDefinition”interface DurableObjectDefinition<TState = unknown> { init?: (ctx: DurableObjectContext) => Awaitable<TState> methods?: Record<string, DurableObjectMethod<TState>> fetch?: (request: Request, ctx: DurableObjectContext<TState>) => Awaitable<Response> webSocketMessage?: WebSocketMessageHandler<TState> webSocketClose?: WebSocketCloseHandler<TState> webSocketError?: WebSocketErrorHandler<TState> alarm?: (ctx: DurableObjectContext<TState>) => Awaitable<void>}
type DurableObjectMethod<TState> = ( ctx: DurableObjectContext<TState>, ...args: any[]) => Awaitable<any>Generated Class
Section titled “Generated Class”Cloudwerk generates a DurableObject class from your definition:
// Auto-generatedexport class Counter extends DurableObject { async increment(amount?: number): Promise<number> { /* ... */ } async getCount(): Promise<number> { /* ... */ } async reset(): Promise<number> { /* ... */ }}Wrangler Configuration
Section titled “Wrangler Configuration”Cloudwerk auto-generates wrangler configuration:
# wrangler.toml (auto-generated)[durable_objects]bindings = [ { name = "COUNTER", class_name = "Counter" }]
[[migrations]]tag = "v1"new_classes = ["Counter"]Best Practices
Section titled “Best Practices”State Design
Section titled “State Design”// Good: Minimal state, load on demandinterface GameState { id: string status: 'waiting' | 'playing' | 'finished'}
export default defineDurableObject<GameState>({ async init(ctx) { return await ctx.storage.get('state') ?? { id: '', status: 'waiting' } },
methods: { async getFullState(ctx) { // Load additional data on demand const players = await ctx.storage.list({ prefix: 'player:' }) return { ...ctx.state, players: Array.from(players.values()) } }, },})Use Transactions for Consistency
Section titled “Use Transactions for Consistency”methods: { async atomicUpdate(ctx, updates: Partial<State>) { await ctx.storage.transaction(async () => { const current = await ctx.storage.get('state') await ctx.storage.put('state', { ...current, ...updates }) await ctx.storage.put('lastUpdate', new Date().toISOString()) }) },}Cleanup with Alarms
Section titled “Cleanup with Alarms”async alarm(ctx) { // Clean up stale data const messages = await ctx.storage.list<Message>({ prefix: 'msg:' }) const staleThreshold = Date.now() - 24 * 60 * 60 * 1000 // 24 hours
const staleKeys = [] for (const [key, msg] of messages) { if (new Date(msg.timestamp).getTime() < staleThreshold) { staleKeys.push(key) } }
if (staleKeys.length > 0) { await ctx.storage.delete(staleKeys) }
// Schedule next cleanup await ctx.setAlarm(Date.now() + 60 * 60 * 1000) // 1 hour}Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Storage per object | 10 GB |
| KV operations per request | 1000 |
| SQL rows per table | Unlimited |
| WebSocket connections | 32,768 per object |
| Alarm precision | 1 second |
Next Steps
Section titled “Next Steps”- Durable Objects Guide - Patterns and best practices
- Services API - Service extraction
- WebSockets Guide - Real-time features