Skip to content

Repository files navigation

Dual-runtime Durable Object examples

The same three named Durable Object lessons, implemented twice: once for Cloudflare Durable Objects and once for celld (self-hosted Workers + Durable Objects).

Package Runtime How the Worker calls a named instance
examples/cloudflare Cloudflare getByName() + RPC
examples/celld celld idFromName()get()stub.fetch()

Read the four source files in either package top to bottom (index.ts, counter.ts, tickets.ts, agent.ts). There is no shared library. HTTP and storage stay paired; only the call style differs.

What a Durable Object is here

A Worker is the public HTTP boundary: parse the path, validate the name, dispatch.

A Durable Object class defines behavior. Each lowercase name selects one isolated instance of that class with its own SQLite storage. alice and bob never share a counter.

Storage is how an instance remembers anything after the request ends:

Class Stored key Unseen read
Counter value 0
Tickets sold 0 of a fixed capacity 5
Agent messages, optional dataset empty history, no file

The runtime serializes requests for one name. Concurrent increments and ticket reserves cannot interleave mid-update. That is the lesson, not a lock you write yourself.

Two call styles

Cloudflare classes extend DurableObject and are called over RPC:

const stub = env.COUNTERS.getByName(name);
if (request.method === "GET") return Response.json({ value: await stub.get() });
if (request.method === "POST") return Response.json({ value: await stub.increment() });

celld classes are plain (state, env) objects. The Worker forwards the original request:

const id = namespace.idFromName(name);
const stub = namespace.get(id);
return stub.fetch(request);

HTTP contract

Names match [a-z0-9][a-z0-9-]{0,63}. Unknown path or method returns 404 { error: "NOT_FOUND" }. There is no health route and no Agent read route.

Route Behavior
GET /counters/:name 200 { value }. Unseen is 0.
POST /counters/:name Increment. 200 { value }.
GET /tickets/:name 200 { capacity: 5, sold, remaining }. Unseen sold is 0.
POST /tickets/:name Reserve one seat. Same success body, or 409 { error: "SOLD_OUT" }.
POST /agents/:name with { "prompt", "csv"?, "filename"? } Optional CSV is stored in this instance’s SQLite (dataset) before the model runs. Prompt-only still works. Tools may search, read, or run JS on that file. 200 { reply }. Missing prompt is 400 { error: "PROMPT_REQUIRED" }.

Counter and Tickets POSTs have no body. Agent uses the Vercel AI SDK ToolLoopAgent with OpenAI. Secret is Worker env.OPENAI_API_KEY; optional env.OPENAI_MODEL defaults to gpt-5.6-sol. There is no filesystem: one optional CSV lives in SQLite next to messages.

Install and check

Requires Node.js and pnpm:

pnpm install --frozen-lockfile
pnpm check

pnpm check formats/lints the workspace and typechecks/builds both packages. There is no automated test suite.

Run locally (Cloudflare)

# examples/cloudflare/.dev.vars (gitignored)
OPENAI_API_KEY=...
OPENAI_MODEL=gpt-5.6-sol

pnpm --filter cloudflare-do-lab dev

Then GET/POST http://127.0.0.1:8787/counters/alice (same shape for /tickets/:name and POST /agents/:name).

Run on celld

celld has no wrangler dev. You deploy the bundle to a fleet bucket and run a celld node against that bucket. See examples/celld/DEPLOYMENT.md and the celld docs.

Default listen is 127.0.0.1:8080. Worker secrets are node env overrides (CELLD_VAR_OPENAI_API_KEY), not wrangler secret.

Try the agent with sample sales data

sales.csv is dummy merch orders. Send it as csv so the instance stores it; later prompts on the same name reuse that file.

curl -s http://127.0.0.1:8787/agents/sales \
  -H 'content-type: application/json' \
  -d "{\"prompt\":\"Which SKU sold the most units?\",\"csv\":$(jq -Rs . sales.csv),\"filename\":\"sales.csv\"}"

curl -s http://127.0.0.1:8787/agents/sales \
  -H 'content-type: application/json' \
  -d '{"prompt":"What can you do for me?"}'

Use http://127.0.0.1:8080 the same way against a celld node. Tools: search_files, read_file, run_code (standard JS in a Worker Loader isolate).

Deploy and undeploy

This is a teaching reset, not an upgrade path. Use a fresh Worker/class set or an empty celld bucket. Existing stored Counter, Tickets, or Agent data from an earlier schema is unsupported.

About

No description, website, or topics provided.

Resources

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages