Skip to content

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.

Terminal window
pnpm add @cloudwerk/durable-object

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>,
})

The generic type TState defines the shape of your Durable Object’s state.

interface CounterState {
count: number
lastUpdated: Date
}
export default defineDurableObject<CounterState>({
// ...
})
ParameterTypeDescription
initFunctionInitialize state when DO is created
methodsRecord<string, Function>RPC methods exposed to callers
fetchFunctionHandle HTTP requests
webSocketMessageFunctionHandle WebSocket messages
webSocketCloseFunctionHandle WebSocket close events
webSocketErrorFunctionHandle WebSocket errors
alarmFunctionHandle scheduled alarms

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>
}

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() }
},
})

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
},
},
})
import { durableObjects } from '@cloudwerk/core/bindings'
// Get stub by name
const 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()

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
}
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)
},
}

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
}
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
},
},
})

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,
})
})
},
}

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 })
},
})

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)
},
})
MethodDescription
ctx.acceptWebSocket(ws, tags?)Accept WebSocket with optional tags
ctx.getWebSockets(tag?)Get connected WebSockets, optionally filtered by tag

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)
},
})
MethodDescription
ctx.setAlarm(time)Schedule alarm (Date or timestamp)
ctx.deleteAlarm()Cancel scheduled alarm
ctx.getAlarm()Get scheduled alarm time (null if none)

import { durableObjects } from '@cloudwerk/core/bindings'
// Get by name (deterministic ID)
const counter = durableObjects.counter.get('user-123')
// Get by unique ID
const uniqueId = durableObjects.counter.newUniqueId()
const counter = durableObjects.counter.getById(uniqueId)
// Call RPC methods
const count = await counter.increment(5)

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)

Check if a Durable Object namespace exists.

import { hasDurableObject } from '@cloudwerk/core/bindings'
if (hasDurableObject('counter')) {
const counter = durableObjects.counter.get('user-123')
}

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
}

Thrown when accessing a non-existent Durable Object.

import { DurableObjectNotFoundError } from '@cloudwerk/durable-object'

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

Terminal window
cloudwerk objects list
Terminal window
cloudwerk objects info <name>
Terminal window
cloudwerk objects generate-types

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>

Cloudwerk generates a DurableObject class from your definition:

// Auto-generated
export class Counter extends DurableObject {
async increment(amount?: number): Promise<number> { /* ... */ }
async getCount(): Promise<number> { /* ... */ }
async reset(): Promise<number> { /* ... */ }
}

Cloudwerk auto-generates wrangler configuration:

# wrangler.toml (auto-generated)
[durable_objects]
bindings = [
{ name = "COUNTER", class_name = "Counter" }
]
[[migrations]]
tag = "v1"
new_classes = ["Counter"]

// Good: Minimal state, load on demand
interface 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()) }
},
},
})
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())
})
},
}
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
}

LimitValue
Storage per object10 GB
KV operations per request1000
SQL rows per tableUnlimited
WebSocket connections32,768 per object
Alarm precision1 second