| name | cf-durable-object-package |
| description | Add or change a Durable Object worker package under durable-objects/ (Alchemy, env.d.ts, Hono on the DO). Use when scaffolding with turbo gen durable-object, editing durable-objects/*/alchemy.run.ts, workers/app.ts, or env.d.ts for a DO. Not for web app bindings or cross-worker rpc—see cf-web-alchemy-bindings and cf-worker-rpc-turbo. |
Durable Object package (Alchemy + Hono)
When to use
- New package under
durable-objects/<name>/ (generator or copy an example).
- Changing
DurableObjectNamespace, Worker, entrypoint, or the DO’s Hono app.
Steps
-
Package layout
Minimum files: alchemy.run.ts, env.d.ts, workers/app.ts, package.json (exports: "./alchemy": "./alchemy.run.ts"), tsconfig.json.
Examples: ping-do (Hono + DO + WorkerEntrypoint), chatroom-do (Socka + DO SQLite + chat-contract).
For WebSocket RPC + server push, prefer Socka + contract over ad hoc WebSocket JSON — cf-socka-realtime.
-
alchemy.run.ts
await alchemy(…) string must match alchemy-cli / package.json → alchemy.app (${PRODUCT_PREFIX}-<suffix> from ALCHEMY_APP_IDS → alchemy-utils/worker-peer-scripts).
requireAlchemyPassword(app) from alchemy-utils.
- Export
DurableObjectNamespace<YourDoRpc> (types from ./workers/rpc).
Worker(...): use DEFAULT_WORKER_RESOURCE_ID (worker); omit name: unless you need an override. Cyclic WorkerRef pairs: omitDefaultPhysicalWorkerScriptName (cf-worker-rpc-turbo).
-
SQLite / Drizzle (persisted DO state)
- Source of truth: package
src/schema.ts.
drizzle.config.ts: dialect: "sqlite", driver: "durable-sqlite", schema → that file.
package.json: "db:generate": "drizzle-kit generate"; run it → drizzle/*.sql, drizzle/meta/*.json, drizzle/migrations.js.
drizzle/sql.d.ts: declare module "*.sql"; tsconfig includes drizzle/**/*.d.ts for SQL imports.
- Never hand-edit Drizzle SQL or meta; commit only generator output.
-
env.d.ts — export type CloudflareEnv = (typeof <yourExportedWorker>)["Env"]. declare global { type Env = CloudflareEnv } and declare module "cloudflare:workers" Env merge (match ping-do/env.d.ts).
-
Hono — In workers/app.ts: import type { CloudflareEnv } from "../env", const app = new Hono<{ Bindings: CloudflareEnv }>() (same as other-worker/workers/app.ts).
-
Scripts
alchemy in package.json: "alchemy": { "app": "<ALCHEMY_APP_IDS key>", "entry": "alchemy.run.ts" } (omit entry when it is alchemy.run.ts).
dev / deploy:* / destroy:*: alchemy-cli --stage local|staging|prod|preview <dev|deploy|destroy> — alchemy-cli loads repo-root dotfiles, sets STAGE, and passes --app from alchemy.app.
- Add
"alchemy-utils": "workspace:*" so alchemy-cli is on script PATH.
- Add
state-hub: workspace:* devDependency so Turbo ^deploy:* runs the shared remote Alchemy state hub first.
- SQLite DOs: also expose
db:generate.
-
After edits — From repo root: bun run typegen and bun run typecheck (or package-local typecheck). If schema changed, run package-local db:generate first.
Next (outside this skill)
Optional web / WebSocket checklist
If this new DO should be reachable from the web app, complete these follow-up edits:
-
Root package.json dev: add --filter=<your-package>.
-
Root turbo.json: add <your-package>#destroy:prod, #destroy:staging, and #destroy:preview depending on the matching @internal/web#destroy:*.
-
apps/web/package.json: add "<your-package>": "workspace:*" and run bun install.
-
apps/web/alchemy.run.ts: import from "<your-package>/alchemy" and bind the namespace/worker into ReactRouter.
-
WebSocket / Socka
- In
apps/web/workers/app.ts: upgrades before React Router; same URL prefix as client; skip Vite HMR; forward to DO /websocket.
@firtoz/socka: cf-socka-realtime/SKILL.md — contract, SockaWebSocketDO, useSockaSession, SSR-safe wss://.
-
Verify from repo root: bun run typegen, bun run typecheck, bun run lint.