All skills
cloudflare avatar

/durable-objects

@41e0d19 official
by cloudflarecloudflare/skills3k stars
298

Build, debug, or review Cloudflare Durable Objects code for persistent state and coordination.

Use this Skill: https://skilld.dev/gh/cloudflare/skills/durable-objects

This session only. Nothing lands on disk.

referencesworkers.md

≈1.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Cloudflare Workers Best Practices

High-level guidance for Workers that invoke Durable Objects.

Wrangler Configuration

wrangler.jsonc (Recommended)

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {
    "bindings": [
      { "name": "CHAT_ROOM", "class_name": "ChatRoom" },
      { "name": "USER_SESSION", "class_name": "UserSession" }
    ]
  },

  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["ChatRoom", "UserSession"] }
  ],

  // Environment variables
  "vars": {
    "ENVIRONMENT": "production"
  },

  // KV namespaces
  "kv_namespaces": [
    { "binding": "CONFIG", "id": "abc123" }
  ],

  // R2 buckets
  "r2_buckets": [
    { "binding": "UPLOADS", "bucket_name": "my-uploads" }
  ],

  // D1 databases
  "d1_databases": [
    { "binding": "DB", "database_id": "xyz789" }
  ]
}

wrangler.toml (Alternative)

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]

[[durable_objects.bindings]]
name = "CHAT_ROOM"
class_name = "ChatRoom"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatRoom"]

[vars]
ENVIRONMENT = "production"

TypeScript Types

Environment Interface

// src/types.ts
import { ChatRoom } from "./durable-objects/chat-room";
import { UserSession } from "./durable-objects/user-session";

export interface Env {
  // Durable Objects
  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
  USER_SESSION: DurableObjectNamespace<UserSession>;

  // KV
  CONFIG: KVNamespace;

  // R2
  UPLOADS: R2Bucket;

  // D1
  DB: D1Database;

  // Environment variables
  ENVIRONMENT: string;
  API_KEY: string; // From secrets
}

Export Durable Object Classes

// src/index.ts
export { ChatRoom } from "./durable-objects/chat-room";
export { UserSession } from "./durable-objects/user-session";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Worker handler
  },
};

Worker Handler Pattern

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    try {
      // Route to appropriate handler
      if (url.pathname.startsWith("/api/rooms")) {
        return handleRooms(request, env);
      }
      if (url.pathname.startsWith("/api/users")) {
        return handleUsers(request, env);
      }

      return new Response("Not Found", { status: 404 });
    } catch (error) {
      console.error("Request failed:", error);
      return new Response("Internal Server Error", { status: 500 });
    }
  },
};

async function handleRooms(request: Request, env: Env): Promise<Response> {
  const url = new URL(request.url);
  const roomId = url.searchParams.get("room");

  if (!roomId) {
    return Response.json({ error: "Missing room parameter" }, { status: 400 });
  }

  const stub = env.CHAT_ROOM.getByName(roomId);

  if (request.method === "POST") {
    const body = await request.json<{ userId: string; message: string }>();
    const result = await stub.sendMessage(body.userId, body.message);
    return Response.json(result);
  }

  const messages = await stub.getMessages();
  return Response.json(messages);
}

Request Validation

import { z } from "zod";

const SendMessageSchema = z.object({
  userId: z.string().min(1),
  message: z.string().min(1).max(1000),
});

async function handleSendMessage(request: Request, env: Env): Promise<Response> {
  const body = await request.json();
  const result = SendMessageSchema.safeParse(body);

  if (!result.success) {
    return Response.json(
      { error: "Validation failed", details: result.error.issues },
      { status: 400 }
    );
  }

  const stub = env.CHAT_ROOM.getByName(result.data.userId);
  const message = await stub.sendMessage(result.data.userId, result.data.message);
  return Response.json(message);
}

Observability & Logging

Structured Logging

function log(level: "info" | "warn" | "error", message: string, data?: Record<string, unknown>) {
  console.log(JSON.stringify({
    level,
    message,
    timestamp: new Date().toISOString(),
    ...data,
  }));
}

// Usage
log("info", "Request received", { path: url.pathname, method: request.method });
log("error", "DO call failed", { roomId, error: String(error) });

