Claude Marketplaces
SkillsMCP serversMarketplacesDigestTools
Submit

This week in Claude

Every Monday: Claude Code, Agent SDK, MCP, and the Anthropic platform moves worth your time.

Skills by category
Frontend DevelopmentBackend & APIsTesting & QASecurityDevOps & CI/CDGit & Pull RequestsDocumentationCode Review & QualityAI & Agent BuildingSkill Development
MCP servers by category
Sales & MarketingWeb & Browser AutomationDatabasesAI & LLM ToolsCloud & InfrastructureCommunication & MessagingDeveloper ToolsDesign & CreativeDocuments & KnowledgeSearch & Web Crawling
Marketplaces by category
AI Agents & OrchestrationLLM IntegrationDevelopment ToolsFrontend & UIBackend & APIsDatabasesTesting & Code QualityDevOps & CloudSecurity & ComplianceGit & Version Control
Claude Marketplaces

An independent directory of skills, plugins and MCP servers for Claude Code. Not affiliated with Anthropic.

Browse

  • Skills
  • MCP servers
  • Marketplaces
  • Weekly digest
  • Skill index
  • MCP index
  • Marketplace index

Site

  • About
  • Tools
  • Submit
  • Feedback
  • Privacy
  • llms.txt
  • Plugins reference

Maker

  • Snug
  • Agent Caffeine
  • knobs
  • Wax
  • Vectoys
  • mertduzgun.com
Crafted at
Skills
butterbase-ai avatar

Durable Objects

butterbase-ai/butterbase-skills

This is the skill you reach for when you need state that sticks around between requests and belongs to a specific key: a chat room, a leaderboard, a rate limiter, a long-running agent. Each instance gets its own in-memory state plus a transactional KV store, and you can attach WebSockets directly to it. The constraints are tight: one TypeScript file per class, no npm imports, PascalCase class names that map to kebab-case URLs. You deploy once and instances spin up on demand per ID. Works well for multiplayer rooms or per-user agents where you need guaranteed single-threaded execution and can't tolerate the cold-start shuffle of stateless functions. Just remember that cross-instance coordination is on you.

GitHub stars
532
last updated
Jun 11, 2026
View on GitHubSave

Install

$ npx -y skills add butterbase-ai/butterbase-skills --skill durable-objects --agent claude-code

Installs into .claude/skills of the current project.

Files

Butterbase Durable Objects

Durable Objects (DOs) are stateful per-key actors running on Cloudflare Workers. Each instance has its own in-memory state and a built-in transactional KV store. Use one when state must survive across requests for a single room/user/agent. For stateless work, use a serverless function instead (butterbase-skills:function-dev).

One tool: manage_durable_objects.


1. The mental model

Class: ChatRoom (deployed once)
   │
   ├── instance "lobby"      ─►  in-memory state + state.storage  +  WebSockets
   ├── instance "general"    ─►  separate state, separate sockets
   └── instance "user-123"   ─►  separate again

Each URL  https://<app>.butterbase.dev/_do/chat-room/<instance-id>
gets routed to the instance with that id. State is isolated per id.

A class is shared code; an instance is a unique key (/lobby, /general, /user-123). Different ids = different state. There is no shared cross-instance state.


2. Constraints (read these first)

  • One TypeScript file per class. No npm imports. Only import { ... } from 'cloudflare:workers' is allowed.
  • Exactly one exported class. export class Foo { ... } — no extra exports, no helpers re-exported.
  • PascalCase class name in source; kebab-case for the URL name (e.g. ChatRoom ↔ chat-room).
  • File size: ≤ 5 MB. Total of all DO classes per app: ≤ 10 MB compressed.
  • ≤ 5 DO classes per app (v1).
  • No service bindings yet. Functions reach DOs over HTTP, not via env binding.
  • state.storage keys/values capped at 128 KB. Larger blobs → Butterbase Storage.
  • Browser WebSockets need access_mode: "public". Browsers can't set custom headers on WS upgrade, and the _do/ dispatcher reads auth only from Authorization — ?token= and Sec-WebSocket-Protocol are silently ignored at the dispatcher (unlike the /realtime route, which does accept ?token=). Set the DO public, then read the token from ?token= (or Sec-WebSocket-Protocol) inside fetch() and verify it yourself before accepting the upgrade. Server-to-server callers can still use authenticated/service_key.

3. The class skeleton

export class ChatRoom {
  constructor(public state: DurableObjectState, public env: Env) {}

  async fetch(req: Request): Promise<Response> {
    if (req.headers.get("Upgrade") === "websocket") {
      const pair = new WebSocketPair();
      this.state.acceptWebSocket(pair[1]);
      return new Response(null, { status: 101, webSocket: pair[0] });
    }

    if (req.method === "POST") {
      // handle plain HTTP
    }

    return Response.json({ ok: true });
  }

