cloudflare-durable-objects

from jezweb/claude-skills

Skills for Claude Code CLI such as full stack dev Cloudflare, React, Tailwind v4, and AI integrations.

213 stars24 forksUpdated Jan 25, 2026
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-durable-objects

SKILL.md

Cloudflare Durable Objects

Status: Production Ready ✅ Last Updated: 2026-01-21 Dependencies: cloudflare-worker-base (recommended) Latest Versions: wrangler@4.58.0, @cloudflare/workers-types@4.20260109.0 Official Docs: https://developers.cloudflare.com/durable-objects/

Recent Updates (2025):

  • Oct 2025: WebSocket message size 1 MiB → 32 MiB, Data Studio UI for SQLite DOs (view/edit storage in dashboard)
  • Aug 2025: getByName() API shortcut for named DOs
  • June 2025: @cloudflare/actors library (beta) - recommended SDK with migrations, alarms, Actor class pattern. Note: Beta stability - see active issues before production use (RPC serialization, vitest integration, memory management)
  • May 2025: Python Workers support for Durable Objects
  • April 2025: SQLite GA with 10GB storage (beta → GA, 1GB → 10GB), Free tier access
  • Feb 2025: PRAGMA optimize support, improved error diagnostics with reference IDs

Quick Start

Scaffold new DO project:

npm create cloudflare@latest my-durable-app -- --template=cloudflare/durable-objects-template --ts

Or add to existing Worker:

// src/counter.ts - Durable Object class
import { DurableObject } from 'cloudflare:workers';

export class Counter extends DurableObject {
  async increment(): Promise<number> {
    let value = (await this.ctx.storage.get<number>('value')) || 0;
    await this.ctx.storage.put('value', ++value);
    return value;
  }
}
export default Counter;  // CRITICAL: Export required
// wrangler.jsonc - Configuration
{
  "durable_objects": {
    "bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Counter"] }  // SQLite backend (10GB limit)
  ]
}
// src/index.ts - Worker
import { Counter } from './counter';
export { Counter };

export default {
  async fetch(request: Request, env: { COUNTER: DurableObjectNamespace<Counter> }) {
    const stub = env.COUNTER.getByName('global-counter');  // Aug 2025: getByName() shortcut
    return new Response(`Count: ${await stub.increment()}`);
  }
};

DO Class Essentials

import { DurableObject } from 'cloudflare:workers';

export class MyDO extends DurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);  // REQUIRED first line

    // Load state before requests (optional)
    ctx.blockConcurrencyWhile(async () => {
      this.value = await ctx.storage.get('key') || defaultValue;
    });
  }

  // RPC methods (recommended)
  async myMethod(): Promise<string> { return 'Hello'; }

  // HTTP fetch handler (optional)
  async fetch(request: Request): Promise<Response> { return new Response('OK'); }
}

export default MyDO;  // CRITICAL: Export required

// Worker must export DO class too
import { MyDO } from './my-do';
export { MyDO };

Constructor Rules:

  • ✅ Call super(ctx, env) first
  • ✅ Keep minimal - heavy work blocks hibernation wake
  • ✅ Use ctx.blockConcurrencyWhile() for storage initialization
  • ❌ Never setTimeout/setInterval (use alarms)
  • ❌ Don't rely on in-memory state with WebSockets (persist to storage)

Storage API

Two backends available:

  • SQLite (recommended): 10GB storage, SQL queries, atomic operations, PITR
  • KV: 128MB storage, key-value only

Enable SQLite in migrations:

{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }] }

SQL API (SQLite backend)

export class MyDO extends DurableObject {
  sql: SqlStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;

    this.sql.exec(`
      CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, text TEXT, created_at INTEGER);
      CREATE INDEX IF NOT EXISTS idx_created ON messages(created_at);
      PRAGMA optimize;  // Feb 2025: Query performance optimization
    `);
  }

  async addMessage(text: string): Promise<number> {
    const cursor = this.sql.exec('INSERT INTO messages (text, created_at) VALUES (?, ?) RETURNING id', text, Date.now());
    return cursor.one<{ id: number }>().id;
  }

  async getMessages(limit = 50): Promise<any[]> {
    return this.sql.exec('SELECT * FROM messages ORDER BY created_at DESC LIMIT ?', limit).toArray();
  }
}

SQL Methods:

  • sql.exec(query, ...params) → cursor
  • cursor.one<T>() → single row (throws if none)
  • cursor.one<T>({ allowNone: true }) → row or null
  • cursor.toArray<T>() → all rows
  • ctx.storage.transactionSync(() => { ... }) → atomic multi-statement

Best Practices:

  • ✅ Use ? placeholders for parameterized queries
  • ✅ Create indexes on frequently queried columns
  • ✅ Use PRAGMA optimize after schema changes
  • ✅ Add STRICT keyword to table definitions to enforce type affinity and catch type mismatches early
  • ✅ Convert booleans to integers (0/1)

...

Read full content

Repository Stats

Stars213
Forks24
LicenseMIT License