diff --git a/.opencode/state/cloudflare-skill-restructure/prd.json b/.opencode/state/cloudflare-skill-restructure/prd.json deleted file mode 100644 index c118e1d..0000000 --- a/.opencode/state/cloudflare-skill-restructure/prd.json +++ /dev/null @@ -1,264 +0,0 @@ -{ - "prdName": "cloudflare-skill-restructure", - "tasks": [ - { - "id": "split-1", - "category": "split", - "description": "Split wrangler/ reference (1470 lines) into <200 line files", - "steps": [ - "README.md exists with overview (<100 lines)", - "configuration.md exists with wrangler.jsonc setup (<150 lines)", - "api.md exists with runtime APIs (<150 lines)", - "patterns.md exists with common patterns (<150 lines)", - "gotchas.md exists with pitfalls/limits (<100 lines)", - "Original SKILL.md deleted", - "All files have cross-reference sections" - ], - "passes": true - }, - { - "id": "split-2", - "category": "split", - "description": "Split cache-reserve/ reference (1439 lines) into <200 line files", - "steps": [ - "README.md exists with overview (<100 lines)", - "configuration.md exists (<150 lines)", - "api.md exists (<150 lines)", - "patterns.md exists (<150 lines)", - "gotchas.md exists (<100 lines)", - "Original SKILL.md deleted" - ], - "passes": true - }, - { - "id": "split-3", - "category": "split", - "description": "Split stream/ reference (1340 lines) into <200 line files", - "steps": [ - "README.md exists with overview (<100 lines)", - "configuration.md exists (<150 lines)", - "api.md exists (<150 lines)", - "patterns.md exists (<150 lines)", - "gotchas.md exists (<100 lines)", - "Original SKILL.md deleted" - ], - "passes": true - }, - { - "id": "split-4", - "category": "split", - "description": "Split workers/ reference (1334 lines) into <200 line files", - "steps": [ - "README.md exists with overview (<100 lines)", - "configuration.md exists (<150 lines)", - "api.md exists (<150 lines)", - "patterns.md exists (<150 lines)", - "gotchas.md exists (<100 lines)", - "Original SKILL.md deleted", - "Cross-references to kv, d1, r2, durable-objects, queues" - ], - "passes": true - }, - { - "id": "split-5", - "category": "split", - "description": "Split realtimekit/ reference (1244 lines) into <200 line files", - "steps": [ - "README.md exists with overview (<100 lines)", - "configuration.md exists (<150 lines)", - "api.md exists (<150 lines)", - "patterns.md exists (<150 lines)", - "gotchas.md exists (<100 lines)", - "Original SKILL.md deleted" - ], - "passes": true - }, - { - "id": "split-6", - "category": "split", - "description": "Split Tier 2 references (1100-1250 lines each)", - "steps": [ - "terraform/ split into <200 line files", - "pulumi/ split into <200 line files", - "sandbox/ split into <200 line files", - "hyperdrive/ split into <200 line files", - "pages/ split into <200 line files", - "ddos/ split into <200 line files", - "workers-for-platforms/ split into <200 line files", - "workflows/ split into <200 line files", - "miniflare/ split into <200 line files", - "workerd/ split into <200 line files", - "cron-triggers/ split into <200 line files", - "secrets-store/ split into <200 line files", - "pages-functions/ split into <200 line files", - "network-interconnect/ split into <200 line files" - ], - "passes": true - }, - { - "id": "split-7", - "category": "split", - "description": "Split Tier 3 references (700-1100 lines each)", - "steps": [ - "All remaining reference files split to <200 lines", - "Each has README.md with overview", - "Each has appropriate sub-files (config, api, patterns, gotchas)" - ], - "passes": true - }, - { - "id": "trim-1", - "category": "trim", - "description": "Trim main SKILL.md from 232 to <200 lines", - "steps": [ - "Frontmatter updated with improved description", - "Frontmatter includes references: field with top 5 products", - "Common patterns section moved to separate file", - "Loading references section removed", - "Product index Summary column removed", - "Total lines <200" - ], - "passes": true - }, - { - "id": "crossref-1", - "category": "crossref", - "description": "Add cross-references to workers/ README.md", - "steps": [ - "In This Reference section with links to config, api, patterns, gotchas", - "See Also section with links to kv, d1, r2, durable-objects, queues" - ], - "passes": true - }, - { - "id": "crossref-2", - "category": "crossref", - "description": "Add cross-references to pages/ README.md", - "steps": [ - "In This Reference section with internal links", - "See Also section with links to pages-functions, d1, kv" - ], - "passes": true - }, - { - "id": "crossref-3", - "category": "crossref", - "description": "Add cross-references to agents-sdk/ README.md", - "steps": [ - "In This Reference section with internal links", - "See Also section with links to durable-objects, d1, workers-ai, vectorize" - ], - "passes": false - }, - { - "id": "crossref-4", - "category": "crossref", - "description": "Add cross-references to d1/ README.md", - "steps": [ - "In This Reference section with internal links", - "See Also section with links to workers, hyperdrive" - ], - "passes": false - }, - { - "id": "crossref-5", - "category": "crossref", - "description": "Add cross-references to durable-objects/ README.md", - "steps": [ - "In This Reference section with internal links", - "See Also section with links to workers, do-storage" - ], - "passes": false - }, - { - "id": "crossref-6", - "category": "crossref", - "description": "Add cross-references to all remaining split references", - "steps": [ - "Each README.md has In This Reference section", - "Each README.md has See Also section where applicable" - ], - "passes": false - }, - { - "id": "fix-1", - "category": "fix", - "description": "Fix type safety violation in d1 reference", - "steps": [ - "Replace any[] with (string | number | boolean | null)[]", - "No any types in TypeScript examples" - ], - "passes": false - }, - { - "id": "fix-2", - "category": "fix", - "description": "Update compatibility dates in all config examples", - "steps": [ - "All wrangler.jsonc examples use 2025-01-01 or later", - "Comment added: Use current date for new projects" - ], - "passes": false - }, - { - "id": "fix-3", - "category": "fix", - "description": "Convert all wrangler.toml to wrangler.jsonc format", - "steps": [ - "No wrangler.toml examples remain", - "All config uses wrangler.jsonc" - ], - "passes": false - }, - { - "id": "fix-4", - "category": "fix", - "description": "Add missing imports to code examples", - "steps": [ - "agents-sdk examples include import { tool } from 'ai'", - "All code examples have necessary imports" - ], - "passes": false - }, - { - "id": "polish-1", - "category": "polish", - "description": "Complete decision trees in main SKILL.md", - "steps": [ - "Run code tree includes snippets/ and tail-workers/", - "Networking tree includes smart-placement/", - "Store data tree includes cache-reserve/" - ], - "passes": false - }, - { - "id": "polish-2", - "category": "polish", - "description": "Standardize error handling in gotchas.md files", - "steps": [ - "Each gotchas.md has Common Errors section", - "Errors have Cause and Solution subsections", - "Each gotchas.md has Limits table" - ], - "passes": false - } - ], - "context": { - "patterns": [ - "Reference structure: references//README.md, configuration.md, api.md, patterns.md, gotchas.md", - "Line limits: README <100, others <150, none >200", - "Cross-ref template: In This Reference + See Also sections" - ], - "keyFiles": [ - "SKILL.md", - "references/workers/SKILL.md", - "references/d1/SKILL.md", - "references/pages/SKILL.md" - ], - "nonGoals": [ - "Rewriting content (quality is 4.7/5)", - "Adding new products", - "Changing technical recommendations" - ] - } -} diff --git a/.opencode/state/cloudflare-skill-restructure/prd.md b/.opencode/state/cloudflare-skill-restructure/prd.md deleted file mode 100644 index cdf967b..0000000 --- a/.opencode/state/cloudflare-skill-restructure/prd.md +++ /dev/null @@ -1,247 +0,0 @@ -# Cloudflare Skill Restructuring Plan - -Plan to bring skill into compliance with skill-creator best practices while preserving content quality. - -## Review Summary - -**Content quality: 4.7/5** — Excellent, needs no rewriting -**Structural compliance: Poor** — Violates line limits, missing frontmatter fields - -### Issues by Severity - -| Severity | Issue | Impact | -|----------|-------|--------| -| Critical | Reference files 700-1400+ lines (limit: 200) | Defeats progressive disclosure | -| High | Main SKILL.md 232 lines (limit: 200) | Loads unnecessary content | -| High | No `references:` in frontmatter | Poor discoverability | -| High | Activation description too broad | False activations | -| High | No cross-reference guidance | Agents miss related files | -| Medium | Reference files lack frontmatter | No metadata for discovery | -| Medium | Outdated compatibility dates | Agents use old dates | -| Medium | Type safety violation (d1 `any[]`) | Violates standards | -| Medium | Inconsistent error handling | Agents can't debug | -| Low | Inconsistent config format | Minor confusion | -| Low | Decision trees incomplete | Miss some products | - ---- - -## Phase 1: Split Reference Files (Critical) - -**Goal:** Split 60 files from 700-1400 lines to <200 lines each - -### Standard Structure - -``` -references// -├── README.md # Overview, when to use (<100 lines) -├── configuration.md # wrangler.jsonc setup (<150 lines) -├── api.md # Runtime API, types (<150 lines) -├── patterns.md # Common patterns (<150 lines) -├── gotchas.md # Pitfalls, limits (<100 lines) -└── advanced.md # Optional (<150 lines) -``` - -### Splitting Priority - -**Tier 1 (largest, split first):** -- wrangler/ (1470 lines) -- cache-reserve/ (1439 lines) -- stream/ (1340 lines) -- workers/ (1334 lines) -- realtimekit/ (1244 lines) - -**Tier 2 (1100-1250 lines):** -terraform, pulumi, sandbox, hyperdrive, pages, ddos, workers-for-platforms, workflows, miniflare, workerd, cron-triggers, secrets-store, pages-functions, network-interconnect - -**Tier 3 (700-1100 lines):** -Remaining 40+ files - -### Process - -For each SKILL.md: -1. Extract overview → `README.md` -2. Extract config sections → `configuration.md` -3. Extract API/types → `api.md` -4. Extract patterns/examples → `patterns.md` -5. Extract gotchas/limits → `gotchas.md` -6. Delete original SKILL.md -7. Add "Related References" section to README.md - -**Approach:** Manual with template -**Commit:** `refactor: split reference files into <200 line chunks` - ---- - -## Phase 2: Trim Main SKILL.md (High) - -**Goal:** Reduce from 232 to <200 lines - -### Changes - -| Section | Current | Action | Target | -|---------|---------|--------|--------| -| Frontmatter | 5 | Improve description, add `references:` | 10 | -| Intro | 3 | Keep | 3 | -| Decision trees | 82 | Keep | 82 | -| Product index | 101 | Remove Summary column | 70 | -| Common patterns | 26 | Move to `patterns.md` | 0 | -| Loading references | 12 | Remove | 0 | -| **Total** | **232** | | **~165** | - -### New Frontmatter - -```yaml ---- -name: cloudflare -description: Build and deploy on Cloudflare. Use when creating Workers/Pages apps, configuring KV/D1/R2 storage, implementing AI with Workers AI/Agents SDK, setting up Tunnels, or managing infrastructure via Terraform/Pulumi. NOT for DNS-only config or general web hosting. -references: - - references/workers/README.md - - references/d1/README.md - - references/pages/README.md - - references/agents-sdk/README.md - - references/wrangler/README.md ---- -``` - -**Commit:** `refactor: trim main SKILL.md to <200 lines` - ---- - -## Phase 3: Add Cross-References (High) - -**Goal:** Guide agents to related files - -### Template for README.md - -```markdown -## In This Reference -- [Configuration](configuration.md) — wrangler.jsonc setup -- [API Reference](api.md) — runtime APIs, types -- [Patterns](patterns.md) — common examples -- [Gotchas](gotchas.md) — pitfalls, limits - -## See Also -- [Related Product](../related-product/) — when to use instead -``` - -### Cross-Reference Map - -| Product | Related Products | -|---------|-----------------| -| workers | kv, d1, r2, durable-objects, queues | -| pages | pages-functions, d1, kv | -| agents-sdk | durable-objects, d1, workers-ai, vectorize | -| d1 | workers, hyperdrive | -| durable-objects | workers, do-storage | - -**Commit:** `docs: add cross-references between split files` - ---- - -## Phase 4: Fix Content Issues (Medium) - -### 4.1 Type Safety - -**File:** d1 api.md or patterns.md (after split) - -```typescript -// Before -const params: any[] = []; - -// After -const params: (string | number | boolean | null)[] = []; -``` - -### 4.2 Compatibility Dates - -Add note to all config examples: -```jsonc -{ - "compatibility_date": "2025-01-01", // Use current date for new projects -} -``` - -### 4.3 Config Format - -Convert all wrangler.toml to wrangler.jsonc - -### 4.4 Missing Imports - -Add imports to code examples, particularly: -- agents-sdk: `import { tool } from "ai"` - -**Commit:** `fix: type safety, config format, imports` - ---- - -## Phase 5: Low Priority Fixes - -### 5.1 Complete Decision Trees - -Add to "I need to run code": -``` -├─ Lightweight request mods → snippets/ -├─ Observability/logging → tail-workers/ -``` - -Add to "I need networking": -``` -├─ Optimize Worker placement → smart-placement/ -``` - -Add to "I need to store data": -``` -├─ Extended edge cache → cache-reserve/ -``` - -### 5.2 Error Handling Template - -Standardize `gotchas.md` structure: -```markdown -## Common Errors - -### `` -**Cause:** ... -**Solution:** ... - -## Limits -| Limit | Value | -|-------|-------| -``` - -**Commit:** `docs: complete decision trees, standardize error handling` - ---- - -## Verification Checklist - -After all phases: -- [ ] Main SKILL.md <200 lines -- [ ] All reference files <200 lines -- [ ] Frontmatter has `references:` field -- [ ] Each README.md has cross-references -- [ ] No `any` types in TypeScript -- [ ] All config uses wrangler.jsonc -- [ ] Decision trees complete -- [ ] Cross-reference paths correct - ---- - -## Estimates - -| Phase | Effort | Files Changed | -|-------|--------|---------------| -| 1 | 6-8 hours | ~300 new, 60 deleted | -| 2 | 30 min | 2 files | -| 3 | 2 hours | ~60 files | -| 4 | 1-2 hours | ~30 files | -| 5 | 1 hour | ~10 files | -| **Total** | **~12 hours** | | - ---- - -## Open Questions - -1. Should smaller reference files (<400 lines) be split, or only trimmed? -2. Should `docs/products.md` be integrated into SKILL.md or kept separate? -3. Priority for Phase 1: all files, or just Tier 1 first? diff --git a/.opencode/state/cloudflare-skill-restructure/progress.txt b/.opencode/state/cloudflare-skill-restructure/progress.txt deleted file mode 100644 index 6ee7044..0000000 --- a/.opencode/state/cloudflare-skill-restructure/progress.txt +++ /dev/null @@ -1,156 +0,0 @@ -# Progress Log -PRD: cloudflare-skill-restructure -Started: 2026-01-12 - -## Codebase Patterns - -### Reference Split Pattern -- README.md: Overview, key commands, navigation (<100 lines) -- configuration.md: Config reference, compact examples (<150 lines) -- api.md: Programmatic APIs, testing patterns (<150 lines) -- patterns.md: Common workflows, best practices (<150 lines) -- gotchas.md: Issues, troubleshooting, limits (<100 lines) -- All files: "See Also" section with cross-links to other files - ---- - - -## Task - split-1 -- Split 1470-line wrangler/SKILL.md into 5 focused files -- Files created: - - README.md (90 lines) - Overview, essential commands - - configuration.md (128 lines) - Config reference, bindings - - api.md (140 lines) - Programmatic API (unstable_startWorker, getPlatformProxy) - - patterns.md (150 lines) - Workflows, testing, optimization - - gotchas.md (93 lines) - Common issues, troubleshooting -- Deleted: SKILL.md (1470 lines) -- All files have cross-reference sections -- **Learnings:** - - Consolidate related subsections into single code blocks with comments - - Aggressive line reduction: combine similar examples, remove redundancy - - Cross-references at bottom keep navigation consistent - -## Task - split-2 -- Split 1439-line cache-reserve/SKILL.md into 5 focused files -- Files created: - - README.md (93 lines) - Overview, eligibility, quick start - - configuration.md (164 lines) - Dashboard, API, Cache Rules, wrangler - - api.md (176 lines) - Workers integration, purging, monitoring, pricing - - patterns.md (180 lines) - Best practices, architecture, cost optimization - - gotchas.md (203 lines) - Issues, solutions, troubleshooting, limits -- Deleted: SKILL.md (1439 lines) -- All files have cross-reference sections -- **Learnings:** - - Inline compression: combine similar patterns, use compact comments - - Remove verbose examples when simpler versions convey same info - - Consolidate header pattern examples into single line - -## Task - split-3 -- Split 1341-line stream/SKILL.md into 5 focused files -- Files created: - - README.md (103 lines) - Overview, upload methods, playback, quick start - - configuration.md (127 lines) - Env vars, wrangler, signing keys, webhooks, access rules - - api.md (204 lines) - Upload, playback, signed URLs, live streaming, video management - - patterns.md (152 lines) - Full-stack upload flow, webhooks, state management, best practices - - gotchas.md (131 lines) - Common errors, troubleshooting, limits, security -- Deleted: SKILL.md (1341 lines) -- All files have cross-reference sections -- **Learnings:** - - Consolidate config interfaces into single examples with inline comments - - Reference full implementations in other files to avoid duplication - - Compress similar API patterns (list/update/delete) into combined examples - -## Task - split-4 -- Split 1334-line workers/SKILL.md into 5 focused files -- Files created: - - README.md (96 lines) - Overview, module pattern, essential commands, handler signatures - - configuration.md (147 lines) - wrangler.jsonc setup, bindings (KV, R2, D1, DO, queues, services), TypeScript - - api.md (137 lines) - Fetch handler, execution context, bindings access, Cache API, HTMLRewriter, WebSockets, Durable Objects, other handlers - - patterns.md (149 lines) - Error handling, CORS, routing, performance, streaming, testing, deployment, monitoring, security, gradual rollouts - - gotchas.md (99 lines) - CPU limits, statelessness, response streams, Node.js compat, global scope, limits table, common errors -- Deleted: SKILL.md (1334 lines) -- All files have cross-reference sections linking to kv, d1, r2, durable-objects, queues -- **Learnings:** - - Aggressive inline compression: combine related concepts in single code blocks - - Reference other files to avoid duplication (e.g., rate limiting -> durable-objects) - - Use compact table format for limits/bindings reference - - Single-line patterns for simple examples when appropriate - -## Task - split-5 -- Split 1244-line realtimekit/SKILL.md into 5 focused files (existing files trimmed) -- Files updated/verified: - - README.md (81 lines) - Overview, core concepts, quick start, pricing - - configuration.md (147 lines) - SDK install, client config, presets, wrangler multi-env, TURN - - api.md (164 lines) - Meeting object API, REST endpoints, session lifecycle - - patterns.md (155 lines) - UI kit, core SDK, video grid, device selection, chat, hooks, backend integration, best practices - - gotchas.md (172 lines) - Common issues, limits, network requirements, debugging, security, performance -- Deleted: SKILL.md (1244 lines) -- All files have cross-reference sections -- **Learnings:** - - Inline code compression: combine similar examples, remove line breaks in simple patterns - - Merge related patterns into single code blocks with comments - - Consolidate security/performance sections into compact lists - -## Task - split-6 -- Split 14 Tier 2 references (1085-1241 lines each) into 5 focused files per ref -- Terraform (1241 lines): - - README.md (76), configuration.md (156), api.md (159), patterns.md (135), gotchas.md (213→200) -- Pulumi (1234 lines): README(107), config(216→200), api(194→200), patterns(268→139), gotchas(223→200) -- Sandbox (1208 lines): README(90), config(131), api(178), patterns(203→200), gotchas(156) -- Hyperdrive (1209 lines): README(62), config(133), api(137), patterns(176), gotchas(184) -- Pages (1207 lines): README(70), config(228→200), api(200), patterns(268→145), gotchas(161) -- DDoS (1197 lines): README(34), config(67), api(136), patterns(158), gotchas(114) -- Workers-for-Platforms (1181 lines): README(48), config(136), api(169), patterns(170), gotchas(130) -- Workflows (1177 lines): README(62), config(177), api(253→125), patterns(235→132), gotchas(247→136) -- Miniflare (1143 lines): README(64), config(203→200), api(144), patterns(211→200), gotchas(187) -- Workerd (1109 lines): README(47), config(185), api(199), patterns(216→200), gotchas(203→200) -- Cron-triggers (1103 lines): README(85), config(151), api(198), patterns(289→122), gotchas(277→129) -- Secrets-store (1100 lines): README(58), config(140), api(182), patterns(218→200), gotchas(129) -- Pages-functions (1100 lines): README(57), config(159), api(201→200), patterns(190), gotchas(151) -- Network-interconnect (1085 lines): README(60), config(127), api(240→200), patterns(171), gotchas(171) -- All 14 refs: 70 files total (5 per ref), all <200 lines, READMEs <110 -- **Learnings:** - - Parallel task execution for bulk operations (14 refs split concurrently) - - Aggressive compression needed: semicolon-separated properties, inline examples - - Post-split trimming pass required to meet hard <200 limit - - Pattern applies across diverse products (IaC, dev tools, security, networking) - -## Task - split-7 -- Split 30 Tier 3 references (700-1100 lines each) into 5 focused files per ref -- Files split: d1, smart-placement, tunnel, r2, kv, queues, do-storage, bot-management, realtime-sfu, api-shield, waf, browser-rendering, web-analytics, observability, durable-objects, workers-playground, containers, analytics-engine, agents-sdk, r2-data-catalog, api, turnstile, static-assets, ai-search, snippets, email-routing, spectrum, bindings, argo-smart-routing, images -- Created: 150 files (30 refs × 5 files: README, configuration, api, patterns, gotchas) -- Deleted: 30 SKILL.md files -- All files under limits: README <100, gotchas <100, config/api/patterns <150 -- **Learnings:** - - Batch processing with task agents for bulk operations - - Post-split trimming pass required for dense content (7 files trimmed) - - Compression: inline examples, compact tables, semicolon lists, reference other files - - Critical refs (storage, AI, workers) prioritized for cross-references - -## Task - trim-1 -- Trimmed main SKILL.md from 233→199 lines -- Files changed: - - skill/cloudflare/SKILL.md: Added references field to frontmatter (top 5 products), removed Summary column from all Product Index tables, removed Common Patterns and Loading References sections - - skill/cloudflare/patterns.md: NEW - extracted Common Patterns section (28 lines) - - .opencode/state/cloudflare-skill-restructure/prd.json: marked trim-1 as passes: true -- **Learnings:** - - Table compression: remove verbose columns when product names suffice - - Extract reusable content to separate files rather than delete - - Frontmatter references field helps agents prioritize key products -## Task - crossref-1 -- Verified workers/README.md has complete cross-references -- In This Reference section: links to configuration, api, patterns, gotchas -- See Also section: links to kv, d1, r2, durable-objects, queues, wrangler -- Files checked: skill/cloudflare/references/workers/README.md -- **Learnings:** - - Cross-references already implemented in split-4 task - - Verification pass confirms existing structure meets requirements - -## Task - crossref-2 -- Added cross-references to pages/README.md -- In This Reference section: links to configuration, api, patterns, gotchas (already present) -- See Also section: added links to pages-functions, d1, kv -- File modified: skill/cloudflare/references/pages/README.md (71→77 lines, well under <100 limit) -- **Learnings:** - - Cross-product references help users discover complementary services - - Pattern: Pages + D1/KV for data, Pages Functions for routing logic diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..61b7ec7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,123 @@ +# PROJECT KNOWLEDGE BASE + +**Generated:** 2026-01-12 +**Commit:** d22f19ea218c + +## OVERVIEW + +Documentation/skill repository for Cloudflare platform—structured reference docs for AI/LLM consumption. No executable code. + +## STRUCTURE + +``` +./ +└── skill/ + └── cloudflare/ + ├── SKILL.md # Main skill manifest + decision trees + ├── patterns.md # Architecture patterns + └── references/ # 60 product subdirs + └── / + ├── README.md + ├── api.md + ├── configuration.md + ├── patterns.md + └── gotchas.md +``` + +## WHERE TO LOOK + +| Task | Location | Notes | +|------|----------|-------| +| Find a product | `skill/cloudflare/SKILL.md` | Decision trees + full index | +| Product reference | `skill/cloudflare/references//` | 5-file structure | +| Platform overview | `docs/products.md` | All 101 products listed | + +## CONVENTIONS + +### VCS: jujutsu (NOT git) + +```bash +# CORRECT +jj status +jj log +jj new +jj commit -m "msg" + +# WRONG - do not use +git status # .jj/ present = use jj +``` + +### Reference File Structure + +Every product follows 5-file pattern: +- `README.md` — Overview, when to use +- `api.md` — Runtime API reference +- `configuration.md` — `wrangler.toml` + binding setup +- `patterns.md` — Usage patterns +- `gotchas.md` — Pitfalls, limitations + +Some products use monolithic `SKILL.md` instead (migration in progress): `ai-gateway`, `c3`, `email-workers`, `pipelines`, `r2-sql`, `tail-workers`, `turn`, `vectorize`, `workers-ai`, `workers-vpc`, `zaraz` + +### YAML Frontmatter + +`SKILL.md` files use frontmatter for machine parsing: +```yaml +--- +name: product-name +description: Brief description +references: + - related-product +--- +``` + +## ANTI-PATTERNS + +### SQL Injection (D1) + +```typescript +// NEVER - string interpolation +const result = await db.prepare(`SELECT * FROM users WHERE id = ${id}`).all(); + +// ALWAYS - prepared statements with bind() +const result = await db.prepare("SELECT * FROM users WHERE id = ?").bind(id).all(); +``` + +### Secrets Management + +```bash +# NEVER commit .dev.vars (contains secrets) +# NEVER hardcode secrets in code +``` + +### Resource Management + +```typescript +// ALWAYS close browser in finally block (browser-rendering) +const browser = await puppeteer.launch(); +try { + // ... +} finally { + await browser.close(); +} + +// ALWAYS call proxyToSandbox() first (sandbox) +await proxyToSandbox(); +``` + +### Configuration + +```toml +# ALWAYS set compatibility_date for new projects (workers) +compatibility_date = "2026-01-12" +``` + +### Bot Management + +JSD (JavaScript Detection) cannot be used on first page visit—requires HTML page loaded first. + +## NOTES + +- No CI/CD configured (docs-only repo) +- No linting/formatting (no code to lint) +- `.opencode/` contains plugin config, not project code +- Two index locations exist: `SKILL.md` (primary) vs `docs/products.md` (overview) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf21028 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The Cloudflare Skill Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5159cca --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Cloudflare Skill for OpenCode + +Comprehensive Cloudflare platform reference docs for AI/LLM consumption. Covers Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), networking, security, and infrastructure-as-code. + +## Install + +Local installation (current project only): + +```bash +curl -fsSL https://raw.githubusercontent.com/dmmulroy/cloudflare-skill/main/install.sh | bash +``` + +Global installation (available in all projects): + +```bash +curl -fsSL https://raw.githubusercontent.com/dmmulroy/cloudflare-skill/main/install.sh | bash -s -- --global +``` + +## Usage + +Once installed, the skill appears in OpenCode's `` list. The agent loads it automatically when working on Cloudflare tasks. + +Use the `/cloudflare` command to load the skill and get contextual guidance: + +``` +/cloudflare set up a D1 database with migrations +``` + +### Updating + +To update to the latest version: + +``` +/cloudflare --update-skill +``` + +## Structure + +The installer adds both a skill and a command: + +``` +# Skill (reference docs) +skill/cloudflare/ +├── SKILL.md # Main manifest + decision trees +├── patterns.md # Multi-product architecture patterns +└── references/ # 60 product subdirectories + └── / + ├── README.md # Overview, when to use + ├── api.md # Runtime API reference + ├── configuration.md # wrangler.toml + bindings + ├── patterns.md # Usage patterns + └── gotchas.md # Pitfalls, limitations + +# Command (slash command) +command/cloudflare.md # /cloudflare entrypoint +``` + +### Decision Trees + +The main `SKILL.md` contains decision trees for: +- Running code (Workers, Pages, Durable Objects, Workflows, Containers) +- Storage (KV, D1, R2, Queues, Vectorize) +- AI/ML (Workers AI, Vectorize, Agents SDK, AI Gateway) +- Networking (Tunnel, Spectrum, WebRTC) +- Security (WAF, DDoS, Bot Management, Turnstile) +- Media (Images, Stream, Browser Rendering) +- Infrastructure-as-code (Terraform, Pulumi) + +## Products Covered + +Workers, Pages, D1, Durable Objects, KV, R2, Queues, Hyperdrive, Workers AI, Vectorize, Agents SDK, AI Gateway, Tunnel, Spectrum, WAF, DDoS, Bot Management, Turnstile, Images, Stream, Browser Rendering, Terraform, Pulumi, and 40+ more. + +## License + +MIT - see [LICENSE](LICENSE) diff --git a/command/cloudflare.md b/command/cloudflare.md new file mode 100644 index 0000000..fdf7746 --- /dev/null +++ b/command/cloudflare.md @@ -0,0 +1,70 @@ +--- +description: Load Cloudflare skill and get contextual guidance for your task +--- + +Load the Cloudflare platform skill and help with any Cloudflare development task. + +## Workflow + +### Step 1: Check for --update-skill flag + +If $ARGUMENTS contains `--update-skill`: + +1. Determine install location by checking which exists: + - Local: `.opencode/skill/cloudflare/` + - Global: `~/.config/opencode/skill/cloudflare/` + +2. Run the appropriate install command: + ```bash + # For local installation + curl -fsSL https://raw.githubusercontent.com/dmmulroy/cloudflare-skill/main/install.sh | bash + + # For global installation + curl -fsSL https://raw.githubusercontent.com/dmmulroy/cloudflare-skill/main/install.sh | bash -s -- --global + ``` + +3. Output success message and stop (do not continue to other steps). + +### Step 2: Load cloudflare skill + +``` +skill({ name: 'cloudflare' }) +``` + +### Step 3: Identify task type from user request + +Analyze $ARGUMENTS to determine: +- **Product(s) needed** (Workers, D1, R2, Durable Objects, etc.) +- **Task type** (new project setup, feature implementation, debugging, config) + +Use decision trees in SKILL.md to select correct product. + +### Step 4: Read relevant reference files + +Based on task type, read from `references//`: + +| Task | Files to Read | +|------|---------------| +| New project | `README.md` + `configuration.md` | +| Implement feature | `README.md` + `api.md` + `patterns.md` | +| Debug/troubleshoot | `gotchas.md` | +| All-in-one (monolithic) | `SKILL.md` | + +### Step 5: Execute task + +Apply Cloudflare-specific patterns and APIs from references to complete the user's request. + +### Step 6: Summarize + +``` +=== Cloudflare Task Complete === + +Product(s): +Files referenced: + + +``` + + +$ARGUMENTS + diff --git a/docs/products.md b/docs/products.md deleted file mode 100644 index 0493d7a..0000000 --- a/docs/products.md +++ /dev/null @@ -1,100 +0,0 @@ -# Cloudflare Developer Platform (2026) - -## Compute - -- **Workers** — Serverless JS/TS/Python/Rust/WASM execution at the edge with sub-ms cold starts -- **Durable Objects** — Stateful serverless primitives with strongly-consistent transactional storage -- **Workflows** — Durable multi-step execution with automatic retries, sleep/scheduling, state persistence -- **Containers** — OCI containers as part of Workers apps, managed via Durable Objects bindings -- **Sandbox SDK** — Isolated execution environments for untrusted code (AI agents, user scripts) -- **Pages** — Full-stack deployment with Git integration, preview deployments, SSR -- **Pages Functions** — Server-side Workers code for Pages projects -- **Workers for Platforms** — Multi-tenant platform for running customer code in isolated Workers -- **Cron Triggers** — Scheduled Worker execution without HTTP requests -- **Snippets** — Lightweight JavaScript code executed at the edge for request customization (header modification, JWT validation, redirects). Limited to 5ms execution, 2MB memory, 32KB package size. Configured via filter expressions in Rules. - -## Storage & Databases - -- **KV** — Global eventually-consistent key-value store with edge caching -- **R2** — S3-compatible object storage, zero egress fees -- **D1** — Serverless SQLite with global read replication and Time Travel backups -- **Durable Objects Storage** — Co-located transactional SQLite/KV per Durable Object -- **Hyperdrive** — Connection pooling and query caching for external Postgres/MySQL -- **Queues** — Message queues with guaranteed delivery, batching, DLQs, pull consumers -- **R2 Data Catalog** — Managed Apache Iceberg catalog, turns R2 into data lakehouse -- **Analytics Engine** — Time-series database for unlimited-cardinality metrics -- **Cache Reserve** — Persistent cache for extended TTLs -- **Secrets Store** — Centralized encrypted secrets, reusable across Workers/AI Gateway - -## Data Pipelines - -- **Pipelines** — Streaming ingestion, SQL transforms, delivery to R2 as Iceberg/Parquet/JSON -- **R2 SQL** — Serverless distributed query engine for Iceberg tables in R2 - -## AI/ML - -- **Workers AI** — 50+ open-source models (LLMs, image gen, embeddings, STT) on serverless GPUs -- **AI Gateway** — Proxy with caching, rate limiting, retries, fallback, analytics for AI APIs -- **Vectorize** — Vector database for embeddings, semantic search, RAG -- **AI Search** — Managed RAG service with auto-indexing of websites/R2 -- **Agents SDK** — Framework for AI agents with state management, WebSockets, scheduling - -## Media - -- **Images** — Store, transform, optimize, deliver images at edge -- **Stream** — Live and on-demand video platform with adaptive bitrate -- **Browser Rendering** — Headless Chrome at edge for screenshots, PDFs, Puppeteer/Playwright - -## Real-Time - -- **RealtimeKit** — High-level SDKs for live video/voice with pre-built UI -- **Realtime SFU** — Low-level WebRTC selective forwarding unit -- **TURN Service** — Managed relay for WebRTC through restrictive firewalls - -## Networking - -- **Workers VPC** — Connect Workers to private networks (AWS/Azure/GCP/on-prem) via Tunnel -- **Smart Placement** — Auto-run Workers near backend services to minimize latency -- **Argo Smart Routing** — Fastest network path routing -- **Cloudflare Tunnel** — Secure outbound-only connections from private networks -- **Spectrum** — Proxy any TCP/UDP application through Cloudflare -- **Network Interconnect** — Direct physical/virtual connections to Cloudflare network - -## Security - -- **Turnstile** — Invisible CAPTCHA alternative, WCAG 2.1 AA compliant -- **WAF** — Web application firewall with managed/custom rules -- **Bot Management** — ML-based bot detection and mitigation -- **DDoS Protection** — Automatic always-on DDoS protection -- **API Shield** — API discovery, schema validation, protection - -## Email - -- **Email Routing** — Custom domain email addresses routed to existing mailboxes -- **Email Workers** — Process incoming emails with Workers code - -## Third-Party & Analytics - -- **Zaraz** — Server-side tag manager for third-party tools at edge -- **Web Analytics** — Privacy-first analytics without cookies - -## Developer Tools - -- **Wrangler** — Official CLI for Workers/Pages development and deployment -- **C3 (create-cloudflare)** — Project scaffolding CLI with templates -- **Miniflare** — Local Workers runtime simulator -- **Workerd** — Open-source Workers runtime for local/self-hosted execution -- **Workers Playground** — Browser-based IDE for testing - -## Infrastructure as Code - -- **Terraform Provider** — Manage Cloudflare via Terraform -- **Pulumi Provider** — Define Cloudflare infra in TS/Python/Go/C# -- **Cloudflare API** — RESTful API with OpenAPI spec and SDKs - -## Platform Features - -- **Bindings** — Typed APIs connecting Workers to storage/compute/services -- **Static Assets** — Host static files with Workers, CDN-served -- **Tail Workers** — Receive logs from other Workers for custom processing -- **Observability** — Built-in logging, real-time logs, metrics, tracing diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..2e61d65 --- /dev/null +++ b/install.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_URL="https://github.com/dmmulroy/cloudflare-skill.git" +SKILL_NAME="cloudflare" + +usage() { + cat </` contains a `README.md` as the entry point, which may be structured in one of two ways: + +**Multi-file format (5 files):** +| File | Purpose | When to Read | +|------|---------|--------------| +| `README.md` | Overview, when to use, getting started | **Always read first** | +| `api.md` | Runtime API, types, method signatures | Writing code | +| `configuration.md` | wrangler.toml, bindings, setup | Configuring a project | +| `patterns.md` | Common patterns, best practices | Implementation guidance | +| `gotchas.md` | Pitfalls, limitations, edge cases | Debugging, avoiding mistakes | + +**Single-file format:** All information consolidated in `README.md`. + +### Reading Order + +1. Start with `README.md` +2. Then read additional files relevant to your task (if multi-file format): + - Building feature → `api.md` + `patterns.md` + - Setting up project → `configuration.md` + - Troubleshooting → `gotchas.md` + +### Example Paths + +``` +./references/workflows/README.md # Start here for Workflows +./references/workflows/api.md # Workflow class, step methods +./references/durable-objects/gotchas.md # DO limitations +./references/workers-ai/README.md # Single-file - all Workers AI docs +``` + ## Quick Decision Trees ### "I need to run code" @@ -99,101 +133,101 @@ Need IaC? ## Product Index ### Compute & Runtime -| Product | Reference | -|---------|-----------| -| Workers | `references/workers/` | -| Pages | `references/pages/` | -| Pages Functions | `references/pages-functions/` | -| Durable Objects | `references/durable-objects/` | -| Workflows | `references/workflows/` | -| Containers | `references/containers/` | -| Workers for Platforms | `references/workers-for-platforms/` | -| Cron Triggers | `references/cron-triggers/` | -| Tail Workers | `references/tail-workers/` | -| Snippets | `references/snippets/` | -| Smart Placement | `references/smart-placement/` | +| Product | Entry File | +|---------|------------| +| Workers | `./references/workers/README.md` | +| Pages | `./references/pages/README.md` | +| Pages Functions | `./references/pages-functions/README.md` | +| Durable Objects | `./references/durable-objects/README.md` | +| Workflows | `./references/workflows/README.md` | +| Containers | `./references/containers/README.md` | +| Workers for Platforms | `./references/workers-for-platforms/README.md` | +| Cron Triggers | `./references/cron-triggers/README.md` | +| Tail Workers | `./references/tail-workers/README.md` | +| Snippets | `./references/snippets/README.md` | +| Smart Placement | `./references/smart-placement/README.md` | ### Storage & Data -| Product | Reference | -|---------|-----------| -| KV | `references/kv/` | -| D1 | `references/d1/` | -| R2 | `references/r2/` | -| Queues | `references/queues/` | -| Hyperdrive | `references/hyperdrive/` | -| DO Storage | `references/do-storage/` | -| Secrets Store | `references/secrets-store/` | -| Pipelines | `references/pipelines/` | -| R2 Data Catalog | `references/r2-data-catalog/` | -| R2 SQL | `references/r2-sql/` | +| Product | Entry File | +|---------|------------| +| KV | `./references/kv/README.md` | +| D1 | `./references/d1/README.md` | +| R2 | `./references/r2/README.md` | +| Queues | `./references/queues/README.md` | +| Hyperdrive | `./references/hyperdrive/README.md` | +| DO Storage | `./references/do-storage/README.md` | +| Secrets Store | `./references/secrets-store/README.md` | +| Pipelines | `./references/pipelines/README.md` | +| R2 Data Catalog | `./references/r2-data-catalog/README.md` | +| R2 SQL | `./references/r2-sql/README.md` | ### AI & Machine Learning -| Product | Reference | -|---------|-----------| -| Workers AI | `references/workers-ai/` | -| Vectorize | `references/vectorize/` | -| Agents SDK | `references/agents-sdk/` | -| AI Gateway | `references/ai-gateway/` | -| AI Search | `references/ai-search/` | +| Product | Entry File | +|---------|------------| +| Workers AI | `./references/workers-ai/README.md` | +| Vectorize | `./references/vectorize/README.md` | +| Agents SDK | `./references/agents-sdk/README.md` | +| AI Gateway | `./references/ai-gateway/README.md` | +| AI Search | `./references/ai-search/README.md` | ### Networking & Connectivity -| Product | Reference | -|---------|-----------| -| Tunnel | `references/tunnel/` | -| Spectrum | `references/spectrum/` | -| TURN | `references/turn/` | -| Network Interconnect | `references/network-interconnect/` | -| Argo Smart Routing | `references/argo-smart-routing/` | -| Workers VPC | `references/workers-vpc/` | +| Product | Entry File | +|---------|------------| +| Tunnel | `./references/tunnel/README.md` | +| Spectrum | `./references/spectrum/README.md` | +| TURN | `./references/turn/README.md` | +| Network Interconnect | `./references/network-interconnect/README.md` | +| Argo Smart Routing | `./references/argo-smart-routing/README.md` | +| Workers VPC | `./references/workers-vpc/README.md` | ### Security -| Product | Reference | -|---------|-----------| -| WAF | `references/waf/` | -| DDoS Protection | `references/ddos/` | -| Bot Management | `references/bot-management/` | -| API Shield | `references/api-shield/` | -| Turnstile | `references/turnstile/` | +| Product | Entry File | +|---------|------------| +| WAF | `./references/waf/README.md` | +| DDoS Protection | `./references/ddos/README.md` | +| Bot Management | `./references/bot-management/README.md` | +| API Shield | `./references/api-shield/README.md` | +| Turnstile | `./references/turnstile/README.md` | ### Media & Content -| Product | Reference | -|---------|-----------| -| Images | `references/images/` | -| Stream | `references/stream/` | -| Browser Rendering | `references/browser-rendering/` | -| Zaraz | `references/zaraz/` | +| Product | Entry File | +|---------|------------| +| Images | `./references/images/README.md` | +| Stream | `./references/stream/README.md` | +| Browser Rendering | `./references/browser-rendering/README.md` | +| Zaraz | `./references/zaraz/README.md` | ### Real-Time Communication -| Product | Reference | -|---------|-----------| -| RealtimeKit | `references/realtimekit/` | -| Realtime SFU | `references/realtime-sfu/` | +| Product | Entry File | +|---------|------------| +| RealtimeKit | `./references/realtimekit/README.md` | +| Realtime SFU | `./references/realtime-sfu/README.md` | ### Developer Tools -| Product | Reference | -|---------|-----------| -| Wrangler | `references/wrangler/` | -| Miniflare | `references/miniflare/` | -| C3 | `references/c3/` | -| Observability | `references/observability/` | -| Analytics Engine | `references/analytics-engine/` | -| Web Analytics | `references/web-analytics/` | -| Sandbox | `references/sandbox/` | -| Workerd | `references/workerd/` | -| Workers Playground | `references/workers-playground/` | +| Product | Entry File | +|---------|------------| +| Wrangler | `./references/wrangler/README.md` | +| Miniflare | `./references/miniflare/README.md` | +| C3 | `./references/c3/README.md` | +| Observability | `./references/observability/README.md` | +| Analytics Engine | `./references/analytics-engine/README.md` | +| Web Analytics | `./references/web-analytics/README.md` | +| Sandbox | `./references/sandbox/README.md` | +| Workerd | `./references/workerd/README.md` | +| Workers Playground | `./references/workers-playground/README.md` | ### Infrastructure as Code -| Product | Reference | -|---------|-----------| -| Pulumi | `references/pulumi/` | -| Terraform | `references/terraform/` | -| API | `references/api/` | +| Product | Entry File | +|---------|------------| +| Pulumi | `./references/pulumi/README.md` | +| Terraform | `./references/terraform/README.md` | +| API | `./references/api/README.md` | ### Other Services -| Product | Reference | -|---------|-----------| -| Email Routing | `references/email-routing/` | -| Email Workers | `references/email-workers/` | -| Static Assets | `references/static-assets/` | -| Bindings | `references/bindings/` | -| Cache Reserve | `references/cache-reserve/` | +| Product | Entry File | +|---------|------------| +| Email Routing | `./references/email-routing/README.md` | +| Email Workers | `./references/email-workers/README.md` | +| Static Assets | `./references/static-assets/README.md` | +| Bindings | `./references/bindings/README.md` | +| Cache Reserve | `./references/cache-reserve/README.md` | diff --git a/skill/cloudflare/patterns.md b/skill/cloudflare/patterns.md deleted file mode 100644 index a874342..0000000 --- a/skill/cloudflare/patterns.md +++ /dev/null @@ -1,28 +0,0 @@ -# Common Cloudflare Patterns - -Reusable architecture patterns combining multiple products. - -## Full-Stack Web App -``` -Pages (frontend) + Pages Functions (API) + D1 (database) -``` - -## Real-Time Collaboration -``` -Workers (entry) + Durable Objects (state) + WebSockets -``` - -## RAG / AI Search -``` -Workers AI (embeddings + LLM) + Vectorize (vector store) + D1/KV (metadata) -``` - -## Background Processing -``` -Workers (producer) + Queues (buffer) + Workers (consumer) + R2 (results) -``` - -## Multi-Region Database Access -``` -Hyperdrive (pooling/caching) → Existing Postgres/MySQL -``` diff --git a/skill/cloudflare/references/ai-gateway/SKILL.md b/skill/cloudflare/references/ai-gateway/README.md similarity index 100% rename from skill/cloudflare/references/ai-gateway/SKILL.md rename to skill/cloudflare/references/ai-gateway/README.md diff --git a/skill/cloudflare/references/ai-search/README.md b/skill/cloudflare/references/ai-search/README.md index 13c39e1..081feec 100644 --- a/skill/cloudflare/references/ai-search/README.md +++ b/skill/cloudflare/references/ai-search/README.md @@ -4,10 +4,10 @@ Expert guidance for implementing Cloudflare AI Search (formerly AutoRAG), Cloudf ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/analytics-engine/README.md b/skill/cloudflare/references/analytics-engine/README.md index 3560ddc..43272c8 100644 --- a/skill/cloudflare/references/analytics-engine/README.md +++ b/skill/cloudflare/references/analytics-engine/README.md @@ -4,10 +4,10 @@ Expert guidance for implementing unlimited-cardinality analytics at scale using ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/api-shield/README.md b/skill/cloudflare/references/api-shield/README.md index f58d9e0..72f100e 100644 --- a/skill/cloudflare/references/api-shield/README.md +++ b/skill/cloudflare/references/api-shield/README.md @@ -4,10 +4,10 @@ Expert guidance for API Shield - comprehensive API security suite for discovery, ## In This Reference -- **[configuration.md](configuration.md)** - Setup, session identifiers, rules, token/mTLS configs -- **[api.md](api.md)** - Endpoint management, discovery, validation APIs, GraphQL operations -- **[patterns.md](patterns.md)** - Common patterns, progressive rollout, OWASP mappings, workflows -- **[gotchas.md](gotchas.md)** - Troubleshooting, false positives, performance, best practices +- **[configuration.md](./configuration.md)** - Setup, session identifiers, rules, token/mTLS configs +- **[api.md](./api.md)** - Endpoint management, discovery, validation APIs, GraphQL operations +- **[patterns.md](./patterns.md)** - Common patterns, progressive rollout, OWASP mappings, workflows +- **[gotchas.md](./gotchas.md)** - Troubleshooting, false positives, performance, best practices ## Quick Start diff --git a/skill/cloudflare/references/api/README.md b/skill/cloudflare/references/api/README.md index b8130e9..6c7d2cc 100644 --- a/skill/cloudflare/references/api/README.md +++ b/skill/cloudflare/references/api/README.md @@ -11,10 +11,10 @@ Use when working with: ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/argo-smart-routing/README.md b/skill/cloudflare/references/argo-smart-routing/README.md index 36cb51e..87dda4b 100644 --- a/skill/cloudflare/references/argo-smart-routing/README.md +++ b/skill/cloudflare/references/argo-smart-routing/README.md @@ -6,10 +6,10 @@ Cloudflare Argo Smart Routing is a performance optimization service that detects ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/bindings/README.md b/skill/cloudflare/references/bindings/README.md index 43a5abd..22c6d70 100644 --- a/skill/cloudflare/references/bindings/README.md +++ b/skill/cloudflare/references/bindings/README.md @@ -4,10 +4,10 @@ Expert guidance on Cloudflare Workers Bindings - the runtime APIs that connect W ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/browser-rendering/README.md b/skill/cloudflare/references/browser-rendering/README.md index bad3ea4..7f6dbf7 100644 --- a/skill/cloudflare/references/browser-rendering/README.md +++ b/skill/cloudflare/references/browser-rendering/README.md @@ -6,10 +6,10 @@ ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/c3/SKILL.md b/skill/cloudflare/references/c3/README.md similarity index 100% rename from skill/cloudflare/references/c3/SKILL.md rename to skill/cloudflare/references/c3/README.md diff --git a/skill/cloudflare/references/containers/README.md b/skill/cloudflare/references/containers/README.md index 513658d..28403dc 100644 --- a/skill/cloudflare/references/containers/README.md +++ b/skill/cloudflare/references/containers/README.md @@ -6,10 +6,10 @@ Use when working with Cloudflare Containers: deploying containerized apps on Wor ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/durable-objects/README.md b/skill/cloudflare/references/durable-objects/README.md index 7c6cced..d35a8a8 100644 --- a/skill/cloudflare/references/durable-objects/README.md +++ b/skill/cloudflare/references/durable-objects/README.md @@ -11,6 +11,42 @@ Durable Objects combine compute with storage in globally-unique, strongly-consis - **Stateful serverless**: In-memory state + persistent storage - **Single-threaded**: Serial request processing (no race conditions) +## When to Use DOs + +Use DOs for **stateful coordination**, not stateless request handling: +- **Coordination**: Multiple clients interacting with shared state (chat rooms, multiplayer games) +- **Strong consistency**: Operations must serialize to avoid races (booking systems, inventory) +- **Per-entity storage**: Each user/tenant/resource needs isolated database (multi-tenant SaaS) +- **Persistent connections**: Long-lived WebSockets that survive across requests +- **Per-entity scheduled work**: Each entity needs its own timer (subscription renewals, game timeouts) + +## When NOT to Use DOs + +| Scenario | Use Instead | +|----------|-------------| +| Stateless request handling | Workers | +| Maximum global distribution | Workers | +| High fan-out (independent requests) | Workers | +| Global singleton handling all traffic | Shard across multiple DOs | +| High-frequency pub/sub | Queues | +| Long-running continuous processes | Workers + Alarms | +| Chatty microservice (every request) | Reconsider architecture | +| Eventual consistency OK, read-heavy | KV | +| Relational queries across entities | D1 | + +## Design Heuristics + +Model each DO around your **atom of coordination**—the logical unit needing serialized access (user, room, document, session). + +| Characteristic | Feels Right | Question It | Reconsider | +|----------------|-------------|-------------|------------| +| Requests/sec (sustained) | < 100 | 100-500 | > 500 | +| Storage keys | < 100 | 100-1000 | > 1000 | +| Total state size | < 10MB | 10MB-100MB | > 1GB | +| Alarm frequency | Minutes-hours | Every 30s | Every few seconds | +| WebSocket duration | Short bursts | Hours (hibernating) | Days always-on | +| Fan-out from this DO | Never/rarely | To < 10 DOs | To 100+ DOs | + ## Core Concepts ### Class Structure diff --git a/skill/cloudflare/references/durable-objects/api.md b/skill/cloudflare/references/durable-objects/api.md index df8b076..459bcd8 100644 --- a/skill/cloudflare/references/durable-objects/api.md +++ b/skill/cloudflare/references/durable-objects/api.md @@ -8,6 +8,10 @@ import { DurableObject } from "cloudflare:workers"; export class MyDO extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); + // Initialize storage/run migrations before any requests + ctx.blockConcurrencyWhile(async () => { + await this.migrate(); + }); } async myMethod(arg: string): Promise { return arg; } async alarm() { } @@ -15,6 +19,62 @@ export class MyDO extends DurableObject { } ``` +## Concurrency Model + +### Input/Output Gates + +DOs are single-threaded but async/await allows request interleaving. The runtime uses **gates** to prevent data races: + +**Input gates** block new events while synchronous JS executes. Awaiting async ops opens the gate, allowing interleaving. Storage operations provide special protection. + +**Output gates** hold outgoing network messages until pending storage writes complete—clients never see confirmation of unpersisted data. + +### Write Coalescing + +Multiple storage writes without intervening `await` are automatically batched into a single atomic transaction: + +```typescript +async transfer(fromId: string, toId: string, amount: number) { + // All three writes commit together atomically + this.ctx.storage.sql.exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromId); + this.ctx.storage.sql.exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toId); + this.ctx.storage.sql.exec("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)", fromId, toId, amount); +} +``` + +### blockConcurrencyWhile() + +Guarantees no other events process until callback completes. **Use sparingly**—only for initialization/migrations. + +```typescript +constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + ctx.blockConcurrencyWhile(async () => { + const version = this.ctx.storage.sql.exec<{ version: number }>("PRAGMA user_version").one()?.version ?? 0; + if (version < 1) { + this.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS data (...); PRAGMA user_version = 1;`); + } + }); +} +``` + +**Anti-pattern**: Using `blockConcurrencyWhile()` on every request or across I/O (fetch, KV, R2) severely degrades throughput. For regular requests, rely on input/output gates and write coalescing. + +### Optimistic Locking (Non-Storage I/O) + +Input gates only protect during storage ops. External I/O like `fetch()` allows interleaving. Use check-and-set: + +```typescript +async updateFromExternal(key: string) { + const version = this.ctx.storage.sql.exec<{ v: number }>("SELECT version as v FROM data WHERE key = ?", key).one()?.v; + const externalData = await fetch("https://api.example.com/data"); // Other requests can interleave here + const newVersion = this.ctx.storage.sql.exec<{ v: number }>("SELECT version as v FROM data WHERE key = ?", key).one()?.v; + + if (version !== newVersion) throw new Error("Concurrent modification"); + this.ctx.storage.sql.exec("UPDATE data SET value = ?, version = version + 1 WHERE key = ?", await externalData.text(), key); +} +``` + ## SQLite Storage ```typescript diff --git a/skill/cloudflare/references/durable-objects/configuration.md b/skill/cloudflare/references/durable-objects/configuration.md index d802fc2..9c9a343 100644 --- a/skill/cloudflare/references/durable-objects/configuration.md +++ b/skill/cloudflare/references/durable-objects/configuration.md @@ -24,16 +24,31 @@ ```jsonc { "migrations": [ - { "tag": "v1", "new_sqlite_classes": ["MyDO"] }, // Create SQLite (recommended) - // { "tag": "v1", "new_classes": ["MyDO"] }, // Create KV (paid only) - { "tag": "v2", "renamed_classes": [{ "from": "Old", "to": "New" }] }, - { "tag": "v3", "transferred_classes": [{ "from": "Src", "from_script": "old", "to": "Dest" }] }, - { "tag": "v4", "deleted_classes": ["Obsolete"] } // Destroys ALL data! + // Create new SQLite-backed class (recommended for new classes) + { "tag": "v1", "new_sqlite_classes": ["MyDO"] }, + + // Create new KV-backed class (legacy, paid only) + // { "tag": "v1", "new_classes": ["MyDO"] }, + + // Rename class - preserves all data and object IDs + { "tag": "v2", "renamed_classes": [{ "from": "OldName", "to": "NewName" }] }, + + // Transfer between scripts - requires coordination + { "tag": "v3", "transferred_classes": [{ "from": "Src", "from_script": "old-worker", "to": "Dest" }] }, + + // DELETE - DESTROYS ALL DATA PERMANENTLY, NO RECOVERY + { "tag": "v4", "deleted_classes": ["Obsolete"] } ] } ``` -Tags unique/sequential, no rollback, auto-applied on deploy, test with `--dry-run` +**Migration rules:** +- Tags must be unique and sequential +- No rollback mechanism—test with `--dry-run` first +- Auto-applied on deploy +- `renamed_classes` preserves data and IDs +- `deleted_classes` is irreversible—all storage gone +- Transfers between scripts require both scripts deployed with coordinated migrations ## Advanced @@ -69,6 +84,57 @@ type DurableObjectNamespace = { }; ``` +## Testing with Vitest + +```typescript +// vitest.config.ts +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { wrangler: { configPath: "./wrangler.toml" } }, + }, + }, +}); +``` + +```typescript +// test/my-do.test.ts +import { env, runInDurableObject, runDurableObjectAlarm } from "cloudflare:test"; +import { describe, it, expect } from "vitest"; + +describe("MyDO", () => { + it("handles RPC methods", async () => { + const id = env.MY_DO.idFromName("test"); + const stub = env.MY_DO.get(id); + + const result = await stub.myMethod("test-arg"); + expect(result).toBe("test-arg"); + }); + + it("can access storage directly", async () => { + const id = env.MY_DO.idFromName("test"); + const stub = env.MY_DO.get(id); + + await runInDurableObject(stub, async (instance, state) => { + const count = state.storage.sql + .exec<{ count: number }>("SELECT COUNT(*) as count FROM data") + .one(); + expect(count.count).toBe(0); + }); + }); + + it("can trigger alarms", async () => { + const id = env.MY_DO.idFromName("test"); + const stub = env.MY_DO.get(id); + + const alarmRan = await runDurableObjectAlarm(stub); + expect(alarmRan).toBe(false); // No alarm scheduled + }); +}); +``` + ## Commands ```bash diff --git a/skill/cloudflare/references/durable-objects/gotchas.md b/skill/cloudflare/references/durable-objects/gotchas.md index e9dff8b..f339099 100644 --- a/skill/cloudflare/references/durable-objects/gotchas.md +++ b/skill/cloudflare/references/durable-objects/gotchas.md @@ -2,45 +2,157 @@ ## Limits -**SQLite**: 10GB/DO, 5GB total (Free) / unlimited (Paid), 2MB key+value, 30s CPU (300s max), 500 classes (Paid) / 100 (Free), 100 SQL cols, 100KB statement, 32MiB WebSocket msg +| Resource | Free | Paid | +|----------|------|------| +| Storage per DO | 10GB (SQLite) | 10GB (SQLite) | +| Total storage | 5GB | Unlimited | +| DO classes | 100 | 500 | +| Requests/sec/DO | ~1000 | ~1000 | +| CPU time | 30s default, 300s max | 30s default, 300s max | +| WebSocket message | 32MiB | 32MiB | +| SQL columns | 100 | 100 | +| SQL statement | 100KB | 100KB | +| Key+value size | 2MB | 2MB | -**General**: ~1000 req/s/DO, unlimited DOs, unlimited WebSockets (within 128MB memory) +## Billing Gotchas -## Common Issues +### Duration Billing Trap +DOs bill for **wall-clock time** while active, not CPU time. WebSocket open 8 hours = 8 hours duration billing, even if DO processed 50 small messages. -**DO overloaded**: 503 errors → shard across DOs with random IDs -**Storage quota**: Write failures → upgrade or cleanup alarms -**CPU exceeded**: Terminated → increase `limits.cpu_ms` or chunk work -**WebSockets disconnect**: Eviction → use hibernation or reconnection -**Migration failed**: Deploy error → check tag uniqueness/class names -**RPC not found**: compatibility_date < 2024-04-03 → update or use fetch -**One alarm limit**: Need multiple → use event queue pattern -**Constructor on wake**: Expensive init → lazy methods or cache +**Fix**: Use Hibernatable WebSockets API. DO sleeps while maintaining connections, only wakes (and bills) when messages arrive. -## Debugging +### storage.list() on Every Request +Storage reads are cheap but not free. Calling `storage.list()` or multiple `storage.get()` on every request adds up. -```bash -npx wrangler dev # Local -npx wrangler dev --remote # Prod DOs -npx wrangler tail # Logs -npx wrangler durable-objects list -``` +**Fix**: Profile actual usage. Options: +- `storage.get(['key1', 'key2', 'key3'])` - cheapest if only need specific keys +- `storage.list()` once on wake, cache in memory - cheapest if serving many requests per wake cycle +- Single `storage.get('allData')` with combined object - cheapest if often need multiple keys together -```typescript -this.ctx.storage.sql.databaseSize // Storage size -cursor.rowsRead, cursor.rowsWritten // Query stats -``` +### Alarm Recursion +Scheduling `setAlarm()` every 5 minutes = 288 wake-ups/day × minimum billable duration. Across thousands of DOs, you're waking them all whether work exists or not. + +**Fix**: Only schedule alarms when actual work is pending. Check if alarm is needed before setting. + +### WebSocket Never Closes +If users close browser tabs without proper disconnect and you don't handle it, connection stays "open" from DO's perspective, preventing hibernation. + +**Fix**: +1. Handle `webSocketClose` and `webSocketError` events +2. Implement heartbeat/ping-pong to detect dead connections +3. Use Hibernatable WebSockets API properly + +### Singleton vs Sharding +Global singleton DO handling all traffic = bottleneck + continuous duration billing (never hibernates). + +| Design | Cost Pattern | +|--------|--------------| +| One global DO | Never hibernates, continuous billing | +| Per-user DO | Each only wakes for their requests, most hibernate | +| Per-user-per-hour | Many cold starts, many minimum durations | + +**Fix**: Use per-entity DOs (per-user, per-room, per-document). They hibernate between activity. + +### Batching Reads +Five separate `storage.get()` calls > one `storage.get(['k1','k2','k3','k4','k5'])`. Each operation has overhead. + +**Fix**: Batch reads/writes. Writes without intervening `await` are automatically coalesced into single atomic transaction. + +### Hibernation State Loss +In-memory state is **lost** when DO hibernates or evicts. Waking DO reconstructs from storage. + +**Fix**: +1. Store all important state in SQLite storage +2. Use `blockConcurrencyWhile()` in constructor to load state on wake +3. Cache in memory for current wake cycle only +4. Accept every wake is potentially "cold" + +### Fan-Out Tax +Event notifying 1,000 DOs = 1,000 DO invocations billed immediately. Queue pattern doesn't reduce invocations but provides retries and batching. + +**Fix**: For time-sensitive, accept cost. For deferrable, use Queues for retry/dead-letter handling. + +### Idempotency Key Explosion +Creating one DO per idempotency key (used once) = millions of single-use DOs that persist until deleted. + +**Fix**: +1. Hash idempotency keys into N sharded buckets +2. Store records as rows in single DO's SQLite table +3. Implement TTL cleanup via alarms +4. Consider if KV is sufficient (if strong consistency not needed) + +### Storage Compaction +Individual writes billed per-operation. Writing 100 events individually = 100× the write operations vs batching. + +**Fix**: Batch writes. Multiple `INSERT` statements without intervening `await` coalesce into single transaction. + +### waitUntil() Behavior +`ctx.waitUntil()` keeps DO alive (billed) until promises resolve. Waiting for slow external calls = paying for wait time. + +**Fix**: For true background work, use alarms or Queues instead of `waitUntil()`. + +### KV vs DO Storage +For read-heavy, write-rare, eventually-consistent-OK data: **KV is cheaper**. + +| | KV | DO Storage | +|-|----|----| +| Reads | Global edge cache, cheap | Every read hits DO compute | +| Writes | ~60s propagation | Immediate consistency | +| Use case | Config, sessions, cache | Read-modify-write, coordination | + +## Common Issues + +| Issue | Cause | Fix | +|-------|-------|-----| +| DO overloaded (503) | Single DO bottleneck | Shard across DOs with random/deterministic IDs | +| Storage quota exceeded | Write failures | Upgrade plan or cleanup via alarms | +| CPU exceeded | Terminated mid-request | Increase `limits.cpu_ms` or chunk work | +| WebSockets disconnect | Eviction | Use hibernation + reconnection logic | +| Migration failed | Deploy error | Check tag uniqueness, class names, use `--dry-run` | +| RPC not found | Old compatibility_date | Update to >= 2024-04-03 or use fetch | +| One alarm limit | Need multiple timers | Use event queue pattern (store events, single alarm) | +| Constructor expensive | Slow cold starts | Lazy load in methods, cache after first load | ## RPC vs Fetch -**RPC** (recommended): Type-safe, simpler, faster, needs compatibility_date >= 2024-04-03 -**Fetch**: HTTP semantics, legacy, proxying +| | RPC | Fetch | +|-|-----|-------| +| Type safety | Full TypeScript support | Manual parsing | +| Simplicity | Direct method calls | HTTP request/response | +| Performance | Slightly faster | HTTP overhead | +| Requirement | compatibility_date >= 2024-04-03 | Always works | +| Use case | **Default choice** | Legacy, proxying | ```typescript -stub.myMethod(arg) // RPC -stub.fetch(new Request("http://do/endpoint")) // Fetch +// RPC (recommended) +const result = await stub.myMethod(arg); + +// Fetch (legacy) +const response = await stub.fetch(new Request("http://do/endpoint")); ``` ## Migration Gotchas -Tags unique/sequential, no rollback, test with `--dry-run`, `deleted_classes` destroys data, transfers need coordination, renames preserve data/IDs +- Tags must be unique and sequential +- No rollback mechanism +- `deleted_classes` **destroys ALL data** permanently +- Test with `--dry-run` before production deploy +- Transfers between scripts need coordination +- Renames preserve data and IDs + +## Debugging + +```bash +npx wrangler dev # Local development +npx wrangler dev --remote # Test against production DOs +npx wrangler tail # Stream logs +npx wrangler durable-objects list +npx wrangler durable-objects info +``` + +```typescript +// Storage diagnostics +this.ctx.storage.sql.databaseSize // Current storage usage +cursor.rowsRead // Rows scanned +cursor.rowsWritten // Rows modified +``` diff --git a/skill/cloudflare/references/durable-objects/patterns.md b/skill/cloudflare/references/durable-objects/patterns.md index 9c33274..94f0962 100644 --- a/skill/cloudflare/references/durable-objects/patterns.md +++ b/skill/cloudflare/references/durable-objects/patterns.md @@ -1,5 +1,143 @@ # Durable Objects Patterns +## Parent-Child Relationships + +Don't put all data in a single DO. For hierarchical data (workspaces → projects, game servers → matches), create separate child DOs. Parent coordinates and tracks children; children handle own state independently. + +```typescript +export class GameServer extends DurableObject { + async createMatch(matchId: string): Promise { + // Store child reference in parent + this.ctx.storage.sql.exec( + "INSERT INTO matches (id, created_at, status) VALUES (?, ?, ?)", + matchId, Date.now(), "active" + ); + return matchId; + } + + async routeToMatch(matchId: string, playerId: string, action: string) { + // Route to child DO - operations on different children run in parallel + const childId = this.env.MATCH.idFromName(matchId); + const child = this.env.MATCH.get(childId); + return await child.handleAction(playerId, action); + } + + async listMatches(): Promise { + // Query parent only - children stay hibernated + return this.ctx.storage.sql + .exec<{ id: string }>("SELECT id FROM matches WHERE status = ?", "active") + .toArray() + .map(r => r.id); + } +} +``` + +Benefits: parallelism across children, each child has own SQLite database, listing doesn't wake children. + +## Fleet Pattern (Hierarchical DOs) + +URL-based hierarchy creates infinite nesting of manager/agent relationships. Each path segment (`/team/project/task`) maps to a unique DO via `idFromName()`. + +```typescript +// Worker: Route all requests based on URL path +app.all('*', async (c) => { + const path = new URL(c.req.url).pathname; + const parts = path.split('/').filter(Boolean); + const doName = parts.length === 0 ? '/' : `/${parts.join('/')}`; + + const id = c.env.FLEET_DO.idFromName(doName); + return c.env.FLEET_DO.get(id).fetch(c.req.raw); +}); + +// Single unified DO class handles both manager and agent roles +export class FleetDO extends DurableObject { + async deleteWithCascade() { + const data = await this.ctx.storage.get<{ agents: string[] }>('data'); + const myPath = /* derive from context */; + + // Cascade delete to all children + for (const agent of data?.agents || []) { + const childPath = myPath === '/' ? `/${agent}` : `${myPath}/${agent}`; + const child = this.env.FLEET_DO.get(this.env.FLEET_DO.idFromName(childPath)); + await child.fetch(new Request('https://internal' + childPath, { method: 'DELETE' })); + } + + await this.ctx.storage.deleteAll(); + } +} +``` + +Use cases: collaborative IDEs (file per DO), distributed task runners, IoT device management, game server infrastructure. + +## Per-User DO Pattern + +One DO per user for settings, presence, inbox, profile data. Deterministic routing via `idFromName(userId)`. + +```typescript +export class UserDO extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + ctx.blockConcurrencyWhile(async () => { + this.ctx.storage.sql.exec(` + CREATE TABLE IF NOT EXISTS profile (key TEXT PRIMARY KEY, value TEXT); + CREATE TABLE IF NOT EXISTS inbox (id TEXT PRIMARY KEY, data TEXT, created_at INTEGER); + `); + }); + } + + async getProfile(): Promise> { + return Object.fromEntries( + this.ctx.storage.sql.exec<{ key: string; value: string }>("SELECT * FROM profile").toArray() + .map(r => [r.key, r.value]) + ); + } + + async updateProfile(updates: Record) { + for (const [key, value] of Object.entries(updates)) { + this.ctx.storage.sql.exec( + "INSERT OR REPLACE INTO profile (key, value) VALUES (?, ?)", key, value + ); + } + this.broadcast({ type: 'profile_updated', data: updates }); + } + + private broadcast(msg: object) { + const payload = JSON.stringify(msg); + for (const ws of this.ctx.getWebSockets()) ws.send(payload); + } +} + +// Worker +const id = env.USER_DO.idFromName(userId); // deterministic per-user routing +const user = env.USER_DO.get(id); +``` + +Benefits: natural ownership boundary, hibernates between user activity, WebSocket for real-time updates. + +## Colo-Aware Sharding + +Use `request.cf.colo` for geographic distribution. Rate limit per-colo before hitting DO. + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const userId = new URL(request.url).searchParams.get("userId") || "unknown"; + const colo = request.cf?.colo || "unknown"; + const shardKey = `${colo}:${userId}`; + + // Rate limit per-colo (counters not shared across datacenters) + const { success } = await env.RATE_LIMITER.limit({ key: shardKey }); + if (!success) return new Response("Rate limited", { status: 429 }); + + // Route to colo-aware DO shard + const stub = env.MY_DO.get(env.MY_DO.idFromName(shardKey)); + return await stub.fetch(request); + } +} +``` + +`request.cf.colo` returns IATA airport code (e.g., "SFO", "LHR"). Useful for high-throughput systems needing geographic awareness. + ## Rate Limiting ```typescript @@ -102,6 +240,10 @@ async alarm() { ## Best Practices +**Design for Hibernation**: DOs should sleep by default, wake for meaningful work, then sleep again. All important state must persist to storage—in-memory state is lost on eviction. Use `blockConcurrencyWhile()` in constructor to reload state on wake. Design so any instance could disappear and reconstruct from storage. + +**Atom of Coordination**: Each DO should own ONE logical unit (user, room, document, session). If data spans multiple boundaries or doesn't have natural ownership, DO may be wrong choice. + **Design**: Keep objects focused, use `idFromName()` for coordination, `newUniqueId()` for sharding, minimize constructor work, leverage WebSocket hibernation **Storage**: Prefer SQLite, create indexes judiciously, batch with transactions, set alarms for cleanup, use PITR before risky ops diff --git a/skill/cloudflare/references/email-routing/README.md b/skill/cloudflare/references/email-routing/README.md index eec3844..111d5b9 100644 --- a/skill/cloudflare/references/email-routing/README.md +++ b/skill/cloudflare/references/email-routing/README.md @@ -8,10 +8,10 @@ Cloudflare Email Routing enables custom email addresses for your domain that rou ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/email-workers/SKILL.md b/skill/cloudflare/references/email-workers/README.md similarity index 100% rename from skill/cloudflare/references/email-workers/SKILL.md rename to skill/cloudflare/references/email-workers/README.md diff --git a/skill/cloudflare/references/images/README.md b/skill/cloudflare/references/images/README.md index d56682b..a7164c0 100644 --- a/skill/cloudflare/references/images/README.md +++ b/skill/cloudflare/references/images/README.md @@ -4,10 +4,10 @@ ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/network-interconnect/README.md b/skill/cloudflare/references/network-interconnect/README.md index dd0af70..542aa44 100644 --- a/skill/cloudflare/references/network-interconnect/README.md +++ b/skill/cloudflare/references/network-interconnect/README.md @@ -54,7 +54,7 @@ Private, high-performance connectivity to Cloudflare's network. **Enterprise-onl ## See Also -- [configuration.md](configuration.md) - BGP, routing, setup -- [api.md](api.md) - API endpoints, SDKs -- [patterns.md](patterns.md) - HA, hybrid cloud, failover -- [gotchas.md](gotchas.md) - Troubleshooting, limits +- [configuration.md](./configuration.md) - BGP, routing, setup +- [api.md](./api.md) - API endpoints, SDKs +- [patterns.md](./patterns.md) - HA, hybrid cloud, failover +- [gotchas.md](./gotchas.md) - Troubleshooting, limits diff --git a/skill/cloudflare/references/network-interconnect/api.md b/skill/cloudflare/references/network-interconnect/api.md index 581c309..68794f2 100644 --- a/skill/cloudflare/references/network-interconnect/api.md +++ b/skill/cloudflare/references/network-interconnect/api.md @@ -1,6 +1,6 @@ # CNI API Reference -See [README.md](README.md) for overview. +See [README.md](./README.md) for overview. ## Base diff --git a/skill/cloudflare/references/network-interconnect/configuration.md b/skill/cloudflare/references/network-interconnect/configuration.md index 1aec85c..4910132 100644 --- a/skill/cloudflare/references/network-interconnect/configuration.md +++ b/skill/cloudflare/references/network-interconnect/configuration.md @@ -1,6 +1,6 @@ # CNI Configuration -See [README.md](README.md) for overview. +See [README.md](./README.md) for overview. ## Workflow (2-4 weeks) diff --git a/skill/cloudflare/references/network-interconnect/gotchas.md b/skill/cloudflare/references/network-interconnect/gotchas.md index ef559f4..ae425c7 100644 --- a/skill/cloudflare/references/network-interconnect/gotchas.md +++ b/skill/cloudflare/references/network-interconnect/gotchas.md @@ -1,6 +1,6 @@ # CNI Gotchas & Troubleshooting -See [README.md](README.md) for overview. +See [README.md](./README.md) for overview. ## Limitations diff --git a/skill/cloudflare/references/network-interconnect/patterns.md b/skill/cloudflare/references/network-interconnect/patterns.md index 494e3c0..1d5ef61 100644 --- a/skill/cloudflare/references/network-interconnect/patterns.md +++ b/skill/cloudflare/references/network-interconnect/patterns.md @@ -1,6 +1,6 @@ # CNI Patterns -See [README.md](README.md) for overview. +See [README.md](./README.md) for overview. ## High Availability diff --git a/skill/cloudflare/references/observability/README.md b/skill/cloudflare/references/observability/README.md index 6808c67..16d0744 100644 --- a/skill/cloudflare/references/observability/README.md +++ b/skill/cloudflare/references/observability/README.md @@ -8,10 +8,10 @@ ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/pipelines/SKILL.md b/skill/cloudflare/references/pipelines/README.md similarity index 100% rename from skill/cloudflare/references/pipelines/SKILL.md rename to skill/cloudflare/references/pipelines/README.md diff --git a/skill/cloudflare/references/r2-data-catalog/README.md b/skill/cloudflare/references/r2-data-catalog/README.md index f25307a..9b04b1e 100644 --- a/skill/cloudflare/references/r2-data-catalog/README.md +++ b/skill/cloudflare/references/r2-data-catalog/README.md @@ -8,10 +8,10 @@ R2 Data Catalog is a managed Apache Iceberg REST catalog built directly into R2 ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/r2-sql/SKILL.md b/skill/cloudflare/references/r2-sql/README.md similarity index 100% rename from skill/cloudflare/references/r2-sql/SKILL.md rename to skill/cloudflare/references/r2-sql/README.md diff --git a/skill/cloudflare/references/realtime-sfu/README.md b/skill/cloudflare/references/realtime-sfu/README.md index 80e9e58..c521a72 100644 --- a/skill/cloudflare/references/realtime-sfu/README.md +++ b/skill/cloudflare/references/realtime-sfu/README.md @@ -4,10 +4,10 @@ Expert guidance for building real-time audio/video/data applications using Cloud ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, environment variables, Wrangler config -- **[api.md](api.md)** - Sessions, tracks, endpoints, request/response patterns -- **[patterns.md](patterns.md)** - Architecture patterns, use cases, integration examples -- **[gotchas.md](gotchas.md)** - Common issues, debugging, performance, security +- **[configuration.md](./configuration.md)** - Setup, deployment, environment variables, Wrangler config +- **[api.md](./api.md)** - Sessions, tracks, endpoints, request/response patterns +- **[patterns.md](./patterns.md)** - Architecture patterns, use cases, integration examples +- **[gotchas.md](./gotchas.md)** - Common issues, debugging, performance, security ## Quick Start diff --git a/skill/cloudflare/references/realtimekit/gotchas.md b/skill/cloudflare/references/realtimekit/gotchas.md index 2a5e64d..94597b5 100644 --- a/skill/cloudflare/references/realtimekit/gotchas.md +++ b/skill/cloudflare/references/realtimekit/gotchas.md @@ -166,7 +166,7 @@ meeting.chat.on('chatUpdate', (data) => console.log('[chat] chatUpdate:', data)) - **Memory**: Clean up event listeners on unmount, call `meeting.leave()` when done, don't store large participant arrays ## In This Reference -- [README.md](README.md) - Overview, core concepts, quick start -- [configuration.md](configuration.md) - SDK config, presets, wrangler setup -- [api.md](api.md) - Client SDK APIs, REST endpoints -- [patterns.md](patterns.md) - Common patterns, React hooks, backend integration +- [README.md](./README.md) - Overview, core concepts, quick start +- [configuration.md](./configuration.md) - SDK config, presets, wrangler setup +- [api.md](./api.md) - Client SDK APIs, REST endpoints +- [patterns.md](./patterns.md) - Common patterns, React hooks, backend integration diff --git a/skill/cloudflare/references/realtimekit/patterns.md b/skill/cloudflare/references/realtimekit/patterns.md index 7202b2f..cf5f591 100644 --- a/skill/cloudflare/references/realtimekit/patterns.md +++ b/skill/cloudflare/references/realtimekit/patterns.md @@ -149,7 +149,7 @@ export default { 3. **Token management** - Backend generates tokens, frontend receives via authenticated endpoint ## In This Reference -- [README.md](README.md) - Overview, core concepts, quick start -- [configuration.md](configuration.md) - SDK config, presets, wrangler setup -- [api.md](api.md) - Client SDK APIs, REST endpoints -- [gotchas.md](gotchas.md) - Common issues, troubleshooting, limits +- [README.md](./README.md) - Overview, core concepts, quick start +- [configuration.md](./configuration.md) - SDK config, presets, wrangler setup +- [api.md](./api.md) - Client SDK APIs, REST endpoints +- [gotchas.md](./gotchas.md) - Common issues, troubleshooting, limits diff --git a/skill/cloudflare/references/snippets/README.md b/skill/cloudflare/references/snippets/README.md index 9fd93e8..228980b 100644 --- a/skill/cloudflare/references/snippets/README.md +++ b/skill/cloudflare/references/snippets/README.md @@ -5,10 +5,10 @@ Expert guidance for **Cloudflare Snippets ONLY** - a lightweight JavaScript-base ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/spectrum/README.md b/skill/cloudflare/references/spectrum/README.md index 678c524..9365194 100644 --- a/skill/cloudflare/references/spectrum/README.md +++ b/skill/cloudflare/references/spectrum/README.md @@ -6,10 +6,10 @@ Cloudflare Spectrum provides security and acceleration for ANY TCP or UDP-based ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/static-assets/README.md b/skill/cloudflare/references/static-assets/README.md index 75b035f..b20f032 100644 --- a/skill/cloudflare/references/static-assets/README.md +++ b/skill/cloudflare/references/static-assets/README.md @@ -4,10 +4,10 @@ Expert guidance for deploying and configuring static assets with Cloudflare Work ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/tail-workers/SKILL.md b/skill/cloudflare/references/tail-workers/README.md similarity index 100% rename from skill/cloudflare/references/tail-workers/SKILL.md rename to skill/cloudflare/references/tail-workers/README.md diff --git a/skill/cloudflare/references/turn/SKILL.md b/skill/cloudflare/references/turn/README.md similarity index 100% rename from skill/cloudflare/references/turn/SKILL.md rename to skill/cloudflare/references/turn/README.md diff --git a/skill/cloudflare/references/turnstile/README.md b/skill/cloudflare/references/turnstile/README.md index 8174cbc..c1a229a 100644 --- a/skill/cloudflare/references/turnstile/README.md +++ b/skill/cloudflare/references/turnstile/README.md @@ -4,10 +4,10 @@ Expert guidance for implementing Cloudflare Turnstile - a smart CAPTCHA alternat ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/turnstile/configuration.md b/skill/cloudflare/references/turnstile/configuration.md index 54819d0..728acf9 100644 --- a/skill/cloudflare/references/turnstile/configuration.md +++ b/skill/cloudflare/references/turnstile/configuration.md @@ -1,6 +1,7 @@ ## Configuration Options ### Widget Configurations + ```javascript { sitekey: 'required', // Your widget sitekey @@ -8,12 +9,23 @@ cData: 'optional-string', // Custom data passed back in validation callback: (token) => {}, // Success callback with token 'error-callback': (code) => {}, // Error callback + execution: 'render', // 'render' (default) or 'execute' 'expired-callback': () => {}, // Token expiry callback - 'timeout-callback': () => {}, // Challenge timeout callback 'before-interactive-callback': () => {}, // Before showing checkbox 'after-interactive-callback': () => {}, // After checkbox interaction + 'unsupported-callback': () => {}, // Client/browser not supported callback theme: 'auto', // 'light', 'dark', 'auto' - size: 'normal', // 'normal', 'flexible', 'compact' + language: 'auto', // 'auto' or ISO 639-1 code (e.g. 'en', 'en-US') tabindex: 0, // Tab index for accessibility + 'timeout-callback': () => {}, // Challenge timeout callback 'response-field': true, // Create hidden input (default: true) - 'response-field-name': 'cf-turnstile-response', / \ No newline at end of file + 'response-field-name': 'cf-turnstile-response', // Name of the input element + size: 'normal', // 'normal', 'flexible', 'compact' + retry: 'auto', // 'auto' (default) or 'never' + 'retry-interval': 8000, // Retry delay in ms (max 900000) + 'refresh-expired': 'auto', // 'auto', 'manual', 'never' + 'refresh-timeout': 'auto', // 'auto', 'manual', 'never' + appearance: 'always', // 'always', 'execute', 'interaction-only' + 'feedback-enabled': true // Allow Cloudflare to gather feedback on failure +} +``` diff --git a/skill/cloudflare/references/vectorize/SKILL.md b/skill/cloudflare/references/vectorize/README.md similarity index 100% rename from skill/cloudflare/references/vectorize/SKILL.md rename to skill/cloudflare/references/vectorize/README.md diff --git a/skill/cloudflare/references/waf/README.md b/skill/cloudflare/references/waf/README.md index 068b683..36076df 100644 --- a/skill/cloudflare/references/waf/README.md +++ b/skill/cloudflare/references/waf/README.md @@ -4,10 +4,10 @@ ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/web-analytics/README.md b/skill/cloudflare/references/web-analytics/README.md index bb66b52..93e8740 100644 --- a/skill/cloudflare/references/web-analytics/README.md +++ b/skill/cloudflare/references/web-analytics/README.md @@ -9,10 +9,10 @@ Cloudflare Web Analytics is a free, privacy-focused analytics service that: ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/workers-ai/SKILL.md b/skill/cloudflare/references/workers-ai/README.md similarity index 98% rename from skill/cloudflare/references/workers-ai/SKILL.md rename to skill/cloudflare/references/workers-ai/README.md index a308129..2a299fd 100644 --- a/skill/cloudflare/references/workers-ai/SKILL.md +++ b/skill/cloudflare/references/workers-ai/README.md @@ -23,7 +23,7 @@ This skill focuses **exclusively on Cloudflare Workers AI** - the serverless AI ### Installation -1. Place `SKILL.md` in your OpenCode skills directory +1. Place `README.md` in your OpenCode skills directory 2. Load the skill when working on Workers AI projects ### When to Load This Skill diff --git a/skill/cloudflare/references/workers-playground/README.md b/skill/cloudflare/references/workers-playground/README.md index cac6eed..345104d 100644 --- a/skill/cloudflare/references/workers-playground/README.md +++ b/skill/cloudflare/references/workers-playground/README.md @@ -6,10 +6,10 @@ Cloudflare Workers Playground is a browser-based sandbox for instantly experimen ## In This Reference -- **[configuration.md](configuration.md)** - Setup, deployment, configuration -- **[api.md](api.md)** - API endpoints, methods, interfaces -- **[patterns.md](patterns.md)** - Common patterns, use cases, examples -- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations +- **[configuration.md](./configuration.md)** - Setup, deployment, configuration +- **[api.md](./api.md)** - API endpoints, methods, interfaces +- **[patterns.md](./patterns.md)** - Common patterns, use cases, examples +- **[gotchas.md](./gotchas.md)** - Troubleshooting, best practices, limitations ## See Also diff --git a/skill/cloudflare/references/workers-vpc/SKILL.md b/skill/cloudflare/references/workers-vpc/README.md similarity index 100% rename from skill/cloudflare/references/workers-vpc/SKILL.md rename to skill/cloudflare/references/workers-vpc/README.md diff --git a/skill/cloudflare/references/zaraz/SKILL.md b/skill/cloudflare/references/zaraz/README.md similarity index 100% rename from skill/cloudflare/references/zaraz/SKILL.md rename to skill/cloudflare/references/zaraz/README.md diff --git a/tmp/durable-objects-gotchas.md b/tmp/durable-objects-gotchas.md new file mode 100644 index 0000000..03eaa80 --- /dev/null +++ b/tmp/durable-objects-gotchas.md @@ -0,0 +1,218 @@ +# Durable Objects Gotchas + +> Source: https://coey.dev/durable-objects-gotchas + +The quiz you wish you had before your first surprise bill. + +**Problem:** Durable Objects billing is deceptively nuanced. Most people discover the gotchas after getting an unexpected bill. + +**Solution:** Self-diagnosis quiz. Learn before you deploy. + +--- + +## Gotcha #1: Duration Billing Trap + +**Scenario:** You have a DO that handles a WebSocket connection for a real-time dashboard. User connects at 9am, disconnects at 5pm. During that time, the DO processes maybe 50 small messages total. + +**Question:** What's your biggest cost driver here? + +- A) The 50 request invocations +- B) 8 hours of wall-clock duration +- C) CPU time processing the messages +- D) Storage for connection state + +**Answer:** B - Wall-clock duration. + +DOs bill for wall-clock time while active, not just CPU time. 8 hours of keeping a WebSocket open = 8 hours of duration billing, even if the DO did almost nothing. + +**The fix:** Use Hibernatable WebSockets. The DO can sleep while maintaining the connection, only waking (and billing) when messages arrive. + +--- + +## Gotcha #2: storage.list() on Every Request + +**Scenario:** Your DO stores user preferences (20 keys). On each request, you need 3 of those keys. + +**Question:** What's the cheapest read pattern? + +- A) `storage.get(['key1', 'key2', 'key3'])` +- B) `storage.list()` once on wake, cache in memory +- C) Single `storage.get('preferences')` with all 20 keys as one object +- D) It depends—on what? + +**Answer:** D - It depends on request patterns and data volatility. + +- **Option A** is cheapest per-request if you only need those 3 keys and data changes frequently +- **Option B** is cheapest if you serve many requests per wake cycle (amortizes the list cost) +- **Option C** is cheapest if you often need multiple keys and can tolerate reading/writing all 20 together + +The real answer: profile your actual usage. Storage reads are cheap, but not free. + +--- + +## Gotcha #3: Alarm Recursion + +**Scenario:** You're using `storage.setAlarm()` to wake a DO every 5 minutes to check for stale data. The check takes 2ms of CPU time. + +**Question:** Over 24 hours, what's happening to your costs that you might not expect? + +**Answer:** 288 wake-ups × (minimum billable duration) per day. + +Even if the check takes 2ms, you're billed for the minimum duration each time. Multiply across thousands of DOs, and you're waking them all up 288 times/day whether they have work or not. + +**The fix:** Only schedule alarms when there's actual work pending. Check if the alarm is needed before setting it. If 90% of your DOs have no stale data, don't wake them. + +--- + +## Gotcha #4: WebSocket Never Closes + +**Scenario:** Your DO handles WebSocket connections. Users sometimes close their browser tabs without properly disconnecting. + +**Question:** What happens if you never call `webSocket.close()` on disconnect? + +**Answer:** The connection stays "open" from the DO's perspective, preventing hibernation and keeping duration billing active. + +WebSocket connections don't automatically clean up on the DO side. If you don't handle the close event and explicitly close the connection, the DO may stay awake indefinitely. + +**The fix:** +1. Handle `webSocketClose` and `webSocketError` events +2. Implement heartbeat/ping-pong to detect dead connections +3. Use Hibernatable WebSockets API properly + +--- + +## Gotcha #5: Singleton vs Sharding + +**Scenario:** You're building a rate limiter. Design A uses one global DO. Design B creates one DO per user. Design C creates one DO per user-per-hour (ephemeral). + +**Question:** For 10,000 users making 100 requests/day each, rank these by cost (cheapest to most expensive). + +**Answer:** Typically: B < A < C (but it depends on patterns). + +- **Design A (global singleton):** One DO, but it never hibernates due to constant traffic. Continuous duration billing. Also a performance bottleneck. +- **Design B (per-user):** 10,000 DOs, but each only wakes for their user's 100 requests. Most hibernate most of the time. +- **Design C (per-user-per-hour):** Creates 240,000 DO instances per day. Each instance is cheap, but you're paying for a lot of cold starts and minimum durations. + +**The nuance:** If users are bursty (all requests in 1 hour, then nothing), Design B might wake/sleep a lot. Design C might actually be cheaper in that case. Profile your actual traffic patterns. + +--- + +## Gotcha #6: storage.get() vs Batching + +**Scenario:** You need to read 5 keys from storage on each request. + +**Question:** Is it cheaper to call `storage.get(['key1', 'key2', 'key3', 'key4', 'key5'])` or make 5 separate `storage.get()` calls? + +**Answer:** Batched `storage.get(['key1', ...])` is cheaper. + +Each storage operation has overhead. Batching reads into a single call reduces the number of billable operations. Same applies to writes—batch them when possible. + +**Bonus:** Writes without intervening `await` calls are automatically coalesced into a single atomic transaction. + +--- + +## Gotcha #7: Hibernation Confusion + +**Scenario:** Your DO needs to respond to requests but also run background work every 10 minutes. + +**Question:** How do you structure this so the DO can hibernate between alarm intervals, but still handle incoming requests without "waking up" a new instance that doesn't have warm state? + +**Answer:** You can't rely on in-memory state across hibernation. + +When a DO hibernates, in-memory state is lost. When it wakes (via request or alarm), it reconstructs state from storage. + +**The fix:** +1. Store all important state in `storage` (SQLite or KV) +2. Use `blockConcurrencyWhile()` in constructor to load state on wake +3. Cache in memory for the current wake cycle only +4. Accept that every wake is potentially a "cold" wake from state perspective + +--- + +## Gotcha #8: Fan-Out Tax + +**Scenario:** You have an event that needs to notify 1,000 DOs. Each notification is tiny (100 bytes). + +**Question:** What's the cost difference between a Worker making 1,000 `stub.fetch()` calls in parallel vs using a queue/alarm pattern? + +**Answer:** Direct fan-out: 1,000 DO invocations billed immediately. Queue pattern: 1,000 queue messages + 1,000 DO invocations (eventually). + +The queue pattern isn't cheaper in DO invocations, but it: +1. Avoids timing out the original Worker request +2. Provides retry/dead-letter handling +3. Can batch notifications if DOs are already awake + +**The real cost:** 1,000 cold-start invocations can have latency spikes. If this is time-sensitive, direct fan-out is faster but more expensive in aggregate. + +--- + +## Gotcha #9: Idempotency Key Explosion + +**Scenario:** You're using DOs for idempotency. Each request gets a unique idempotency key. You create one DO per key. + +**Question:** What happens if you create a new DO instance for each idempotency key that's used once? + +**Answer:** You create millions of tiny DOs that each handle one request and never get cleaned up. + +Each DO instance persists until explicitly deleted. Storage isn't free. You end up with a growing pile of single-use DOs. + +**The fix:** +1. Use a sharded approach: hash idempotency keys into N buckets +2. Store idempotency records as rows in a single DO's SQLite table +3. Implement TTL/cleanup via alarms to delete old records +4. Consider if you actually need DO-level consistency for idempotency (maybe KV is enough?) + +--- + +## Gotcha #10: Storage Compaction + +**Scenario:** You're writing events to DO storage. Each event is 100 bytes. You write 1 event per request. + +**Question:** What's the cost difference between writing 1 item per event vs batching 100 events per write? + +**Answer:** Individual writes: 100x the write operations. Batched writes: 1x the write operations, but you need buffering logic. + +Storage writes are billed per operation. Batching reduces operations but adds complexity (buffer in memory, flush on threshold or timer). + +**The nuance:** If you're using SQLite storage, transactions are your friend. Multiple `INSERT` statements without intervening `await` are coalesced into a single transaction. + +--- + +## Gotcha #11: waitUntil() in DOs + +**Scenario:** You want to do background work after returning a response. You use `waitUntil()` like in Workers. + +**Question:** Does `waitUntil()` work in DOs the same way it works in Workers? + +**Answer:** No. `ctx.waitUntil()` in DOs keeps the DO alive but doesn't extend the request's lifetime. Use it for fire-and-forget work, but be aware: + +1. The DO stays active (billed) until `waitUntil` promises resolve +2. If you're waiting for slow external calls, you're paying for that wait time +3. Errors in `waitUntil` code don't propagate to the original request + +**The fix:** For true background work, consider using alarms or queues instead of `waitUntil`. + +--- + +## Gotcha #12: KV vs DO Storage + +**Scenario:** You need to store user session data. It's read-heavy (20 reads per write), write-rare. + +**Question:** Should you use KV or DO storage? + +**Answer:** For read-heavy, write-rare, eventually-consistent-OK data: **KV is cheaper**. + +KV: +- Global edge caching +- Cheap reads from cache +- Writes propagate eventually (~60 seconds) +- No compute cost for reads + +DO Storage: +- Strong consistency +- Every read hits the DO (compute cost) +- Co-located with compute +- Better for read-modify-write patterns + +**The rule:** If you don't need strong consistency and writes are rare, KV is almost always cheaper for read-heavy workloads. + diff --git a/tmp/fleet-pattern.md b/tmp/fleet-pattern.md new file mode 100644 index 0000000..49d2b07 --- /dev/null +++ b/tmp/fleet-pattern.md @@ -0,0 +1,477 @@ +# Fleet Pattern: Hierarchical Durable Objects + +> Source: https://coey.dev/fleet-pattern + +Infinite nesting of manager/agent relationships through URL paths. + +## The Hierarchical Coordination Challenge + +**Problem:** Building hierarchical systems with manager/agent relationships requires complex coordination, state management, and real-time communication. + +**Solution:** Fleet Pattern uses URL-based routing to create infinite nesting of Durable Objects, each capable of managing child agents with real-time WebSocket communication. + +``` +Client → ManagerDO → AgentDO (N) + ↑ ↓ + heartbeats results + ↓ ↓ + WebSocket Cascading + Updates Operations +``` + +## Architecture Overview + +### Core Features + +- **URL-based hierarchy** - Each path creates a unique DO +- **Infinite nesting** - Unlimited depth of manager/agent relationships +- **Real-time communication** - WebSocket-based state synchronization +- **Cascading operations** - Delete operations propagate down the tree +- **Message routing** - Direct and broadcast messaging between agents + +### Tech Stack + +- **Hono** - Edge-first web framework +- **Durable Objects** - Persistent state and WebSocket handling +- **TypeScript** - End-to-end type safety +- **WebSocket API** - Real-time bidirectional communication +- **Cloudflare Workers** - Global edge deployment + +## Key Innovations + +- **URL-based hierarchy** - `/team1/project1/task1` creates nested DO structure +- **Unified DO class** - Single class handles both manager and agent roles +- **Dynamic DO creation** - Agents created on-demand based on URL paths +- **Real-time state sync** - WebSocket connections maintain live state updates +- **Cascading deletion** - Removing a manager deletes all child agents recursively +- **Message routing** - Direct messages to specific agents or broadcast to all children + +## Quick Start + +```bash +# Clone and setup +git clone https://github.com/acoyfellow/fleet-pattern +cd fleet-pattern +bun install + +# Start development server +bun run dev + +# Test hierarchy +# http://localhost:8787/ (root manager) +# http://localhost:8787/team1 (team manager) +# http://localhost:8787/team1/project1 (project manager) + +# Deploy to Cloudflare +bun run deploy +``` + +Creates a complete hierarchical system with real-time communication and cascading operations. + +## Main Worker Routing + +### URL-based Durable Object Creation + +```ts +// src/index.ts - Main Worker with URL-based routing +import { Hono } from 'hono' +import { DurableObjectNamespace, DurableObjectState } from '@cloudflare/workers-types' +import type { Request as CFRequest } from '@cloudflare/workers-types' + +interface Env { + FLEET_DO: DurableObjectNamespace, +} + +// The Worker: routes all requests through Hono +const app = new Hono<{ Bindings: Env }>() + +// Route everything else to DOs based on URL path +app.all('*', async (c) => { + const path = new URL(c.req.url).pathname + const parts = path.split('/').filter(Boolean) + const doName = parts.length === 0 ? '/' : `/${parts.join('/')}` + + const id = c.env.FLEET_DO.idFromName(doName) + const stub = c.env.FLEET_DO.get(id) + return stub.fetch(c.req.raw as CFRequest) +}) + +export default { + fetch: app.fetch +} +``` + +## Durable Object Implementation + +### Unified Manager/Agent Class + +```ts +// FleetDO class - Unified Manager/Agent implementation +export class FleetDO { + private app = new Hono() + private connections = new Set() + + constructor(private durableState: DurableObjectState, private env: Env) { + this.app.get('*', c => { + const upgradeHeader = c.req.header('Upgrade') + if (upgradeHeader?.toLowerCase() === 'websocket') { + return this.handleWebSocket(c) + } + return this.handleView(c) + }) + + // Handle cascading deletion + this.app.delete('*', async () => { + const data = await this.getState() + + if (data?.agents) { + const path = new URL(this.durableState.id.toString()).pathname + for (const agent of data.agents) { + const childPath = path === '/' ? `/${agent}` : `${path}/${agent}` + const childId = this.env.FLEET_DO.idFromName(childPath) + const childStub = this.env.FLEET_DO.get(childId) + await childStub.fetch(new Request(childPath, { method: 'DELETE' })) + } + } + + for (const ws of this.connections) { + ws.close(1000, 'Agent deleted') + } + + await this.durableState.storage.deleteAll() + return new Response('OK') + }) + } + + private async getState(): Promise { + return await this.durableState.storage.get('data') || { + data: { count: 0 }, + agents: [] + } + } + + private async setState(data: AgentState): Promise { + await this.durableState.storage.put('data', data) + } +} +``` + +## WebSocket Message Handling + +### Real-time Communication Protocol + +```ts +// WebSocket message handling for real-time communication +server.addEventListener('message', async event => { + try { + const msg = JSON.parse(event.data as string) as WSMessage + const data = await this.getState() + const path = new URL(c.req.url).pathname + const senderName = path.split('/').filter(Boolean).pop() || 'root' + + switch (msg.type) { + case 'increment': + data.data.count++ + await this.setState(data) + break + + case 'createAgent': + if (!msg.payload?.name) throw new Error('Agent name required') + if (!this.validateAgentName(msg.payload.name)) { + throw new Error('Invalid agent name') + } + if (data.agents.includes(msg.payload.name)) { + throw new Error('Agent already exists') + } + data.agents.push(msg.payload.name) + await this.setState(data) + break + + case 'deleteAgent': + if (!msg.payload?.name) throw new Error('Agent name required') + const index = data.agents.indexOf(msg.payload.name) + if (index === -1) throw new Error('Agent not found') + + // Cascading deletion + const childPath = path === '/' ? `/${msg.payload.name}` : `${path}/${msg.payload.name}` + const childId = this.env.FLEET_DO.idFromName(childPath) + const childStub = this.env.FLEET_DO.get(childId) + await childStub.fetch(new Request(`https://internal${childPath}`, { method: 'DELETE' })) + + data.agents.splice(index, 1) + await this.setState(data) + break + + case 'sendMessage': + // Direct message to specific agent + const recipientPath = path === '/' ? `/${msg.payload.recipient}` : `${path}/${msg.payload.recipient}` + const recipientId = this.env.FLEET_DO.idFromName(recipientPath) + const recipientStub = this.env.FLEET_DO.get(recipientId) + + await recipientStub.fetch(new Request(`https://internal${recipientPath}/_message`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'message', + payload: { + message: msg.payload.message, + sender: senderName + } + }) + })) + break + + case 'broadcast': + // Broadcast to all child agents + for (const agent of data.agents) { + const childPath = path === '/' ? `/${agent}` : `${path}/${agent}` + const childId = this.env.FLEET_DO.idFromName(childPath) + const childStub = this.env.FLEET_DO.get(childId) + + await childStub.fetch(new Request(`https://internal${childPath}/_message`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'message', + payload: { + message: msg.payload.message, + sender: senderName + } + }) + })) + } + break + } + + this.broadcast({ type: 'state', payload: data }) + + } catch (err) { + server.send(JSON.stringify({ + type: 'error', + payload: { error: err.message } + })) + } +}) +``` + +## Message Protocol + +### WebSocket Communication Types + +```ts +// WebSocket message protocol +interface WSMessage { + type: 'increment' | 'createAgent' | 'deleteAgent' | 'sendMessage' | 'broadcast'; + payload?: { + name?: string; + message?: string; + recipient?: string; + }; +} + +interface WSResponse { + type: 'state' | 'error' | 'message' | 'broadcast'; + payload: { + data?: { count: number }; + agents?: string[]; + error?: string; + message?: string; + sender?: string; + }; +} + +// Message flow examples +const messages = { + // Local state change + increment: { type: 'increment' }, + + // Hierarchy management + createAgent: { type: 'createAgent', payload: { name: 'newAgent' } }, + deleteAgent: { type: 'deleteAgent', payload: { name: 'oldAgent' } }, + + // Communication + sendMessage: { type: 'sendMessage', payload: { recipient: 'agent1', message: 'Hello!' } }, + broadcast: { type: 'broadcast', payload: { message: 'System update' } } +}; +``` + +## Client-Side Integration + +### Real-time UI Updates + +```js +// Real-time UI updates with WebSocket +function updateUI(state) { + // Update counter + document.getElementById('count').textContent = state.data.count; + + // Update agents list + const agentsList = document.getElementById('agents'); + agentsList.innerHTML = ''; + + if (state.agents.length === 0) { + agentsList.innerHTML = '
  • No agents
  • '; + return; + } + + state.agents.forEach(name => { + const li = document.createElement('li'); + li.innerHTML = ` +
    + ${name} +
    + + + +
    +
    + `; + agentsList.appendChild(li); + }); +} + +function sendMessageTo(recipient) { + const row = document.querySelector(`[onclick="sendMessageTo('${recipient}')"]`).closest('.agent-row'); + const message = row.querySelector('.message-input').value.trim(); + + if (message) { + sendMessage({ + type: 'sendMessage', + payload: { recipient, message } + }); + row.querySelector('.message-input').value = ''; + } +} +``` + +## Configuration + +### Wrangler Configuration + +```toml +# wrangler.toml - Cloudflare Workers configuration +name = "fleet" +main = "src/index.ts" +compatibility_date = "2024-01-01" + +assets = { directory = "public" } + +[build.upload] +format = "modules" + +[durable_objects] +bindings = [ + { name = "FLEET_DO", class_name = "FleetDO" } +] + +[[migrations]] +tag = "v1" +new_classes = ["FleetDO"] + +[observability.logs] +enabled = true +``` + +## Architecture Patterns + +### URL-Based Hierarchy + +- Each path segment creates a unique DO +- Infinite nesting depth supported +- Clean separation of concerns +- Natural resource organization + +### Real-Time Communication + +- WebSocket connections per DO +- State synchronization across clients +- Direct and broadcast messaging +- Automatic reconnection handling + +### Cascading Operations + +- Delete operations propagate down tree +- Automatic cleanup of child resources +- Safe resource management +- Consistent state maintenance + +### Unified Implementation + +- Single class for all roles +- Role determined by context +- Simplified codebase +- Consistent behavior patterns + +## Production Use Cases + +| Use Case | Description | +|----------|-------------| +| **Real-Time Collaborative IDE** | Each file is a DO with operational transform engine. Real-time cursors, editing, and file-specific permissions. | +| **Distributed Task Runner** | Each stage manages its own tasks with status communication up the chain. Automatic retry and failure management. | +| **IoT Device Management** | Each level manages device fleet with real-time sensor data aggregation and hierarchical monitoring. | +| **Game Server Infrastructure** | Each instance is a game server with real-time player state management and instance-to-instance communication. | +| **Content Management System** | Each page manages its own content and cache with real-time preview and hierarchical permissions. | +| **Distributed Chat System** | Each thread manages its own messages with real-time presence indicators and cross-thread notifications. | + +## Scaling Patterns + +### Multi-tenant and Geographic Distribution + +```ts +// Scaling patterns for different use cases + +// 1. Geographic Distribution +const region = getClosestRegion(clientIP); +const obj = env.FLEET_DO.getByName(`${region}-team1-project1`); + +// 2. Tenant Isolation +const obj = env.FLEET_DO.getByName(`tenant-${tenantId}-team1`); + +// 3. Feature-based Sharding +const obj = env.FLEET_DO.getByName(`feature-${featureId}-instance-${instanceId}`); + +// 4. Time-based Partitioning +const date = new Date().toISOString().split('T')[0]; +const obj = env.FLEET_DO.getByName(`${date}-analytics`); + +// 5. Load-based Distribution +const shardId = hash(userId) % numShards; +const obj = env.FLEET_DO.getByName(`shard-${shardId}-user-${userId}`); +``` + +## Performance Characteristics + +| Metric | Value | +|--------|-------| +| **DO Creation** | On-demand based on URL access patterns | +| **State Management** | 128MB storage per Durable Object instance | +| **WebSocket Connections** | 1,000 concurrent per DO instance | +| **Message Latency** | Sub-100ms for direct agent communication | +| **Hierarchy Depth** | Unlimited nesting with URL path limits | +| **Global Distribution** | 200+ edge locations worldwide | + +## Development Workflow + +1. **Clone Repository:** `git clone https://github.com/acoyfellow/fleet-pattern` +2. **Install Dependencies:** `bun install` sets up everything +3. **Local Development:** `bun run dev` starts with hot reloading +4. **Test Hierarchy:** Navigate to different URL paths to test nesting +5. **Deploy:** `bun run deploy` deploys to Cloudflare Workers + +## Operational Considerations + +- **Durable Object limits:** 1,000 concurrent instances per account +- **Storage limits:** 128MB per Durable Object instance +- **WebSocket limits:** 1,000 concurrent connections per DO +- **URL path limits:** 8,192 characters maximum path length +- **Cold starts:** ~10-50ms for new Durable Object instances +- **Message validation:** Input sanitization and error handling + +## Security Features + +- **Input validation:** Agent names restricted to alphanumeric, dash, underscore (1-32 chars) +- **WebSocket security:** Proper connection lifecycle management +- **Cascading deletion safety:** Hierarchical cleanup with error handling +- **Message sanitization:** JSON parsing with error boundaries +- **Resource isolation:** Each DO instance is completely isolated +- **Rate limiting:** Built-in protection against abuse diff --git a/tmp/perfect-do.md b/tmp/perfect-do.md new file mode 100644 index 0000000..271a952 --- /dev/null +++ b/tmp/perfect-do.md @@ -0,0 +1,152 @@ +# The Perfect Durable Object? + +> Source: https://coey.dev/perfect-do + +Trying to figure out what "good" looks like. + +Keep building things with DOs and something feels off. Not broken.. just wrong. What would a well-designed DO actually look like? + +## Metaphors That Help Me + +### The Night Watchman + +A DO is like a night watchman who: + +- Owns **ONE building** (not the whole city) +- Sleeps until something happens +- Wakes instantly, handles it with full authority +- Goes back to sleep +- Can coordinate with other watchmen via RPC when needed + +With Workers RPC, DO-to-DO communication chains like 1->2->3 are treated as a single end-to-end call. However, this is still a network hop and can be slow, especially between colos. The question isn't just performance, but whether the coordination complexity is necessary. + +### The Safety Deposit Box + +Your data, your box, your rules. + +- Bank handles infrastructure +- But the box is **YOURS** +- Opens when needed, closes when done +- Not a vault for the whole bank's operations + +### Database Row That Can Think + +Database row that woke up, realized it could run code, handles its own business logic. Single-threaded. Authoritative over its slice of state. + +Not a database. Not a general-purpose compute node. + +## Rough Numbers + +Updated based on [official Cloudflare docs](https://developers.cloudflare.com/durable-objects/platform/limits/) and employee feedback. These are design heuristics, not hard limits. DOs routinely scale into "red" territory with proper sharding patterns. + +| Characteristic | Feels Right | Yellow Flag | Red Flag | +|----------------|-------------|-------------|----------| +| Requests/sec (sustained) | < 100 | 100-500 | > 500 | +| Storage keys | < 100 | 100-1000 | > 1000 (consider access patterns) | +| Total state size | < 10MB | 10MB-100MB | > 1GB | +| Alarm frequency | Minutes-hours (only when work exists) | Every 30s | Every few seconds (waking idle DOs) | +| WebSocket duration | Short bursts | Hours (hibernating) | Days always-on | +| Fan-out from this DO | Never/rarely | To < 10 DOs | To 100+ DOs | + +Aim for green. Yellow = question it. Red = consider sharding patterns. + +## What a "DO" Seems To Be + +- **Single-threaded authority** over a piece of state. No locks, no races, no coordination. +- **Stateful actor that hibernates.** Sleep is default, not optimization. +- **Colocated compute + storage.** Code runs where data lives. +- **Natural ownership boundary.** One user. One room. One session. One document. The "atom" of coordination. +- **Parent-child relationships.** Hierarchical data uses separate child DOs for parallelism while maintaining single-threaded consistency per child. + +## What a "DO" Probably Isn't + +- **A global singleton.** One DO handling everything = bottleneck with extra steps. +- **A message broker.** High-frequency pub/sub? Use Queues. +- **A long-running process.** Needs to run continuously? Use Workers + alarms, not always-on DO. +- **A chatty microservice.** Calling it on every request? Reconsider. +- **A general database.** Consistency doesn't matter or ownership unclear? Use KV or D1. +- **A place for blockConcurrencyWhile() on every request.** Use sparingly.. only for initialization/migrations. Regular requests should rely on input/output gates. + +## Questions Before Creating One + +### 1. Does this data have natural ownership? + +User/room/session-owned? If shared across boundaries, DO might be wrong. + +### 2. Do I actually need strong consistency here? + +DOs give strong consistency. If eventual is fine, KV is cheaper. + +### 3. Will instances communicate infrequently? + +RPC chains are network hops (can be slow between colos). Constant coordination suggests wrong boundaries. Use parent-child relationships for hierarchical data instead of constant cross-DO calls. + +### 4. Can this DO hibernate between meaningful work? + +If it needs to stay awake, you're paying wall-clock time. Adds up. + +### 5. Am I going to create thousands of these? + +Thousands = fine. Millions = think about sharding. Billions = wrong tool. + +## The "Perfect" DO + +```ts +// - Owns ONE thing (user, room, document, session) - the "atom" of coordination +// - Uses deterministic IDs (getByName) for predictable routing +// - Uses SQLite storage as source of truth (not KV) +// - Uses RPC methods, not fetch() handler +// - Wakes up infrequently +// - Does meaningful work when awake +// - Stores < 100 keys, < 10MB total (heuristics) +// - Hibernates ASAP +// - Can coordinate via RPC when needed (zero latency chains) +// - Could disappear and reconstruct from storage +// - Is the authority, not a cache +// - Uses parent-child relationships for hierarchical data +``` + +## Worth Deeper Exploration + +These areas have solutions and patterns, but warrant deeper thought and exploration based on your specific use case. + +### WebSockets + +Use Hibernatable WebSockets API.. DO owns the connection. Allows DO to sleep while maintaining connections, significantly reducing costs for idle connections. + +### Sharding + +One DO per user vs hash into N buckets? For high-throughput systems, use `request.cf.colo` for geographic awareness. Rate limit binding is per-colo and enables dynamic sharding patterns. + +`request.cf.colo` gives you the IATA airport code of the datacenter (e.g., "SFO", "LHR"). Rate limiting bindings are per-colo (counters not shared across datacenters), so combining them gives automatic geographic distribution. + +```ts +// Worker: Colo-aware sharding with rate limiting +export default { + async fetch(request: Request, env: Env): Promise { + const userId = new URL(request.url).searchParams.get("userId") || "unknown"; + const colo = request.cf?.colo || "unknown"; + const shardKey = `${colo}:${userId}`; + + // Rate limit per-colo before hitting DO + const { success } = await env.RATE_LIMITER.limit({ key: shardKey }); + if (!success) return new Response("Rate limited", { status: 429 }); + + // Route to colo-aware DO shard + const stub = env.MY_DO.getByName(shardKey); + return await stub.fetch(request); + } +} +``` + +### DO-to-DO Communication + +RPC chains (1->2->3) are treated as a single end-to-end call, but this is still a network hop and can be slow, especially between colos. + +**Use when:** Parent-child hierarchies (fleet pattern), infrequent coordination, clear ownership boundaries. + +**Avoid when:** Every request needs multiple DOs, constant cross-DO calls, or when a database would be simpler. + +### Storage as Cache vs Source of Truth + +Use SQLite-backed storage as source of truth. In-memory state is NOT preserved on eviction or crash.. always persist critical state. Use SQLite for relational queries, indexes, transactions. diff --git a/tmp/rules-of-durable-objects.md b/tmp/rules-of-durable-objects.md new file mode 100644 index 0000000..f8e4c80 --- /dev/null +++ b/tmp/rules-of-durable-objects.md @@ -0,0 +1,694 @@ +# Rules of Durable Objects + +> Source: https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/ + +Durable Objects provide a powerful primitive for building stateful, coordinated applications. Each Durable Object is a single-threaded, globally-unique instance with its own persistent storage. Understanding how to design around these properties is essential for building effective applications. + +This is a guidebook on how to build more effective and correct Durable Object applications. + +## When to use Durable Objects + +### Use Durable Objects for stateful coordination, not stateless request handling + +Workers are stateless functions: each request may run on a different instance, in a different location, with no shared memory between requests. Durable Objects are stateful compute: each instance has a unique identity, runs in a single location, and maintains state across requests. + +Use Durable Objects when you need: + +* **Coordination** — Multiple clients need to interact with shared state (chat rooms, multiplayer games, collaborative documents) +* **Strong consistency** — Operations must be serialized to avoid race conditions (inventory management, booking systems, turn-based games) +* **Per-entity storage** — Each user, tenant, or resource needs its own isolated database (multi-tenant SaaS, per-user data) +* **Persistent connections** — Long-lived WebSocket connections that survive across requests (real-time notifications, live updates) +* **Scheduled work per entity** — Each entity needs its own timer or scheduled task (subscription renewals, game timeouts) + +Use plain Workers when you need: + +* **Stateless request handling** — API endpoints, proxies, or transformations with no shared state +* **Maximum global distribution** — Requests should be handled at the nearest edge location +* **High fan-out** — Each request is independent and can be processed in parallel + +#### JavaScript + +```js +import { DurableObject } from "cloudflare:workers"; + +// Good use of Durable Objects: Seat booking requires coordination +// All booking requests for a venue must be serialized to prevent double-booking +export class SeatBooking extends DurableObject { + async bookSeat(seatId, userId) { + // Check if seat is already booked + const existing = this.ctx.storage.sql + .exec("SELECT user_id FROM bookings WHERE seat_id = ?", seatId) + .toArray(); + + if (existing.length > 0) { + return { success: false, message: "Seat already booked" }; + } + + // Book the seat - this is safe because Durable Objects are single-threaded + this.ctx.storage.sql.exec( + "INSERT INTO bookings (seat_id, user_id, booked_at) VALUES (?, ?, ?)", + seatId, + userId, + Date.now(), + ); + + return { success: true, message: "Seat booked successfully" }; + } +} + +export default { + async fetch(request, env) { + const url = new URL(request.url); + const eventId = url.searchParams.get("event") ?? "default"; + + // Route to a Durable Object by event ID + // All bookings for the same event go to the same instance + const id = env.BOOKING.idFromName(eventId); + const booking = env.BOOKING.get(id); + + const { seatId, userId } = await request.json(); + const result = await booking.bookSeat(seatId, userId); + + return Response.json(result, { + status: result.success ? 200 : 409, + }); + }, +}; +``` + +#### TypeScript + +```ts +import { DurableObject } from "cloudflare:workers"; + +export interface Env { + BOOKING: DurableObjectNamespace; +} + +// Good use of Durable Objects: Seat booking requires coordination +// All booking requests for a venue must be serialized to prevent double-booking +export class SeatBooking extends DurableObject { + async bookSeat( + seatId: string, + userId: string + ): Promise<{ success: boolean; message: string }> { + // Check if seat is already booked + const existing = this.ctx.storage.sql + .exec<{ user_id: string }>( + "SELECT user_id FROM bookings WHERE seat_id = ?", + seatId + ) + .toArray(); + + if (existing.length > 0) { + return { success: false, message: "Seat already booked" }; + } + + // Book the seat - this is safe because Durable Objects are single-threaded + this.ctx.storage.sql.exec( + "INSERT INTO bookings (seat_id, user_id, booked_at) VALUES (?, ?, ?)", + seatId, + userId, + Date.now() + ); + + return { success: true, message: "Seat booked successfully" }; + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const eventId = url.searchParams.get("event") ?? "default"; + + // Route to a Durable Object by event ID + // All bookings for the same event go to the same instance + const id = env.BOOKING.idFromName(eventId); + const booking = env.BOOKING.get(id); + + const { seatId, userId } = await request.json<{ + seatId: string; + userId: string; + }>(); + const result = await booking.bookSeat(seatId, userId); + + return Response.json(result, { + status: result.success ? 200 : 409, + }); + }, +}; +``` + +A common pattern is to use Workers as the stateless entry point that routes requests to Durable Objects when coordination is needed. The Worker handles authentication, validation, and response formatting, while the Durable Object handles the stateful logic. + +## Design and sharding + +### Model your Durable Objects around your "atom" of coordination + +The most important design decision is choosing what each Durable Object represents. Create one Durable Object per logical unit that needs coordination: a chat room, a game session, a document, a user's data, or a tenant's workspace. + +This is the key insight that makes Durable Objects powerful. Instead of a shared database with locks, each "atom" of your application gets its own single-threaded execution environment with private storage. + +#### JavaScript + +```js +import { DurableObject } from "cloudflare:workers"; + +// Each chat room is its own Durable Object instance +export class ChatRoom extends DurableObject { + async sendMessage(userId, message) { + // All messages to this room are processed sequentially by this single instance. + // No race conditions, no distributed locks needed. + this.ctx.storage.sql.exec( + "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)", + userId, + message, + Date.now(), + ); + } +} + +export default { + async fetch(request, env) { + const url = new URL(request.url); + const roomId = url.searchParams.get("room") ?? "lobby"; + + // Each room ID maps to exactly one Durable Object instance globally + const id = env.CHAT_ROOM.idFromName(roomId); + const stub = env.CHAT_ROOM.get(id); + + await stub.sendMessage("user-123", "Hello, room!"); + return new Response("Message sent"); + }, +}; +``` + +#### TypeScript + +```ts +import { DurableObject } from "cloudflare:workers"; + +export interface Env { + CHAT_ROOM: DurableObjectNamespace; +} + +// Each chat room is its own Durable Object instance +export class ChatRoom extends DurableObject { + async sendMessage(userId: string, message: string) { + // All messages to this room are processed sequentially by this single instance. + // No race conditions, no distributed locks needed. + this.ctx.storage.sql.exec( + "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)", + userId, + message, + Date.now() + ); + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const roomId = url.searchParams.get("room") ?? "lobby"; + + // Each room ID maps to exactly one Durable Object instance globally + const id = env.CHAT_ROOM.idFromName(roomId); + const stub = env.CHAT_ROOM.get(id); + + await stub.sendMessage("user-123", "Hello, room!"); + return new Response("Message sent"); + }, +}; +``` + +> **Note:** If you have global application or user configuration that you need to access frequently (on every request), consider using [Workers KV](https://developers.cloudflare.com/kv/) instead. + +Do not create a single "global" Durable Object that handles all requests: + +```js +// Bad: A single Durable Object handling ALL chat rooms +export class ChatRoom extends DurableObject { + async sendMessage(roomId, userId, message) { + // All messages for ALL rooms go through this single instance. + // This becomes a bottleneck as traffic grows. + this.ctx.storage.sql.exec( + "INSERT INTO messages (room_id, user_id, content) VALUES (?, ?, ?)", + roomId, + userId, + message, + ); + } +} + +export default { + async fetch(request, env) { + // Bad: Always using the same ID means one global instance + const id = env.CHAT_ROOM.idFromName("global"); + const stub = env.CHAT_ROOM.get(id); + + await stub.sendMessage("room-123", "user-456", "Hello!"); + return new Response("Sent"); + }, +}; +``` + +### Use deterministic IDs for predictable routing + +Use `getByName()` with meaningful, deterministic strings for consistent routing. The same input always produces the same Durable Object ID, ensuring requests for the same logical entity always reach the same instance. + +```js +// Good: Deterministic ID from a meaningful string +// All requests for "game-abc123" go to the same Durable Object +const stub = env.GAME_SESSION.getByName(gameId); +``` + +Creating a stub does not instantiate or wake up the Durable Object. The Durable Object is only activated when you call a method on the stub. + +Use `newUniqueId()` only when you need a new, random instance and will store the mapping externally: + +```js +// newUniqueId() creates a random ID - useful when creating new instances +// You must store this ID somewhere (e.g., D1) to find it again later +const id = env.GAME_SESSION.newUniqueId(); +const stub = env.GAME_SESSION.get(id); + +// Store the mapping: gameCode -> id.toString() +// await env.DB.prepare("INSERT INTO games (code, do_id) VALUES (?, ?)").bind(gameCode, id.toString()).run(); +``` + +### Use parent-child relationships for related entities + +Do not put all your data in a single Durable Object. When you have hierarchical data (workspaces containing projects, game servers managing matches), create separate child Durable Objects for each entity. The parent coordinates and tracks children, while children handle their own state independently. + +This enables parallelism: operations on different children can happen concurrently, while each child maintains its own single-threaded consistency. + +With this pattern: + +* Listing matches only queries the parent (children stay hibernated) +* Different matches process player actions in parallel +* Each match has its own SQLite database for player data + +### Consider location hints for latency-sensitive applications + +By default, a Durable Object is created near the location of the first request it receives. For most applications, this works well. However, you can provide a location hint to influence where the Durable Object is created. + +```js +// Provide a location hint for where this Durable Object should be created +const id = env.GAME_SESSION.idFromName(gameId, { + locationHint: region, +}); +``` + +Location hints are suggestions, not guarantees. Refer to [Data location](https://developers.cloudflare.com/durable-objects/reference/data-location/) for available regions and details. + +## Storage and state + +### Use SQLite-backed Durable Objects + +[SQLite storage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/) is the recommended storage backend for new Durable Objects. It provides a familiar SQL API for relational queries, indexes, transactions, and better performance than the legacy key-value storage backed Durable Objects. SQLite Durable Objects also support the KV API in synchronous and asynchronous versions. + +Configure your Durable Object class to use SQLite storage in your Wrangler configuration: + +**wrangler.jsonc:** +```jsonc +{ + "migrations": [ + { "tag": "v1", "new_sqlite_classes": ["ChatRoom"] } + ] +} +``` + +**wrangler.toml:** +```toml +[[migrations]] +tag = "v1" +new_sqlite_classes = [ "ChatRoom" ] +``` + +### Initialize storage and run migrations in the constructor + +Use `blockConcurrencyWhile()` in the constructor to run migrations and initialize state before any requests are processed. This ensures your schema is ready and prevents race conditions during initialization. + +```ts +export class ChatRoom extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + // blockConcurrencyWhile() ensures no requests are processed until this completes + ctx.blockConcurrencyWhile(async () => { + await this.migrate(); + }); + } + + private async migrate() { + // Check current schema version + const version = + this.ctx.storage.sql + .exec<{ version: number }>("PRAGMA user_version") + .one()?.version ?? 0; + + if (version < 1) { + this.ctx.storage.sql.exec(` + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at); + PRAGMA user_version = 1; + `); + } + + if (version < 2) { + // Future migration: add a new column + this.ctx.storage.sql.exec(` + ALTER TABLE messages ADD COLUMN edited_at INTEGER; + PRAGMA user_version = 2; + `); + } + } +} +``` + +### Understand the difference between in-memory state and persistent storage + +Durable Objects provide multiple state management layers, each with different characteristics: + +| Type | Speed | Persistence | Use Case | +| - | - | - | - | +| In-memory (class properties) | Fastest | Lost on eviction or crash | Caching, active connections | +| SQLite storage | Fast | Durable across restarts | Primary data storage | +| External (R2, D1) | Variable | Durable, cross-DO accessible | Large files, shared data | + +In-memory state is **not preserved** if the Durable Object is evicted from memory due to inactivity, or if it crashes from an uncaught exception. Always persist important state to SQLite storage. + +> **Warning:** If an uncaught exception occurs in your Durable Object, the runtime may terminate the instance. Any in-memory state will be lost, but SQLite storage remains intact. Always persist critical state to storage before performing operations that might fail. + +### Create indexes for frequently-queried columns + +Just like any database, indexes dramatically improve read performance for frequently-filtered columns. The cost is slightly more storage and marginally slower writes. + +```sql +-- Index for queries filtering by user +CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id); + +-- Index for time-based queries (recent messages) +CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at); + +-- Composite index for user + time queries +CREATE INDEX IF NOT EXISTS idx_messages_user_time ON messages(user_id, created_at); +``` + +### Understand how input and output gates work + +While Durable Objects are single-threaded, JavaScript's `async`/`await` can allow multiple requests to interleave execution while a request waits for the result of an asynchronous operation. Cloudflare's runtime uses **input gates** and **output gates** to prevent data races and ensure correctness by default. + +**Input gates** block new events (incoming requests, fetch responses) while synchronous JavaScript execution is in progress. Awaiting async operations like `fetch()` or KV storage methods opens the input gate, allowing other requests to interleave. However, storage operations provide special protection. + +**Output gates** hold outgoing network messages (responses, fetch requests) until pending storage writes complete. This ensures clients never see confirmation of data that has not been persisted. + +**Write coalescing:** Multiple storage writes without intervening `await` calls are automatically batched into a single atomic implicit transaction: + +```ts +async transfer(fromId: string, toId: string, amount: number) { + // Good: These writes are coalesced into one atomic transaction + this.ctx.storage.sql.exec( + "UPDATE accounts SET balance = balance - ? WHERE id = ?", + amount, + fromId + ); + this.ctx.storage.sql.exec( + "UPDATE accounts SET balance = balance + ? WHERE id = ?", + amount, + toId + ); + this.ctx.storage.sql.exec( + "INSERT INTO transfers (from_id, to_id, amount, created_at) VALUES (?, ?, ?, ?)", + fromId, + toId, + amount, + Date.now() + ); + // All three writes commit together atomically +} +``` + +For more details, see [Durable Objects: Easy, Fast, Correct — Choose three](https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/) and the [glossary](https://developers.cloudflare.com/durable-objects/reference/glossary/). + +### Avoid race conditions with non-storage I/O + +Input gates only protect during storage operations. Non-storage I/O like `fetch()` or writing to R2 allows other requests to interleave, which can cause race conditions. + +To handle this, use optimistic locking (check-and-set) patterns: read a version number before the external call, then verify it has not changed before writing. + +> **Note:** With the legacy KV storage backend, use the [`transaction()`](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#transaction) method for atomic read-modify-write operations across async boundaries. + +### Use `blockConcurrencyWhile()` sparingly + +The [`blockConcurrencyWhile()`](https://developers.cloudflare.com/durable-objects/api/state/#blockconcurrencywhile) method guarantees that no other events are processed until the provided callback completes, even if the callback performs asynchronous I/O. This is useful for operations that must be atomic, such as state initialization from storage in the constructor. + +Because `blockConcurrencyWhile()` blocks *all* concurrency unconditionally, it significantly reduces throughput. If each call takes ~5ms, that individual Durable Object is limited to approximately 200 requests/second. Reserve it for initialization and migrations, not regular request handling. For normal operations, rely on input/output gates and write coalescing instead. + +For atomic read-modify-write operations during request handling, prefer [`transaction()`](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#transaction) over `blockConcurrencyWhile()`. Transactions provide atomicity for storage operations without blocking unrelated concurrent requests. + +> **Warning:** Using `blockConcurrencyWhile()` across I/O operations (such as `fetch()`, KV, R2, or other external API calls) is an anti-pattern. This is equivalent to holding a lock across I/O in other languages or concurrency frameworks — it blocks all other requests while waiting for slow external operations, severely degrading throughput. Keep `blockConcurrencyWhile()` callbacks fast and limited to local storage operations. + +## Communication and API design + +### Use RPC methods instead of the `fetch()` handler + +Projects with a [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-flags/) of `2024-04-03` or later should use RPC methods. RPC is more ergonomic, provides better type safety, and eliminates manual request/response parsing. + +Define public methods on your Durable Object class, and call them directly from stubs with full TypeScript support. + +Refer to [Invoke methods](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/) for more details on RPC and the legacy `fetch()` handler. + +### Initialize Durable Objects explicitly with an `init()` method + +Durable Objects do not know their own name or ID from within. If your Durable Object needs to know its identity (for example, to store a reference to itself or to communicate with related objects), you must explicitly initialize it. + +### Always `await` RPC calls + +When calling methods on a Durable Object stub, always use `await`. Unawaited calls create dangling promises, causing errors to be swallowed and return values to be lost. + +```ts +// Bad: Not awaiting the call +// The message ID is lost, and any errors are swallowed +stub.sendMessage("user-123", "Hello"); + +// Good: Properly awaited +const messageId = await stub.sendMessage("user-123", "Hello"); +``` + +## Error handling + +### Handle errors and use exception boundaries + +Uncaught exceptions in a Durable Object can leave it in an unknown state and may cause the runtime to terminate the instance. Wrap risky operations in try/catch blocks, and handle errors appropriately. + +When calling Durable Objects from a Worker, errors may include `.retryable` and `.overloaded` properties indicating whether the operation can be retried. For transient failures, implement exponential backoff to avoid overwhelming the system. + +Refer to [Error handling](https://developers.cloudflare.com/durable-objects/best-practices/error-handling/) for details on error properties, retry strategies, and exponential backoff patterns. + +## WebSockets and real-time + +### Use the Hibernatable WebSockets API for cost efficiency + +The Hibernatable WebSockets API allows Durable Objects to sleep while maintaining WebSocket connections. This significantly reduces costs for applications with many idle connections. + +```ts +export class ChatRoom extends DurableObject { + async fetch(request: Request): Promise { + const url = new URL(request.url); + + if (url.pathname === "/websocket") { + // Check for WebSocket upgrade + if (request.headers.get("Upgrade") !== "websocket") { + return new Response("Expected WebSocket", { status: 400 }); + } + + const pair = new WebSocketPair(); + const [client, server] = Object.values(pair); + + // Accept the WebSocket with Hibernation API + this.ctx.acceptWebSocket(server); + + return new Response(null, { status: 101, webSocket: client }); + } + + return new Response("Not found", { status: 404 }); + } + + // Called when a message is received (even after hibernation) + async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { + const data = typeof message === "string" ? message : "binary data"; + + // Broadcast to all connected clients + for (const client of this.ctx.getWebSockets()) { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(data); + } + } + } + + // Called when a WebSocket is closed + async webSocketClose( + ws: WebSocket, + code: number, + reason: string, + wasClean: boolean + ) { + console.log(`WebSocket closed: ${code} ${reason}`); + } + + // Called when a WebSocket error occurs + async webSocketError(ws: WebSocket, error: unknown) { + console.error("WebSocket error:", error); + } +} +``` + +With the Hibernation API, your Durable Object can go to sleep when there is no active JavaScript execution, but WebSocket connections remain open. When a message arrives, the Durable Object wakes up automatically. + +Refer to [WebSockets](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) for more details. + +### Use `serializeAttachment()` to persist per-connection state + +WebSocket attachments let you store metadata for each connection that survives hibernation. Use this for user IDs, session tokens, or other per-connection data. + +## Scheduling and lifecycle + +### Use alarms for per-entity scheduled tasks + +Each Durable Object can schedule its own future work using the [Alarms API](https://developers.cloudflare.com/durable-objects/api/alarms/), allowing a Durable Object to execute background tasks on any interval without an incoming request, RPC call, or WebSocket message. + +Key points about alarms: + +* **`setAlarm(timestamp)`** schedules the `alarm()` handler to run at any time in the future (millisecond precision) +* **Alarms do not repeat automatically** — you must call `setAlarm()` again to schedule the next execution +* **Only schedule alarms when there is work to do** — avoid waking up every Durable Object on short intervals (seconds), as each alarm invocation incurs costs + +### Make alarm handlers idempotent + +In rare cases, alarms may fire more than once. Your `alarm()` handler should be safe to run multiple times without causing issues. + +### Clean up storage with `deleteAll()` + +To fully clear a Durable Object's storage, call `deleteAll()`. Simply deleting individual keys or dropping tables is not sufficient, as some internal metadata may remain. If you have alarms set, delete those first. + +```ts +async clearStorage() { + // If you have an alarm set, delete it first + await this.ctx.storage.deleteAlarm(); + + // Delete all storage + await this.ctx.storage.deleteAll(); + + // The Durable Object instance still exists, but with empty storage + // A subsequent request will find no data +} +``` + +## Anti-patterns to avoid + +### Do not use a single Durable Object as a global singleton + +A single Durable Object handling all traffic becomes a bottleneck. While async operations allow request interleaving, all synchronous JavaScript execution is single-threaded, and storage operations provide serialization guarantees that limit throughput. + +A common mistake is using a Durable Object for global rate limiting or global counters. This funnels all traffic through a single instance. + +This pattern does not scale. As traffic increases, the single Durable Object becomes a chokepoint. Instead, identify natural coordination boundaries in your application (per user, per room, per document) and create separate Durable Objects for each. + +## Testing and migrations + +### Test with Vitest and plan for class migrations + +Use `@cloudflare/vitest-pool-workers` for testing Durable Objects. The integration provides isolated storage per test and utilities for direct instance access. + +```ts +import { + env, + runInDurableObject, + runDurableObjectAlarm, +} from "cloudflare:test"; +import { describe, it, expect } from "vitest"; + +describe("ChatRoom", () => { + // Each test gets isolated storage automatically + it("should send and retrieve messages", async () => { + const id = env.CHAT_ROOM.idFromName("test-room"); + const stub = env.CHAT_ROOM.get(id); + + // Call RPC methods directly on the stub + await stub.sendMessage("user-1", "Hello!"); + await stub.sendMessage("user-2", "Hi there!"); + + const messages = await stub.getMessages(10); + expect(messages).toHaveLength(2); + }); + + it("can access instance internals and trigger alarms", async () => { + const id = env.CHAT_ROOM.idFromName("test-room"); + const stub = env.CHAT_ROOM.get(id); + + // Access storage directly for verification + await runInDurableObject(stub, async (instance, state) => { + const count = state.storage.sql + .exec<{ count: number }>("SELECT COUNT(*) as count FROM messages") + .one(); + expect(count.count).toBe(0); // Fresh instance due to test isolation + }); + + // Trigger alarms immediately without waiting + const alarmRan = await runDurableObjectAlarm(stub); + expect(alarmRan).toBe(false); // No alarm was scheduled + }); +}); +``` + +Configure Vitest in your `vitest.config.ts`: + +```ts +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.toml" }, + }, + }, + }, +}); +``` + +For schema changes, run migrations in the constructor using `blockConcurrencyWhile()`. For class renames or deletions, use Wrangler migrations: + +**wrangler.jsonc:** +```jsonc +{ + "migrations": [ + // Rename a class + { "tag": "v2", "renamed_classes": [{ "from": "OldChatRoom", "to": "ChatRoom" }] }, + // Delete a class (removes all data!) + { "tag": "v3", "deleted_classes": ["DeprecatedRoom"] } + ] +} +``` + +**wrangler.toml:** +```toml +[[migrations]] +tag = "v2" + + [[migrations.renamed_classes]] + from = "OldChatRoom" + to = "ChatRoom" + +[[migrations]] +tag = "v3" +deleted_classes = [ "DeprecatedRoom" ] +``` + +Refer to [Durable Objects migrations](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/) for more details on class migrations, and [Testing with Durable Objects](https://developers.cloudflare.com/durable-objects/examples/testing-with-durable-objects/) for comprehensive testing patterns including SQLite queries and alarm testing. diff --git a/tmp/userdo.md b/tmp/userdo.md new file mode 100644 index 0000000..8a66e52 --- /dev/null +++ b/tmp/userdo.md @@ -0,0 +1,151 @@ +# UserDO: Per-User Durable Objects + +> Source: https://coey.dev/userdo + +User data pods with live updates and boring ops. + +Each user gets a Durable Object—ordered state, simple auth, instant SSE. + +## The Quick Version + +Give every user their own Durable Object. Read/write their profile and stream updates over SSE. It's simple, ordered, and fast. + +Great for settings, presence, inboxes, and profile-driven UIs. + +## Architecture + +``` +Client <-> Worker -> UserDO(name = user.sub) + | | + JWT auth SSE updates +``` + +## Infrastructure + +Install the package. + +```bash +# Install +bun install userdo +``` + +## Your Durable Object + +Extend `UserDO` to add your tables and methods. + +```ts +// 1) Extend UserDO (your data + logic) +import { UserDO, type Env } from "userdo/server"; +import { z } from "zod"; + +const PostSchema = z.object({ title: z.string(), content: z.string() }); + +export class BlogDO extends UserDO { + posts: any; + constructor(state: DurableObjectState, env: Env) { + super(state, env); + this.posts = this.table('posts', PostSchema, { userScoped: true }); + } + async createPost(title: string, content: string) { + return await this.posts.create({ title, content }); + } + async getPosts() { + return await this.posts.orderBy('createdAt', 'desc').get(); + } +} +``` + +## Worker + +Auth endpoints come built-in. Add your own routes. + +```ts +// 2) Create Worker (auth built-in; add your endpoints) +import { createUserDOWorker, createWebSocketHandler, getUserDOFromContext } from 'userdo/server'; +import type { BlogDO } from './blog-do'; + +const app = createUserDOWorker('BLOG_DO'); +const wsHandler = createWebSocketHandler('BLOG_DO'); + +app.post('/api/posts', async (c) => { + const user = c.get('user'); + if (!user) return c.json({ error: 'Unauthorized' }, 401); + const { title, content } = await c.req.json(); + const blog = getUserDOFromContext(c, user.email, 'BLOG_DO') as unknown as BlogDO; + const post = await blog.createPost(title, content); + return c.json({ post }); +}); + +export default { + async fetch(request: Request, env: any, ctx: any) { + if (request.headers.get('upgrade') === 'websocket') return wsHandler.fetch(request, env, ctx); + return app.fetch(request, env, ctx); + } +}; +``` + +## Client + +Use the browser client. It handles auth and real-time. + +```ts +// 4) Browser client +import { UserDOClient } from 'userdo/client'; + +const client = new UserDOClient('/api'); +await client.signup('user@example.com', 'password'); +await client.login('user@example.com', 'password'); +client.onChange('table:posts', (evt) => console.log('post changed', evt)); +``` + +## Wrangler Config + +Enable SQLite for your DO via migration. + +```jsonc +{ + "main": "src/index.ts", + "compatibility_flags": ["nodejs_compat"], + "vars": { "JWT_SECRET": "your-jwt-secret-here" }, + "durable_objects": { + "bindings": [ { "name": "BLOG_DO", "class_name": "BlogDO" } ] + }, + "migrations": [ { "tag": "v1", "new_sqlite_classes": ["BlogDO"] } ] +} +``` + +## Built-In Endpoints + +``` +Built-in endpoints: +- POST /api/signup +- POST /api/login +- POST /api/logout +- GET /api/me +- GET /api/ws (WebSocket) +- GET /data +- POST /data +- Organizations: /api/organizations... +``` + +## Organizations + +Org-scoped tables and membership management. + +```ts +// Organization-scoped example +import { UserDO, type Env } from 'userdo/server'; +import { z } from 'zod'; + +const Project = z.object({ name: z.string() }); +const Task = z.object({ title: z.string() }); + +export class TeamDO extends UserDO { + projects: any; tasks: any; + constructor(state: DurableObjectState, env: Env) { + super(state, env); + this.projects = this.table('projects', Project, { organizationScoped: true }); + this.tasks = this.table('tasks', Task, { organizationScoped: true }); + } +} +```