  // Optional WebSocket lifecycle hooks — called by the runtime
  async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {
    if (typeof msg !== "string") return;             // guard binary
    for (const peer of this.state.getWebSockets()) {
      try { peer.send(msg); } catch {}
    }
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {}
  async webSocketError(ws: WebSocket, err: Error) {}
}

Key APIs:

APIPurpose
state.storage.get/put/delete/deleteAll/listAsync transactional KV store
state.acceptWebSocket(ws)Hold a WS connection; runtime routes messages to webSocketMessage
state.getWebSockets()All active WS connections for this instance
new WebSocketPair()Returns [client, server] — return client to browser, accept server
this.env.KEYRead DO env vars (set via set_env)

4. Deploy

manage_durable_objects({
  app_id: "app_abc123",
  action: "deploy",
  name: "chat-room",                  // kebab-case URL name
  code: "<single TypeScript file>",
  access_mode: "authenticated"        // "public" | "authenticated" (default) | "service_key"
})
// → { id, name, class_name, status: "READY", access_mode, last_deployed_at }

Re-deploying with the same name updates the class; old in-memory state is evicted on next request. Storage persists across redeploys (same instance id = same state.storage).

Access modes

ModeAuth required
publicNone — validate tokens inside fetch() if you need any
authenticated (default)End-user JWT in Authorization: Bearer <token>
service_keyButterbase service key — backend-to-backend

The dispatcher only checks header shape, not validity. For real auth on production DOs, validate the token inside fetch().


5. Address an instance

https://<your-subdomain>.butterbase.dev/_do/<name>/<instance-id>
  • <name> = kebab-case DO name from deploy
  • <instance-id> = anything you choose (/lobby, /user-123, /main)

Both HTTP and WebSocket upgrade work on the same URL.

// HTTP
fetch("https://app.butterbase.dev/_do/chat-room/lobby", {
  method: "POST",
  body: JSON.stringify({ user: "alice", text: "hi" })
});

// WebSocket
const ws = new WebSocket("wss://app.butterbase.dev/_do/chat-room/lobby");

Different instance ids → completely separate state. There is no shared global view; if you need one, build it yourself (e.g. a /registry instance that other instances report into).


6. Env vars

Env vars are app-wide across all DO classes. Setting one redeploys the DO Worker — existing in-memory state is evicted, active WS connections drop.

manage_durable_objects({ app_id, action: "list_env" })                                  // keys only, never values
manage_durable_objects({ app_id, action: "set_env", key: "AI_API_KEY", value: "sk-..." })
manage_durable_objects({ app_id, action: "delete_env", key: "AI_API_KEY" })
  • Keys must match ^[A-Z_][A-Z0-9_]*$ (UPPER_SNAKE).
  • A key can't collide with a DO class binding (e.g. chat-room reserves CHAT_ROOM).
  • Read in code as this.env.KEY_NAME.

7. Lifecycle, listing, deletion

manage_durable_objects({ app_id, action: "list" })
manage_durable_objects({ app_id, action: "get", name: "chat-room" })             // includes full source + status + error_message
manage_durable_objects({ app_id, action: "delete", name: "chat-room" })          // IRREVERSIBLE: purges all instances + storage
manage_durable_objects({ app_id, action: "usage", name: "chat-room" })           // do_requests, do_cpu_ms (refreshed every 15 min)

Status transitions: PENDING → BUILDING → READY or ERROR (with error_message).


8. Patterns

Chat room (broadcast)

export class ChatRoom {
  constructor(public state: DurableObjectState, public env: any) {}

  async fetch(req: Request): Promise<Response> {
    if (req.headers.get("Upgrade") === "websocket") {
      const pair = new WebSocketPair();
      this.state.acceptWebSocket(pair[1]);
      const history = (await this.state.storage.get("messages")) ?? [];
      pair[1].send(JSON.stringify({ type: "init", messages: history }));
      return new Response(null, { status: 101, webSocket: pair[0] });
    }
    return Response.json(await this.state.storage.get("messages") ?? []);
  }

  async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {
    if (typeof msg !== "string") return;
    const history: any[] = (await this.state.storage.get("messages")) ?? [];
    const parsed = JSON.parse(msg);
    history.push(parsed);
    await this.state.storage.put("messages", history.slice(-200));
    for (const peer of this.state.getWebSockets()) {
      try { peer.send(msg); } catch {}
    }
  }
}

Sliding-window rate limiter

export class RateLimiter {
  constructor(public state: DurableObjectState, public env: any) {}