Request Tracing

async function handleRequest(request: Request, env: Env): Promise<Response> {
  const requestId = crypto.randomUUID();
  const startTime = Date.now();

  try {
    const response = await processRequest(request, env);

    log("info", "Request completed", {
      requestId,
      duration: Date.now() - startTime,
      status: response.status,
    });

    return response;
  } catch (error) {
    log("error", "Request failed", {
      requestId,
      duration: Date.now() - startTime,
      error: String(error),
    });
    throw error;
  }
}

Tail Workers (Production)

For production logging, use Tail Workers to forward logs:

// wrangler.jsonc
{
  "tail_consumers": [
    { "service": "log-collector" }
  ]
}

Error Handling

Graceful DO Errors

async function callDO(stub: DurableObjectStub<ChatRoom>, method: string): Promise<Response> {
  try {
    const result = await stub.getMessages();
    return Response.json(result);
  } catch (error) {
    if (error instanceof Error) {
      // DO threw an error
      log("error", "DO operation failed", { error: error.message });
      return Response.json(
        { error: "Service temporarily unavailable" },
        { status: 503 }
      );
    }
    throw error;
  }
}

Timeout Handling

async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), ms)
  );
  return Promise.race([promise, timeout]);
}

// Usage
const result = await withTimeout(stub.processData(data), 5000);

CORS Handling

function corsHeaders(): HeadersInit {
  return {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
  };
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method === "OPTIONS") {
      return new Response(null, { headers: corsHeaders() });
    }

    const response = await handleRequest(request, env);
    
    // Add CORS headers to response
    const newHeaders = new Headers(response.headers);
    Object.entries(corsHeaders()).forEach(([k, v]) => newHeaders.set(k, v));
    
    return new Response(response.body, {
      status: response.status,
      headers: newHeaders,
    });
  },
};

Secrets Management

Set secrets via wrangler CLI (not in config files):

wrangler secret put API_KEY
wrangler secret put DATABASE_URL

Access in code:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const apiKey = env.API_KEY; // From secret
    // ...
  },
};

Development Commands

# Local development
wrangler dev

# Deploy
wrangler deploy

# Tail logs
wrangler tail

# List DOs
wrangler d1 execute DB --command "SELECT * FROM _cf_DO"

Source: SKILL.md on GitHub

No alerts8d5 checks · Risk SAFE
  • Gen Agent Trust Hub8d

    This skill provides comprehensive guidance for building and debugging Cloudflare Durable Objects. It includes standard coding patterns, configuration examples, and references to official documentation. A minor security consideration is the inherent risk of indirect prompt injection when processing user-provided code for review, which is typical for development-focused skills.

  • Socket8d

    No alerts

  • Snyk8d

    Risk: LOW · No issues

  • Runlayer7mo

    4 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 41e0d19. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 4 hours ago.

Activeupdated 6 hours ago
  • TypeScript
  • cloudflare
  • durable-objects
  • workers
  • stateful
  • coordination
  • websockets
  • sqlite
  • vitest
  • wrangler

README badge

README badge for cloudflare/skills/durable-objects

Creates and reviews Cloudflare Durable Objects for stateful edge applications, covering RPC methods, SQLite storage, alarms, and WebSocket handlers. Use for building coordination systems like chat rooms, multiplayer games, and booking systems, plus Vitest testing and wrangler configuration.

Generated from the current SKILL.md.

Does this skill work with plain Workers or only Durable Objects?
This skill targets Durable Objects specifically. For stateless request handling, use plain Workers instead.
What storage backends does this cover?
The skill covers SQLite (recommended and synchronous) and KV storage (async). SQLite is the primary focus for Durable Objects.
Can I use this skill to test Durable Objects with Vitest?
Yes. The skill includes Vitest setup and testing patterns using `@cloudflare/vitest-pool-workers`, with examples for unit and integration tests including alarm testing.
Does this skill prioritize Cloudflare's official documentation?
Yes. The skill biases towards retrieving from Cloudflare's official docs over pre-trained knowledge for any Durable Objects task.

Generated from the current SKILL.md. These answers refresh after source changes.