  async fetch(req: Request): Promise<Response> {
    const now = Date.now();
    const window = 60_000;
    const limit = 100;
    const requests: number[] = (await this.state.storage.get("requests")) ?? [];
    const recent = requests.filter(t => now - t < window);
    if (recent.length >= limit) return new Response("rate limit", { status: 429 });
    recent.push(now);
    await this.state.storage.put("requests", recent);
    return Response.json({ ok: true, remaining: limit - recent.length });
  }
}

Address one instance per actor: /_do/rate-limiter/<user-id> or /_do/rate-limiter/<api-key-hash>.

Long-running AI agent

export class Agent {
  constructor(public state: DurableObjectState, public env: any) {}
  async fetch(req: Request): Promise<Response> {
    const { prompt } = await req.json();
    const r = await fetch(this.env.AI_API_ENDPOINT, {
      method: "POST",
      headers: { Authorization: `Bearer ${this.env.AI_API_KEY}` },
      body: JSON.stringify({ prompt })
    });
    const data = await r.json();
    await this.state.storage.put("last_response", data);
    return Response.json(data);
  }
}

One DO instance per conversation; storage holds the rolling history.

Counter / leaderboard

Each instance is its own counter. /_do/leaderboard/main and /_do/leaderboard/season-2 have independent state — no coordination needed in v1.


9. Errors

Build-time (rejected on deploy):

CodeCause
NO_EXPORTED_CLASSSource doesn't export a class
MULTIPLE_EXPORTSMore than one export, or export of non-class
INVALID_IMPORTImported anything other than cloudflare:workers
CLASS_NAME_PARSE_ERRORTS AST couldn't extract the class name
NAME_REGEX_VIOLATIONname doesn't match ^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$
QUOTA_DO_LIMITAlready 5 classes for this app
BUNDLE_SIZE_EXCEEDED / SOURCE_SIZE_EXCEEDEDOver the 10 MB / 5 MB limits

Runtime / async-deploy:

  • Status ERROR after a deploy → check manage_durable_objects (get) error_message.
  • WebSocket message handler only fires if you called state.acceptWebSocket(ws) — easy to forget.
  • webSocketMessage receives string | ArrayBuffer. Always guard before JSON.parse.

10. Anti-patterns

Don'tDo
Try to share state between instances directlyPick a single "registry" instance and have others fetch into it
Use a DO for stateless HTTP workUse a function — DOs cost more and have stricter constraints
Rely on dispatcher access_mode for real authValidate JWTs inside fetch() for production
Use access_mode: "authenticated" for browser WebSocketsUse public + token-in-query-string + manual validation; browsers can't set headers
Stuff > 128 KB blobs into state.storageUse Butterbase Storage and store the object_id in DO state
Update env vars in tight loopsEach set_env redeploys the Worker — drops connections
Forget redeploy semanticsCode change or env change evicts all instances; storage survives but in-memory caches don't

If a docs/butterbase/00-state.md exists in the working directory, prefer invoking via /butterbase-skills:journey-durable so the journey orchestrator stays in sync.

SKILL.md on GitHub

Sponsors

SnugThe clipboard manager you and your agent can use, safelyAgent CaffeineClose your lid, come back to finished workWaxBlock anything with supervision, without erasing your iPhoneOpen StepsSponsoredFree and open-source skills that make AI coding agents easier to understand, verify, and control.

Details

Repository
github.com/butterbase-ai/butterbase-skills
License
MIT
First seen
Jun 11, 2026

More from butterbase-ai/butterbase-skills

All 39
  • Function Dev
  • Integrations
  • Journey
  • Journey Agents
  • Journey Ai
  • Journey Auth
  • Journey Deploy
  • Journey Docs
  • Journey Durable
  • Journey Frontend
  • Journey Functions
  • Journey Idea
  • Journey Plan
  • Journey Preflight
  • Journey Rag
  • Journey Realtime
  • Journey Rls
  • Journey Schema
  • Journey Storage
  • Journey Submit
  • Journey Substrate
  • Journey Templates
  • Migrations
  • Payments

Recommended

1juliusbrussee avatarcavemanjuliusbrussee/cavemanUltra-compressed communication mode. Cuts output tokens 65% (measured) by speaking like caveman while keeping full technical accuracy. Supports intensity levels594.9Kinstalls
2mattpocock avatargrill-memattpocock/skillsA relentless interview to sharpen a plan or design.1.4Minstalls
3shadcn avatarimproveshadcn/improveSurvey any codebase as a senior advisor and produce prioritized, self-contained implementation plans for OTHER models/agents to execute. Strictly read-only on s37.9Kinstalls
4obra avatarsystematic-debuggingobra/superpowersUse when encountering any bug, test failure, or unexpected behavior, before proposing fixes309.2Kinstalls
5forrestchang avatarkarpathy-guidelinesforrestchang/andrej-karpathy-skillsBehavioral guidelines to reduce common LLM coding mistakes through explicit assumptions, simplicity, and verifiable success criteria.17.9Kinstalls
6vercel-labs avatarfind-skillsvercel-labs/skillsHelps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express int4.1Minstalls