Repository: openai/skills Branch: main Commit: dc48aff82081 Files: 644 Total size: 3.2 MB Directory structure: gitextract_f89akwzt/ ├── .gitignore ├── README.md ├── contributing.md └── skills/ ├── .curated/ │ ├── aspnet-core/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ ├── _sections.md │ │ ├── apis-minimal-and-controllers.md │ │ ├── data-state-and-services.md │ │ ├── program-and-pipeline.md │ │ ├── realtime-grpc-and-background-work.md │ │ ├── security-and-identity.md │ │ ├── source-map.md │ │ ├── stack-selection.md │ │ ├── testing-performance-and-operations.md │ │ ├── ui-blazor.md │ │ ├── ui-mvc.md │ │ ├── ui-razor-pages.md │ │ └── versioning-and-upgrades.md │ ├── chatgpt-apps/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ ├── app-archetypes.md │ │ │ ├── apps-sdk-docs-workflow.md │ │ │ ├── interactive-state-sync-patterns.md │ │ │ ├── repo-contract-and-validation.md │ │ │ ├── search-fetch-standard.md │ │ │ ├── upstream-example-workflow.md │ │ │ └── window-openai-patterns.md │ │ └── scripts/ │ │ └── scaffold_node_ext_apps.mjs │ ├── cloudflare-deploy/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ ├── agents-sdk/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── ai-gateway/ │ │ │ ├── README.md │ │ │ ├── configuration.md │ │ │ ├── dynamic-routing.md │ │ │ ├── features.md │ │ │ ├── sdk-integration.md │ │ │ └── troubleshooting.md │ │ ├── ai-search/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── analytics-engine/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── api/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── api-shield/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── argo-smart-routing/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── bindings/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── bot-management/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── browser-rendering/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── c3/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── cache-reserve/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── containers/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── cron-triggers/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── d1/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── ddos/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── do-storage/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ ├── patterns.md │ │ │ └── testing.md │ │ ├── durable-objects/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── email-routing/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── email-workers/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── hyperdrive/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── images/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── kv/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── miniflare/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── network-interconnect/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── observability/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── pages/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── pages-functions/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── pipelines/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── pulumi/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── queues/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── r2/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── r2-data-catalog/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── r2-sql/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── realtime-sfu/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── realtimekit/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── sandbox/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── secrets-store/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── smart-placement/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── snippets/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── spectrum/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── static-assets/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── stream/ │ │ │ ├── README.md │ │ │ ├── api-live.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── tail-workers/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── terraform/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── tunnel/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ ├── networking.md │ │ │ └── patterns.md │ │ ├── turn/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── turnstile/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── vectorize/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── waf/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── web-analytics/ │ │ │ ├── README.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ ├── integration.md │ │ │ └── patterns.md │ │ ├── workerd/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── workers/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── frameworks.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── workers-ai/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── workers-for-platforms/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── workers-playground/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── workers-vpc/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── workflows/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ ├── wrangler/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── auth.md │ │ │ ├── configuration.md │ │ │ ├── gotchas.md │ │ │ └── patterns.md │ │ └── zaraz/ │ │ ├── IMPLEMENTATION_SUMMARY.md │ │ ├── README.md │ │ ├── api.md │ │ ├── configuration.md │ │ ├── gotchas.md │ │ └── patterns.md │ ├── develop-web-game/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ └── action_payloads.json │ │ └── scripts/ │ │ └── web_game_playwright_client.js │ ├── doc/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── scripts/ │ │ └── render_docx.py │ ├── figma/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ ├── figma-mcp-config.md │ │ └── figma-tools-and-prompts.md │ ├── figma-implement-design/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ └── agents/ │ │ └── openai.yaml │ ├── gh-address-comments/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── scripts/ │ │ └── fetch_comments.py │ ├── gh-fix-ci/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── scripts/ │ │ └── inspect_pr_checks.py │ ├── imagegen/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ ├── cli.md │ │ │ ├── codex-network.md │ │ │ ├── image-api.md │ │ │ ├── prompting.md │ │ │ └── sample-prompts.md │ │ └── scripts/ │ │ └── image_gen.py │ ├── jupyter-notebook/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── assets/ │ │ │ ├── experiment-template.ipynb │ │ │ └── tutorial-template.ipynb │ │ ├── references/ │ │ │ ├── experiment-patterns.md │ │ │ ├── notebook-structure.md │ │ │ ├── quality-checklist.md │ │ │ └── tutorial-patterns.md │ │ └── scripts/ │ │ └── new_notebook.py │ ├── linear/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ └── agents/ │ │ └── openai.yaml │ ├── netlify-deploy/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ ├── cli-commands.md │ │ ├── deployment-patterns.md │ │ └── netlify-toml.md │ ├── notion-knowledge-capture/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── evaluations/ │ │ │ ├── README.md │ │ │ ├── conversation-to-wiki.json │ │ │ └── decision-record.json │ │ ├── examples/ │ │ │ ├── conversation-to-faq.md │ │ │ ├── decision-capture.md │ │ │ └── how-to-guide.md │ │ └── reference/ │ │ ├── database-best-practices.md │ │ ├── decision-log-database.md │ │ ├── documentation-database.md │ │ ├── faq-database.md │ │ ├── how-to-guide-database.md │ │ ├── learning-database.md │ │ └── team-wiki-database.md │ ├── notion-meeting-intelligence/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── evaluations/ │ │ │ ├── README.md │ │ │ ├── decision-meeting-prep.json │ │ │ └── status-meeting-prep.json │ │ ├── examples/ │ │ │ ├── customer-meeting.md │ │ │ ├── executive-review.md │ │ │ ├── project-decision.md │ │ │ └── sprint-planning.md │ │ └── reference/ │ │ ├── brainstorming-template.md │ │ ├── decision-meeting-template.md │ │ ├── one-on-one-template.md │ │ ├── retrospective-template.md │ │ ├── sprint-planning-template.md │ │ ├── status-update-template.md │ │ └── template-selection-guide.md │ ├── notion-research-documentation/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── evaluations/ │ │ │ ├── README.md │ │ │ ├── basic-research.json │ │ │ └── research-to-database.json │ │ ├── examples/ │ │ │ ├── competitor-analysis.md │ │ │ ├── market-research.md │ │ │ ├── technical-investigation.md │ │ │ └── trip-planning.md │ │ └── reference/ │ │ ├── advanced-search.md │ │ ├── citations.md │ │ ├── comparison-format.md │ │ ├── comparison-template.md │ │ ├── comprehensive-report-format.md │ │ ├── comprehensive-report-template.md │ │ ├── format-selection-guide.md │ │ ├── quick-brief-format.md │ │ ├── quick-brief-template.md │ │ ├── research-summary-format.md │ │ └── research-summary-template.md │ ├── notion-spec-to-implementation/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── evaluations/ │ │ │ ├── README.md │ │ │ ├── basic-spec-implementation.json │ │ │ └── spec-to-tasks.json │ │ ├── examples/ │ │ │ ├── api-feature.md │ │ │ ├── database-migration.md │ │ │ └── ui-component.md │ │ └── reference/ │ │ ├── milestone-summary-template.md │ │ ├── progress-tracking.md │ │ ├── progress-update-template.md │ │ ├── quick-implementation-plan.md │ │ ├── spec-parsing.md │ │ ├── standard-implementation-plan.md │ │ ├── task-creation-template.md │ │ └── task-creation.md │ ├── openai-docs/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ ├── gpt-5p4-prompting-guide.md │ │ ├── latest-model.md │ │ └── upgrading-to-gpt-5p4.md │ ├── pdf/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ └── agents/ │ │ └── openai.yaml │ ├── playwright/ │ │ ├── LICENSE.txt │ │ ├── NOTICE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ ├── cli.md │ │ │ └── workflows.md │ │ └── scripts/ │ │ └── playwright_cli.sh │ ├── playwright-interactive/ │ │ ├── LICENSE.txt │ │ ├── NOTICE.txt │ │ ├── SKILL.md │ │ └── agents/ │ │ └── openai.yaml │ ├── render-deploy/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── assets/ │ │ │ ├── docker.yaml │ │ │ ├── go-api.yaml │ │ │ ├── nextjs-postgres.yaml │ │ │ ├── node-express.yaml │ │ │ ├── python-django.yaml │ │ │ └── static-site.yaml │ │ └── references/ │ │ ├── blueprint-spec.md │ │ ├── codebase-analysis.md │ │ ├── configuration-guide.md │ │ ├── deployment-details.md │ │ ├── direct-creation.md │ │ ├── error-patterns.md │ │ ├── post-deploy-checks.md │ │ ├── runtimes.md │ │ ├── service-types.md │ │ └── troubleshooting-basics.md │ ├── screenshot/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── scripts/ │ │ ├── ensure_macos_permissions.sh │ │ ├── macos_display_info.swift │ │ ├── macos_permissions.swift │ │ ├── macos_window_info.swift │ │ ├── take_screenshot.ps1 │ │ └── take_screenshot.py │ ├── security-best-practices/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ ├── golang-general-backend-security.md │ │ ├── javascript-express-web-server-security.md │ │ ├── javascript-general-web-frontend-security.md │ │ ├── javascript-jquery-web-frontend-security.md │ │ ├── javascript-typescript-nextjs-web-server-security.md │ │ ├── javascript-typescript-react-web-frontend-security.md │ │ ├── javascript-typescript-vue-web-frontend-security.md │ │ ├── python-django-web-server-security.md │ │ ├── python-fastapi-web-server-security.md │ │ └── python-flask-web-server-security.md │ ├── security-ownership-map/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ └── neo4j-import.md │ │ └── scripts/ │ │ ├── build_ownership_map.py │ │ ├── community_maintainers.py │ │ ├── query_ownership.py │ │ └── run_ownership_map.py │ ├── security-threat-model/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ ├── prompt-template.md │ │ └── security-controls-and-assets.md │ ├── sentry/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── scripts/ │ │ └── sentry_api.py │ ├── slides/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── assets/ │ │ │ └── pptxgenjs_helpers/ │ │ │ ├── code.js │ │ │ ├── image.js │ │ │ ├── index.js │ │ │ ├── latex.js │ │ │ ├── layout.js │ │ │ ├── layout_builders.js │ │ │ ├── svg.js │ │ │ ├── text.js │ │ │ └── util.js │ │ ├── references/ │ │ │ └── pptxgenjs-helpers.md │ │ └── scripts/ │ │ ├── create_montage.py │ │ ├── detect_font.py │ │ ├── ensure_raster_image.py │ │ ├── render_slides.py │ │ └── slides_test.py │ ├── sora/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ ├── cinematic-shots.md │ │ │ ├── cli.md │ │ │ ├── codex-network.md │ │ │ ├── prompting.md │ │ │ ├── sample-prompts.md │ │ │ ├── social-ads.md │ │ │ ├── troubleshooting.md │ │ │ └── video-api.md │ │ └── scripts/ │ │ └── sora.py │ ├── speech/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ ├── accessibility.md │ │ │ ├── audio-api.md │ │ │ ├── cli.md │ │ │ ├── codex-network.md │ │ │ ├── ivr.md │ │ │ ├── narration.md │ │ │ ├── prompting.md │ │ │ ├── sample-prompts.md │ │ │ ├── voice-directions.md │ │ │ └── voiceover.md │ │ └── scripts/ │ │ └── text_to_speech.py │ ├── spreadsheet/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── references/ │ │ └── examples/ │ │ └── openpyxl/ │ │ ├── create_basic_spreadsheet.py │ │ ├── create_spreadsheet_with_styling.py │ │ ├── read_existing_spreadsheet.py │ │ └── styling_spreadsheet.py │ ├── transcribe/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── references/ │ │ │ └── api.md │ │ └── scripts/ │ │ └── transcribe_diarize.py │ ├── vercel-deploy/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ └── scripts/ │ │ └── deploy.sh │ ├── winui-app/ │ │ ├── LICENSE.txt │ │ ├── SKILL.md │ │ ├── agents/ │ │ │ └── openai.yaml │ │ ├── config.yaml │ │ └── references/ │ │ ├── _sections.md │ │ ├── accessibility-input-and-localization.md │ │ ├── build-run-and-launch-verification.md │ │ ├── community-toolkit-controls-and-helpers.md │ │ ├── controls-layout-and-adaptive-ui.md │ │ ├── foundation-environment-audit-and-remediation.md │ │ ├── foundation-setup-and-project-selection.md │ │ ├── foundation-template-first-recovery.md │ │ ├── foundation-winui-app-structure.md │ │ ├── motion-animations-and-polish.md │ │ ├── performance-diagnostics-and-responsiveness.md │ │ ├── sample-source-map.md │ │ ├── shell-navigation-and-windowing.md │ │ ├── styling-theming-materials-and-icons.md │ │ ├── testing-debugging-and-review-checklists.md │ │ └── windows-app-sdk-lifecycle-notifications-and-deployment.md │ └── yeet/ │ ├── LICENSE.txt │ ├── SKILL.md │ └── agents/ │ └── openai.yaml └── .system/ ├── openai-docs/ │ ├── LICENSE.txt │ ├── SKILL.md │ ├── agents/ │ │ └── openai.yaml │ └── references/ │ ├── gpt-5p4-prompting-guide.md │ ├── latest-model.md │ └── upgrading-to-gpt-5p4.md ├── skill-creator/ │ ├── LICENSE.txt │ ├── SKILL.md │ ├── agents/ │ │ └── openai.yaml │ ├── references/ │ │ └── openai_yaml.md │ └── scripts/ │ ├── generate_openai_yaml.py │ ├── init_skill.py │ └── quick_validate.py └── skill-installer/ ├── LICENSE.txt ├── SKILL.md ├── agents/ │ └── openai.yaml └── scripts/ ├── github_utils.py ├── install-skill-from-github.py └── list-skills.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # macOS system files .DS_Store # Python bytecode files __pycache__/ *.pyc ================================================ FILE: README.md ================================================ # Agent Skills Agent Skills are folders of instructions, scripts, and resources that AI agents can discover and use to perform at specific tasks. Write once, use everywhere. Codex uses skills to help package capabilities that teams and individuals can use to complete specific tasks in a repeatable way. This repository catalogs skills for use and distribution with Codex. Learn more: - [Using skills in Codex](https://developers.openai.com/codex/skills) - [Create custom skills in Codex](https://developers.openai.com/codex/skills/create-skill) - [Agent Skills open standard](https://agentskills.io) ## Installing a skill Skills in [`.system`](skills/.system/) are automatically installed in the latest version of Codex. To install [curated](skills/.curated/) or [experimental](skills/.experimental/) skills, you can use the `$skill-installer` inside Codex. Curated skills can be installed by name (defaults to `skills/.curated`): ``` $skill-installer gh-address-comments ``` For experimental skills, specify the skill folder. For example: ``` $skill-installer install the create-plan skill from the .experimental folder ``` Or provide the GitHub directory URL: ``` $skill-installer install https://github.com/openai/skills/tree/main/skills/.experimental/create-plan ``` After installing a skill, restart Codex to pick up new skills. ## License The license of an individual skill can be found directly inside the skill's directory inside the `LICENSE.txt` file. ================================================ FILE: contributing.md ================================================ ## Contributing ### Community values - **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/). - **Assume good intent.** Written communication is hard - err on the side of generosity. - **Teach & learn.** If you spot something confusing, open an issue or PR with improvements. ### Security & responsible AI Have you discovered a vulnerability or have concerns about model output? Please e-mail **security@openai.com** and we will respond promptly. ================================================ FILE: skills/.curated/aspnet-core/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/aspnet-core/SKILL.md ================================================ --- name: aspnet-core description: Build, review, refactor, or architect ASP.NET Core web applications using current official guidance for .NET web development. Use when working on Blazor Web Apps, Razor Pages, MVC, Minimal APIs, controller-based Web APIs, SignalR, gRPC, middleware, dependency injection, configuration, authentication, authorization, testing, performance, deployment, or ASP.NET Core upgrades. --- # ASP.NET Core ## Overview Choose the right ASP.NET Core application model, compose the host and request pipeline correctly, and implement features in the framework style Microsoft documents today. Load the smallest set of references that fits the task. Do not load every reference by default. ## Workflow 1. Confirm the target framework, SDK, and current app model. 2. Open [references/stack-selection.md](references/stack-selection.md) first for new apps or major refactors. 3. Open [references/program-and-pipeline.md](references/program-and-pipeline.md) next for `Program.cs`, DI, configuration, middleware, routing, logging, and static assets. 4. Open exactly one primary app-model reference: - [references/ui-blazor.md](references/ui-blazor.md) - [references/ui-razor-pages.md](references/ui-razor-pages.md) - [references/ui-mvc.md](references/ui-mvc.md) - [references/apis-minimal-and-controllers.md](references/apis-minimal-and-controllers.md) 5. Add cross-cutting references only as needed: - [references/data-state-and-services.md](references/data-state-and-services.md) - [references/security-and-identity.md](references/security-and-identity.md) - [references/realtime-grpc-and-background-work.md](references/realtime-grpc-and-background-work.md) - [references/testing-performance-and-operations.md](references/testing-performance-and-operations.md) 6. Open [references/versioning-and-upgrades.md](references/versioning-and-upgrades.md) before introducing new platform APIs into an older solution or when migrating between major versions. 7. Use [references/source-map.md](references/source-map.md) when you need the Microsoft Learn section that corresponds to a task not already covered by the focused references. ## Default Operating Assumptions - Prefer the latest stable ASP.NET Core and .NET unless the repository or user request pins an older target. - As of March 2026, prefer .NET 10 / ASP.NET Core 10 for new production work. Treat ASP.NET Core 11 as preview unless the user explicitly asks for preview features. - Prefer `WebApplicationBuilder` and `WebApplication`. Avoid older `Startup` and `WebHost` patterns unless the codebase already uses them or the task is migration. - Prefer built-in DI, options/configuration, logging, ProblemDetails, OpenAPI, health checks, rate limiting, output caching, and Identity before adding third-party infrastructure. - Keep feature slices cohesive so the page, component, endpoint, controller, validation, service, data access, and tests are easy to trace. - Respect the existing app model. Do not rewrite Razor Pages to MVC or controllers to Minimal APIs without a clear reason. ## Reference Guide - [references/_sections.md](references/_sections.md): Quick index and reading order. - [references/stack-selection.md](references/stack-selection.md): Choose the right ASP.NET Core application model and template. - [references/program-and-pipeline.md](references/program-and-pipeline.md): Structure `Program.cs`, services, middleware, routing, configuration, logging, and static assets. - [references/ui-blazor.md](references/ui-blazor.md): Build Blazor Web Apps, choose render modes, and use components, forms, and JS interop correctly. - [references/ui-razor-pages.md](references/ui-razor-pages.md): Build page-focused server-rendered apps with handlers, model binding, and conventions. - [references/ui-mvc.md](references/ui-mvc.md): Build controller/view applications with clear separation of concerns. - [references/apis-minimal-and-controllers.md](references/apis-minimal-and-controllers.md): Build HTTP APIs with Minimal APIs or controllers, including validation and response patterns. - [references/data-state-and-services.md](references/data-state-and-services.md): Use EF Core, `DbContext`, options, `IHttpClientFactory`, session, temp data, and app state responsibly. - [references/security-and-identity.md](references/security-and-identity.md): Apply authentication, authorization, Identity, secrets, data protection, CORS, CSRF, and HTTPS guidance. - [references/realtime-grpc-and-background-work.md](references/realtime-grpc-and-background-work.md): Use SignalR, gRPC, and hosted services. - [references/testing-performance-and-operations.md](references/testing-performance-and-operations.md): Add integration tests, browser tests, caching, compression, health checks, rate limits, and deployment concerns. - [references/versioning-and-upgrades.md](references/versioning-and-upgrades.md): Handle target frameworks, breaking changes, obsolete APIs, and migrations. - [references/source-map.md](references/source-map.md): Map the official ASP.NET Core documentation tree to the references in this skill. ## Execution Notes - When generating new code, start from the correct `dotnet new` template and keep the generated structure recognizable. - When editing an existing solution, follow the solution's conventions first and use these references to avoid framework misuse or outdated patterns. - When a task mentions "latest", verify the feature on Microsoft Learn or the ASP.NET Core docs repo before relying on memory. ================================================ FILE: skills/.curated/aspnet-core/agents/openai.yaml ================================================ interface: display_name: "ASP.NET Core" short_description: "[Windows only] Build and review ASP.NET Core web apps" icon_large: "./assets/dotnet-logo.png" default_prompt: "Create a new $aspnet-core website for me." ================================================ FILE: skills/.curated/aspnet-core/references/_sections.md ================================================ # Reference Sections Use this file as the routing table for the rest of the skill. ## Start Here - New app or major redesign: `stack-selection.md` -> `program-and-pipeline.md` -> one primary app-model reference -> `security-and-identity.md` -> `testing-performance-and-operations.md` - Existing app feature work: primary app-model reference -> `program-and-pipeline.md` -> any needed cross-cutting references - API-first work: `apis-minimal-and-controllers.md` -> `security-and-identity.md` -> `data-state-and-services.md` -> `testing-performance-and-operations.md` - Authentication, authorization, or secrets: `security-and-identity.md` - Realtime, streaming, or background processing: `realtime-grpc-and-background-work.md` - Upgrade or migration work: `versioning-and-upgrades.md` ## Primary References | File | Open when | | --- | --- | | `stack-selection.md` | Choose Blazor, Razor Pages, MVC, Minimal APIs, controllers, SignalR, or gRPC | | `program-and-pipeline.md` | Structure `Program.cs`, services, configuration, middleware, routing, logging, static files, and app startup | | `ui-blazor.md` | Build or review Blazor Web Apps and component-based UI | | `ui-razor-pages.md` | Build or review page-focused server-rendered applications | | `ui-mvc.md` | Build or review controller/view applications | | `apis-minimal-and-controllers.md` | Build or review HTTP APIs | ## Cross-Cutting References | File | Open when | | --- | --- | | `data-state-and-services.md` | Register services, use EF Core, handle options/configuration, or manage app state | | `security-and-identity.md` | Add Identity, cookies, bearer auth, policies, CORS, CSRF, HTTPS, or secrets handling | | `realtime-grpc-and-background-work.md` | Add SignalR, gRPC, streaming, or hosted services | | `testing-performance-and-operations.md` | Add tests, caching, compression, health checks, rate limits, deployment, or proxy configuration | | `versioning-and-upgrades.md` | Migrate across ASP.NET Core versions, avoid obsolete APIs, or target preview features deliberately | | `source-map.md` | Map a task to the official ASP.NET Core documentation tree | ## Reading Strategy - Open one app-model reference at a time unless the codebase genuinely mixes models. - Prefer the framework's built-in abstractions first. - Check `versioning-and-upgrades.md` before introducing APIs that might not exist in the repository's target framework. ================================================ FILE: skills/.curated/aspnet-core/references/apis-minimal-and-controllers.md ================================================ # APIs: Minimal And Controllers Primary docs: - https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis - https://learn.microsoft.com/aspnet/core/web-api/ - https://learn.microsoft.com/aspnet/core/fundamentals/error-handling-api ## First Decision Choose between: - Minimal APIs for focused, low-ceremony HTTP endpoints - controller-based APIs for richer MVC conventions and attribute-driven behavior Do not mix both styles in the same feature unless that split is genuinely useful. ## Minimal API Guidance Prefer Minimal APIs when the surface is small to medium and you want concise endpoint definitions. Good defaults: - organize endpoints with route groups - keep route handlers thin - move business logic into services - prefer `TypedResults` over untyped results - use endpoint filters when cross-cutting behavior belongs at the endpoint layer - use built-in validation support on supported target frameworks Minimal API reminders: - handler parameters can be bound from route, query, headers, body, form, or DI - authorization can be applied with `RequireAuthorization` - return `IResult` or `TypedResults` when response shape matters - use OpenAPI support for discoverable contracts On .NET 10, Minimal APIs support built-in validation with `AddValidation()`. Use that instead of inventing parallel validation infrastructure when the target framework supports it. ## Controller API Guidance Prefer controllers when the API needs: - `[ApiController]` behaviors - attribute routing and conventions - filters - custom formatters - mature controller organization in an existing codebase Controller defaults: - derive API controllers from `ControllerBase` - annotate with `[ApiController]` - use attribute routing - return ProblemDetails-compatible failures - let automatic model validation handle invalid requests unless there is a concrete override requirement Key `[ApiController]` behaviors: - attribute routing is required - invalid model state automatically becomes HTTP 400 - binding source inference applies - error responses use ProblemDetails patterns ## Shared API Practices - Keep request and response DTOs separate from persistence models - Use version-stable route and payload contracts - Use `CreatedAt...` patterns for resource creation - Prefer explicit status codes and typed results over implicit behavior - Apply authorization at the endpoint or controller boundary, not only inside service methods - Use `ProblemDetails` for errors instead of ad hoc JSON shapes ## Browser-Facing Notes - Be careful with cookie-authenticated API endpoints and CORS - For browser-based form or file upload endpoints, account for antiforgery requirements - In ASP.NET Core 10, known API endpoints no longer use cookie-login redirects by default; rely on API-appropriate unauthorized responses instead ## Native AOT Use `dotnet new webapiaot` only when native AOT is an explicit deployment requirement. Treat it as a constraint that affects library choice, reflection, JSON patterns, and compatibility. ================================================ FILE: skills/.curated/aspnet-core/references/data-state-and-services.md ================================================ # Data, State, And Services Primary docs: - https://learn.microsoft.com/aspnet/core/data/ - https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injection - https://learn.microsoft.com/aspnet/core/fundamentals/http-requests - https://learn.microsoft.com/aspnet/core/fundamentals/app-state ## Dependency Injection Defaults - Register infrastructure and business services in `Program.cs` - Inject dependencies through constructors by default - Keep scoped services request-bound - Avoid resolving scoped services from singletons - Use keyed or named patterns only when there is a real need for multiple implementations ## EF Core And DbContext Use EF Core for common relational data access patterns unless the repository already uses another data layer. Default guidance: - register `DbContext` with `AddDbContext` - treat `DbContext` as scoped - keep queries and transactions in services, not UI code - use migrations intentionally - keep entities out of public API contracts and UI view models Use `IDbContextFactory` when the execution model is not request-scoped, such as: - Blazor components with longer-lived scopes - background services - explicit factory-driven data work ## Options And Configuration - Bind structured configuration into options classes - validate options early when bad configuration should fail fast - keep configuration access close to the service that owns it - avoid scattering raw configuration keys across the codebase ## Outbound HTTP Use `IHttpClientFactory` for outbound HTTP calls. Prefer: - named clients for distinct external systems - typed clients for richer integrations - delegating handlers for retries, headers, or telemetry concerns Avoid manual `new HttpClient()` patterns scattered through request handlers. ## App State Use the smallest state mechanism that fits: - query string or route values for transparent request state - form posts for user input - TempData for short-lived redirect-friendly messages - session only when necessary and with an understanding of its server-side and scaling implications Do not treat session as the primary application data store. ## Caching And State Boundaries - Keep cached data derivable from a durable source - Separate cache shape from persistence shape when it improves safety or performance - Revisit session, in-memory cache, and singleton state when the app scales to multiple instances ================================================ FILE: skills/.curated/aspnet-core/references/program-and-pipeline.md ================================================ # Program And Pipeline Primary docs: - https://learn.microsoft.com/aspnet/core/fundamentals/ - https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/webapplication - https://learn.microsoft.com/aspnet/core/fundamentals/middleware/ - https://learn.microsoft.com/aspnet/core/fundamentals/configuration/ ## Startup Shape Prefer the modern hosting model: 1. Create `var builder = WebApplication.CreateBuilder(args);` 2. Register services on `builder.Services` 3. Build `var app = builder.Build();` 4. Configure middleware in the correct order 5. Map endpoints 6. Call `app.Run();` Use older `Startup` patterns only when the repository already uses them or the task is migration. ## Service Registration - Register framework services explicitly: Razor Pages, controllers, Razor components, authentication, authorization, health checks, rate limiting, response compression, output caching, EF Core, and `IHttpClientFactory` - Keep business logic in services instead of controllers, page models, or route handlers - Use constructor injection as the default - Use options classes for structured configuration - Choose lifetimes intentionally: - singleton: stateless or shared infrastructure - scoped: request-bound work such as `DbContext` - transient: lightweight stateless services ## Configuration Defaults `WebApplication.CreateBuilder` already loads configuration from common providers such as: - `appsettings.json` - environment-specific `appsettings.{Environment}.json` - environment variables - command-line arguments For secrets: - use Secret Manager in development - use a secure external store in production - do not commit secrets to source control ## Middleware Order Middleware order is a frequent source of broken behavior. Favor this shape and adjust only with a concrete reason: 1. Forwarded headers if behind a proxy or load balancer 2. Exception handling and HSTS for non-development environments 3. HTTPS redirection 4. Static files 5. Routing when explicit routing middleware is needed 6. CORS when endpoints require it 7. Authentication 8. Authorization 9. Endpoint-specific middleware such as rate limiting or session as required 10. Endpoint mapping with `MapRazorPages`, `MapControllers`, `MapGet`, `MapHub`, or `MapGrpcService` Important ordering rules: - Call `UseAuthentication()` before `UseAuthorization()` - Keep proxy/header processing before auth, redirects, and link generation - Do not insert custom middleware randomly between auth and authorization without a reason - In Minimal API apps, explicit `UseRouting()` is usually unnecessary unless you need to control order ## Routing And Endpoints - Prefer endpoint routing everywhere - Use route groups for larger Minimal API surfaces - Keep MVC and API routes explicit and predictable - Use areas only when the application is large enough to benefit from bounded sections - Keep endpoint names stable when generating links or integrating with clients ## Error Handling - Use centralized exception handling instead of scattered `try/catch` blocks for ordinary request failures - Prefer ProblemDetails-style responses for APIs - Keep the developer exception page limited to development - Separate user-facing failures from internal exception details ## Logging And Diagnostics - Use `ILogger` from DI - Log structured values, not concatenated strings - Put correlation and request diagnostics in middleware or infrastructure, not business logic - Enable HTTP logging only when the scenario warrants it and avoid leaking sensitive data ## Static Assets And Web Root - Keep public assets in `wwwroot` - Treat the web root as publicly readable content - Prevent publishing local-only static content through project file rules when needed - Use Razor Class Libraries for reusable UI assets across apps ## Architectural Defaults - Keep `Program.cs` readable; extract feature registration to extension methods when it starts accumulating unrelated concerns - Prefer vertical slices or feature folders over giant "Controllers", "Services", and "Repositories" buckets with weak boundaries - Keep framework configuration close to the host and business logic out of it ================================================ FILE: skills/.curated/aspnet-core/references/realtime-grpc-and-background-work.md ================================================ # Realtime, gRPC, And Background Work Primary docs: - https://learn.microsoft.com/aspnet/core/signalr/introduction - https://learn.microsoft.com/aspnet/core/grpc/ - https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services ## SignalR Use SignalR when the server must push updates to connected clients in near real time. Good fits: - chat - dashboards - notifications - collaborative editing - live status streams Guidance: - model the hub as a communication boundary, not the home of business logic - use groups and user targeting deliberately - authenticate connections when data is user-specific - plan for scale-out if the app may run on multiple instances Remember that Blazor interactive server rendering already relies on a real-time connection. Do not add a second realtime channel unless the feature truly needs one. ## gRPC Use gRPC for efficient service-to-service communication, strongly typed contracts, and streaming over HTTP/2. Prefer gRPC when: - both ends are under your control - performance and contract fidelity matter - streaming is a first-class requirement Guidance: - keep `.proto` contracts versioned and stable - generate client and server types from contracts - keep auth, logging, and DI integrated with the host - account for browser interoperability differences before choosing gRPC for public browser clients ## Background Work Use `IHostedService` or `BackgroundService` for in-process background tasks tied to the application host. Defaults: - keep background services small and observable - create scopes for scoped dependencies - do not capture scoped services directly in singleton hosted services - respect cancellation tokens - avoid long blocking startup paths If the work is durable, high-volume, or business-critical, consider whether it belongs in an out-of-process queue or worker instead of only inside the web host. ================================================ FILE: skills/.curated/aspnet-core/references/security-and-identity.md ================================================ # Security And Identity Primary docs: - https://learn.microsoft.com/aspnet/core/security/ - https://learn.microsoft.com/aspnet/core/security/authentication/identity - https://learn.microsoft.com/aspnet/core/security/authorization/introduction ## Security Defaults - Use the most secure authentication flow available - Keep secrets out of source code and plain configuration files - Use Secret Manager in development - Use a secure production secret store - Enforce HTTPS - Apply least privilege to users, services, and data access ## Authentication And Authorization Authentication answers who the user or caller is. Authorization answers what they can do. Default pipeline order: 1. `UseAuthentication()` 2. `UseAuthorization()` Apply authorization at boundaries: - `[Authorize]` on controllers, actions, page models, or hubs - `RequireAuthorization()` on endpoints and route groups - policies for reusable rules - roles only when role-based checks are actually the right abstraction Use `AllowAnonymous` sparingly and intentionally. ## Identity Use ASP.NET Core Identity when the app needs first-party user accounts, login flows, password management, email confirmation, MFA, or related account management. Useful starting points: - `dotnet new webapp -au Individual` - `dotnet new mvc -au Individual` Identity guidance: - scaffold only the pages you truly need to customize - keep Identity UI updates maintainable; full scaffolding increases merge and upgrade cost - use policies and claims for authorization rather than encoding all decisions in page logic - persist data-protection keys appropriately in multi-instance deployments On ASP.NET Core 10, Identity metrics are available for observing auth-related behavior. Use them when the app has meaningful authentication traffic or security monitoring requirements. ## CSRF, CORS, And Browser Security - Use antiforgery protection for cookie-based interactive apps and form posts - Do not confuse CORS with authentication or authorization - Avoid permissive `AllowAnyOrigin` plus credentials combinations - Treat browser-side state as untrusted ## HTTPS, HSTS, And Forwarded Headers - redirect HTTP to HTTPS - enable HSTS outside development when appropriate - configure forwarded headers correctly when behind proxies or load balancers - do not generate links or evaluate scheme-sensitive behavior before proxy headers are processed ## Data Protection And Secrets - persist data-protection keys outside ephemeral local storage when the app runs on multiple instances - do not use environment variables as the preferred long-term home for production secrets when a stronger secret store is available - never check production credentials into source control ## Blazor Note For Blazor apps, read the general ASP.NET Core security guidance first and then the Blazor-specific security docs. Some Blazor security guidance adds to or supersedes the general guidance. ================================================ FILE: skills/.curated/aspnet-core/references/source-map.md ================================================ # ASP.NET Core Source Map This skill is synthesized from the official ASP.NET Core documentation tree and overview pages. Use this file to map a task to the corresponding Microsoft Learn area before opening deeper docs. Core sources: - https://learn.microsoft.com/aspnet/core/ - https://raw.githubusercontent.com/dotnet/AspNetCore.Docs/main/aspnetcore/toc.yml - https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore ## Documentation Tree Mapping | ASP.NET Core docs area | Use this skill reference first | | --- | --- | | Overview, Get started, What's new | `stack-selection.md`, `versioning-and-upgrades.md` | | Fundamentals | `program-and-pipeline.md` | | Web apps | `ui-blazor.md`, `ui-razor-pages.md`, `ui-mvc.md` | | APIs | `apis-minimal-and-controllers.md` | | Real-time apps | `realtime-grpc-and-background-work.md` | | Remote Procedure Call apps | `realtime-grpc-and-background-work.md` | | Servers, Host and deploy | `testing-performance-and-operations.md` | | Test, Debug, Troubleshoot | `testing-performance-and-operations.md` | | Data access | `data-state-and-services.md` | | Security and Identity | `security-and-identity.md` | | Performance | `testing-performance-and-operations.md` | | Migration and updates | `versioning-and-upgrades.md` | ## Areas To Consult Directly On Microsoft Learn The following topics are part of the ASP.NET Core documentation tree but are not expanded into their own dedicated reference file here: - globalization and localization - advanced hosting and YARP details - debugger and diagnostics tooling specifics - narrow API-reference pages for individual types When a task is dominated by one of those areas, go straight to the matching Microsoft Learn section after checking the reference files in this skill. ## Practical Deep-Dive Rule - Start with the focused reference in this skill - If the task depends on a narrow platform detail, open the matching Learn article - If the task depends on version-specific behavior, confirm the correct moniker or breaking-changes page ================================================ FILE: skills/.curated/aspnet-core/references/stack-selection.md ================================================ # Stack Selection Primary docs: - https://learn.microsoft.com/aspnet/core/ - https://learn.microsoft.com/aspnet/core/blazor/ - https://learn.microsoft.com/aspnet/core/razor-pages/ - https://learn.microsoft.com/aspnet/core/mvc/overview - https://learn.microsoft.com/aspnet/core/web-api/ - https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis ## Default Version Choice - Prefer the latest stable .NET and ASP.NET Core for new production work. - As of March 2026, that means `net10.0` unless the repository or user request says otherwise. - Treat ASP.NET Core 11 as preview. Do not adopt preview APIs by default. - If the repository already targets `net8.0`, `net9.0`, or another framework, stay within that target unless the task is explicitly an upgrade. ## Template Short Names The current .NET 10 SDK templates include: - `dotnet new blazor` - `dotnet new webapp` - `dotnet new mvc` - `dotnet new webapi` - `dotnet new webapiaot` - `dotnet new grpc` - `dotnet new web` - `dotnet new razorclasslib` Verify template names with `dotnet new list` if the environment differs. ## Application Model Matrix | Model | Prefer when | Watch out for | Typical starting point | | --- | --- | --- | --- | | Blazor Web App | Build full-stack .NET UI with SSR plus optional interactivity | Interactive server needs a live connection; WebAssembly increases payload size | `dotnet new blazor` | | Razor Pages | Build page-focused CRUD, forms, dashboards, and line-of-business apps | Authorization cannot be applied per page handler; use MVC if handler-level control matters | `dotnet new webapp` | | MVC | Build large server-rendered apps with clear controller/view separation, filters, and action-based patterns | More ceremony than Razor Pages for simple page flows | `dotnet new mvc` | | Minimal APIs | Build focused HTTP APIs, internal services, lightweight backends, and small surface areas | Route handlers can become hard to manage if business logic or metadata grows without structure | `dotnet new webapi` or `dotnet new web` | | Controller-based Web API | Build APIs that benefit from `[ApiController]`, content negotiation, filters, formatters, and mature controller conventions | More ceremony than Minimal APIs for small endpoints | `dotnet new webapi` | | SignalR | Add server push, live updates, chat, collaborative UI, or notifications | Requires connection lifecycle management and scale-out planning | Add to an existing ASP.NET Core app | | gRPC | Build service-to-service or streaming RPC over HTTP/2 | Browser support is different from ordinary JSON APIs; use gRPC-Web only when needed | `dotnet new grpc` | ## Fast Heuristics - Choose Blazor Web App when the UI itself should be a .NET component model. - Choose Razor Pages when the app is mostly page and form oriented. - Choose MVC when actions, views, filters, and controller conventions are the center of the design. - Choose Minimal APIs first for small to medium HTTP services. - Switch to controllers when the API needs richer attribute-driven behavior, custom formatters, or strong alignment with existing MVC/Web API conventions. - Keep the current app model in an existing codebase unless the mismatch is causing real complexity. ## Mixed-Model Guidance ASP.NET Core can mix models in one host. Common combinations: - Razor Pages or MVC for server-rendered UI plus Minimal APIs for AJAX or mobile endpoints - Blazor Web App plus Minimal APIs for external integration endpoints - MVC or Razor Pages plus SignalR for live updates - Web API plus gRPC for internal service-to-service calls Mix models only when it simplifies the public surface. Do not add a second app model just because ASP.NET Core allows it. ================================================ FILE: skills/.curated/aspnet-core/references/testing-performance-and-operations.md ================================================ # Testing, Performance, And Operations Primary docs: - https://learn.microsoft.com/aspnet/core/test/integration-tests - https://learn.microsoft.com/aspnet/core/host-and-deploy/ - https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks - https://learn.microsoft.com/aspnet/core/performance/ ## Testing Strategy Use layered testing instead of relying on one style: - unit tests for pure services and business logic - integration tests for request pipeline, DI, database, auth, and framework wiring - browser tests for end-to-end user flows ## Integration Tests Use `Microsoft.AspNetCore.Mvc.Testing` and `WebApplicationFactory` for integration tests. Guidance from the official docs: - use a test host and `HttpClient` - replace services with test doubles when needed - control redirects when asserting auth behavior - handle antiforgery correctly for form posts - prefer SQLite in-memory over the EF Core in-memory provider for more realistic database tests For SPA or browser-driven scenarios, Microsoft recommends browser automation such as Playwright for .NET. ## Performance Defaults Reach for built-in features before custom optimization layers: - output caching - response caching where appropriate - response compression - HTTP request timeouts - rate limiting - static file handling General performance guidance: - measure first - keep database and network round trips visible - reduce payload size - use streaming or pagination when data is large - keep synchronous blocking out of hot paths ## Health Checks And Observability Add health checks for dependencies that matter operationally. Use separate checks or tags when you need: - liveness - readiness - dependency-specific health surfaces Also ensure: - structured logs - request tracing where applicable - metrics for critical paths such as auth, API latency, and background work ## Hosting And Deployment Typical deployment flow: 1. `dotnet publish` 2. deploy the publish output 3. run behind a process manager 4. place a reverse proxy in front when the environment requires it Know the deployment environment: - IIS or Windows Service on Windows - Kestrel plus Nginx or another reverse proxy on Linux - container hosting when the platform expects it Behind proxies or load balancers: - configure forwarded headers - validate scheme, host, and remote IP behavior - test auth redirects and callback URLs in the deployed topology ## Operational Safeguards - add health checks for databases and critical external services - fail fast on invalid configuration where possible - keep secrets out of publish artifacts - verify data-protection key persistence in multi-instance deployments ================================================ FILE: skills/.curated/aspnet-core/references/ui-blazor.md ================================================ # Blazor Primary docs: - https://learn.microsoft.com/aspnet/core/blazor/ - https://learn.microsoft.com/aspnet/core/blazor/fundamentals/ - https://learn.microsoft.com/aspnet/core/blazor/security/ ## Choose Blazor Deliberately Prefer Blazor when the UI itself should be built as reusable .NET components and the team wants a full-stack .NET model. Current guidance centers on the Blazor Web App model, which can combine: - static SSR for fast first render - interactive server rendering - interactive WebAssembly rendering - per-component render mode choices Use standalone Blazor WebAssembly only when the app is intentionally client-heavy or must run as static files without a server-rendered host. ## Render Mode Heuristics - Start with static SSR when the page is mostly read-only and fast first paint matters - Use interactive server rendering when you want rich interactivity without shipping the full .NET runtime to the browser - Use interactive WebAssembly when offline capability, client-side execution, or browser-local compute is the point - Mix render modes only when the split is clear and justified ## Component Patterns - Keep components focused and composable - Move data access and business rules into injected services - Pass data through parameters, not hidden global state - Use forms and validation with Blazor's built-in editing and validation components - Prefer shared Razor Class Libraries for reusable component sets ## Data And Interactivity - Use DI in components with restraint; avoid turning components into service locators - Treat JS interop as an edge mechanism for browser APIs or third-party libraries, not the primary application model - Keep long-running work off the UI event path - Be deliberate about prerendering, streaming rendering, and enhanced navigation when they improve perceived performance ## Security Notes - Follow the general ASP.NET Core security guidance first, then load the Blazor-specific docs for details that supersede it - Remember that client-side code and browser state are not trusted - Keep secrets and privileged operations on the server - Use authorization-aware UI only as a convenience layer; enforce rules on the server as well ## When Not To Use Blazor - Do not force Blazor onto a mostly conventional server-rendered app that already fits Razor Pages or MVC well - Do not choose WebAssembly by default for small interaction needs that SSR or interactive server rendering handles more simply ================================================ FILE: skills/.curated/aspnet-core/references/ui-mvc.md ================================================ # MVC Primary docs: - https://learn.microsoft.com/aspnet/core/mvc/overview - https://learn.microsoft.com/aspnet/core/mvc/controllers/ - https://learn.microsoft.com/aspnet/core/mvc/views/ ## Choose MVC When Actions And Views Matter Prefer MVC when the application benefits from explicit controllers, action-based routing, filters, view models, and a strong separation between orchestration and presentation. This is often the right fit for: - large server-rendered sites - applications with many cross-cutting filters or action conventions - applications that mix views and APIs in the same controller layer - teams already organized around controllers and views ## Core Shape Enable MVC with views using: - `builder.Services.AddControllersWithViews();` - `app.MapControllerRoute(...)` Keep views focused on presentation. Keep controllers focused on HTTP orchestration. Put business rules in services. ## Controller Guidance - Derive from `Controller` when the controller returns views - Keep actions small and explicit - Use model binding and validation instead of manual request parsing - Return view models, not EF entities, to views - Use POST-Redirect-GET for form submissions ## View Guidance - Use layouts, partial views, and Tag Helpers to keep markup consistent - Keep complex display logic out of Razor markup when it becomes hard to follow - Use strongly typed view models - Avoid coupling views directly to persistence models ## Structure And Scale - Use areas for large bounded sections such as Admin or BackOffice - Keep route conventions explicit - Apply filters when behavior truly belongs at the MVC layer - Avoid giant god controllers; split by cohesive feature or resource ## Choosing MVC Over Razor Pages Prefer MVC over Razor Pages when: - multiple related actions share controller-level behavior - handler-level authorization or action filters matter - URL and action design are more natural than page-file routing ================================================ FILE: skills/.curated/aspnet-core/references/ui-razor-pages.md ================================================ # Razor Pages Primary docs: - https://learn.microsoft.com/aspnet/core/razor-pages/ - https://learn.microsoft.com/aspnet/core/tutorials/razor-pages/ ## Choose Razor Pages For Page-Centered Apps Prefer Razor Pages when requests naturally map to pages, forms, and page-level handlers. This is a strong default for internal tools, CRUD apps, account flows, and admin surfaces. ## Core Shape Enable Razor Pages with: - `builder.Services.AddRazorPages();` - `app.MapRazorPages();` Use the `@page` directive to turn a `.cshtml` file into an endpoint. Keep request logic in the paired `PageModel` class when the page is more than trivial. ## Routing Model - File system location defines the route by default - `Pages/Index.cshtml` maps to `/` - `Pages/Store/Index.cshtml` maps to `/Store` - Keep folder structure meaningful because it becomes the URL structure ## PageModel Guidance - Use `OnGet`, `OnPost`, and named handlers for request processing - Use bindable properties and model validation for forms - Keep page models thin; move business logic into injected services - Use Tag Helpers and model binding instead of manual request parsing ## Good Fits - form-heavy workflows - dashboards and back-office applications - simple content with server-side validation - applications where a page is the primary navigation unit ## Key Limitation Do not rely on per-handler authorization with Razor Pages. Microsoft explicitly recommends using MVC controllers when different handlers on the same logical surface need different authorization behavior. Preferred responses to that limitation: - split the handlers into separate pages - move the surface to MVC if action-level authorization is a better fit ## Organizational Guidance - Group related pages into folders - Use partial views for repeated fragments - Use areas only when the application has clear bounded sections - Keep shared layout and page conventions centralized ================================================ FILE: skills/.curated/aspnet-core/references/versioning-and-upgrades.md ================================================ # Versioning And Upgrades Primary docs: - https://learn.microsoft.com/aspnet/core/release-notes/ - https://learn.microsoft.com/aspnet/core/release-notes/aspnetcore-10.0 - https://learn.microsoft.com/aspnet/core/release-notes/aspnetcore-9.0 - https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/breaking-changes ## Versioning Default - For new production apps in March 2026, prefer `net10.0` - For existing apps, match the repository's target framework unless the task is explicitly an upgrade - Before using a new API, confirm it exists in the target framework ## Upgrade Workflow 1. Identify the current target framework and SDK 2. Read the "What's new" and breaking-changes pages for each version hop 3. Compile and resolve obsoletions intentionally 4. Re-run integration tests and auth flows 5. Re-test deployment-specific behavior such as proxies, cookies, and static assets ## High-Value Breaking-Change Checks When moving to ASP.NET Core 10, watch for: - cookie login redirects disabled for known API endpoints - `WithOpenApi` deprecation - `WebHostBuilder`, `IWebHost`, and `WebHost` obsolescence - Razor runtime compilation obsolescence When moving to ASP.NET Core 9, watch for: - `ValidateOnBuild` and `ValidateScopes` enabled in development when using `HostBuilder` - middleware constructor expectations and DI validation changes When moving to ASP.NET Core 8, watch for: - Minimal API `IFormFile` antiforgery requirements - `AddRateLimiter()` and `AddHttpLogging()` requirements when corresponding middleware is used ## Migration Principles - Prefer migration to the modern hosting model when touching startup extensively - Remove compatibility shims only after tests confirm behavior - Avoid mixing new framework idioms with old startup architecture in a half-migrated state - Keep one authoritative target framework in project files unless multi-targeting is deliberate ## Preview Feature Rule Do not introduce preview-only APIs or docs guidance unless the user explicitly asks for preview adoption or the repository is already on preview SDKs. ================================================ FILE: skills/.curated/chatgpt-apps/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/chatgpt-apps/SKILL.md ================================================ --- name: chatgpt-apps description: Build, scaffold, refactor, and troubleshoot ChatGPT Apps SDK applications that combine an MCP server and widget UI. Use when Codex needs to design tools, register UI resources, wire the MCP Apps bridge or ChatGPT compatibility APIs, apply Apps SDK metadata or CSP or domain settings, or produce a docs-aligned project scaffold. Prefer a docs-first workflow by invoking the openai-docs skill or OpenAI developer docs MCP tools before generating code. --- # ChatGPT Apps ## Overview Scaffold ChatGPT Apps SDK implementations with a docs-first, example-first workflow, then generate code that follows current Apps SDK and MCP Apps bridge patterns. Use this skill to produce: - A primary app-archetype classification and repo-shape decision - A tool plan (names, schemas, annotations, outputs) - An upstream starting-point recommendation (official example, ext-apps example, or local fallback scaffold) - An MCP server scaffold (resource registration, tool handlers, metadata) - A widget scaffold (MCP Apps bridge first, `window.openai` compatibility/extensions second) - A reusable Node + `@modelcontextprotocol/ext-apps` starter scaffold for low-dependency fallbacks - A validation report against the minimum working repo contract - Local dev and connector setup steps - A short stakeholder summary of what the app does (when requested) ## Mandatory Docs-First Workflow Use `$openai-docs` first whenever building or changing a ChatGPT Apps SDK app. 1. Invoke `$openai-docs` (preferred) or call the OpenAI docs MCP server directly. 2. Fetch current Apps SDK docs before writing code, especially (baseline pages): - `apps-sdk/build/mcp-server` - `apps-sdk/build/chatgpt-ui` - `apps-sdk/build/examples` - `apps-sdk/plan/tools` - `apps-sdk/reference` 3. Fetch `apps-sdk/quickstart` when scaffolding a new app or generating a first-pass implementation, and check the official examples repo/page before inventing a scaffold from scratch. 4. Fetch deployment/submission docs when the task includes local ChatGPT testing, hosting, or public launch: - `apps-sdk/deploy` - `apps-sdk/deploy/submission` - `apps-sdk/app-submission-guidelines` 5. Cite the docs URLs you used when explaining design choices or generated scaffolds. 6. Prefer current docs guidance over older repo patterns when they differ, and call out compatibility aliases explicitly. 7. If doc search times out or returns poor matches, fetch the canonical Apps SDK pages directly by URL and continue; do not let search failure block scaffolding. If `$openai-docs` is unavailable, use: - `mcp__openaiDeveloperDocs__search_openai_docs` - `mcp__openaiDeveloperDocs__fetch_openai_doc` Read `references/apps-sdk-docs-workflow.md` for suggested doc queries and a compact checklist. Read `references/app-archetypes.md` to classify the request into a small number of supported app shapes before choosing examples or scaffolds. Read `references/repo-contract-and-validation.md` when generating or reviewing a repo so the output stays inside a stable “working app” contract. Read `references/search-fetch-standard.md` when the app is connector-like, data-only, sync-oriented, or meant to work well with company knowledge or deep research. Read `references/upstream-example-workflow.md` when starting a greenfield app or when deciding whether to adapt an upstream example or use the local fallback scaffold. Read `references/window-openai-patterns.md` when the task needs ChatGPT-specific widget behavior or when translating repo examples that use wrapper-specific `app.*` helpers. ## Prompt Guidance Use prompts that explicitly pair this skill with `$openai-docs` so the resulting scaffold is grounded in current docs. Preferred prompt patterns: - `Use $chatgpt-apps with $openai-docs to scaffold a ChatGPT app for with a MCP server and widget.` - `Use $chatgpt-apps with $openai-docs to adapt the closest official Apps SDK example into a ChatGPT app for .` - `Use $chatgpt-apps and $openai-docs to refactor this Apps SDK demo into a production-ready structure with tool annotations, CSP, and URI versioning.` - `Use $chatgpt-apps with $openai-docs to plan tools first, then generate the MCP server and widget code.` When responding, ask for or infer these inputs before coding: - Use case and primary user flows - Read-only vs mutating tools - Demo vs production target - Private/internal use vs public directory submission - Backend language and UI stack - Auth requirements - External API domains for CSP allowlists - Hosting target and local dev approach - Org ownership/verification readiness (for submission tasks) ## Classify The App Before Choosing Code Before choosing examples, repo shape, or scaffolds, classify the request into one primary archetype and state it. - `tool-only` - `vanilla-widget` - `react-widget` - `interactive-decoupled` - `submission-ready` Infer the archetype unless a missing detail is truly blocking. Use the archetype to choose: - whether a UI is needed at all - whether to preserve a split `server/` + `web/` layout - whether to prefer official OpenAI examples, ext-apps examples, or the local fallback scaffold - which validation checks matter most - whether `search` and `fetch` should be the default read-only tool surface Read `references/app-archetypes.md` for the decision rubric. ## Default Starting-Point Order For greenfield apps, prefer these starting points in order: 1. **Official OpenAI examples** when a close example already matches the requested stack or interaction pattern. 2. **Version-matched `@modelcontextprotocol/ext-apps` examples** when the user needs a lower-level or more portable MCP Apps baseline. 3. **`scripts/scaffold_node_ext_apps.mjs`** only when no close example fits, the user wants a tiny Node + vanilla starter, or network access/example retrieval is undesirable. Do not generate a large custom scaffold from scratch if a close upstream example already exists. Copy the smallest matching example, remove unrelated demo code, then patch it to the current docs and the user request. ## Build Workflow ### 0. Classify The App Archetype Pick one primary archetype before planning tools or choosing a starting point. - Prefer a single primary archetype instead of mixing several. - If the request is broad, infer the smallest archetype that can still satisfy it. - Escalate to `submission-ready` only when the user asks for public launch, directory submission, or review-ready deployment. - Call out the chosen archetype in your response so the user can correct it early if needed. ### 1. Plan Tools Before Code Define the tool surface area from user intents. - Use one job per tool. - Write tool descriptions that start with "Use this when..." behavior cues. - Make inputs explicit and machine-friendly (enums, required fields, bounds). - Decide whether each tool is data-only, render-only, or both. - Set annotations accurately (`readOnlyHint`, `destructiveHint`, `openWorldHint`; add `idempotentHint` when true). - If the app is connector-like, data-only, sync-oriented, or intended for company knowledge or deep research, default to the standard `search` and `fetch` tools instead of inventing custom read-only equivalents. - For educational/demo apps, prefer one concept per tool so the model can pick the right example cleanly. - Group demo tools by learning objective: data into the widget, widget actions back into the conversation or tools, host/layout environment signals, and lifecycle/streaming behavior. Read `references/search-fetch-standard.md` when `search` and `fetch` may be relevant. ### 2. Choose an App Architecture Choose the simplest structure that fits the goal. - Use a **minimal demo pattern** for quick prototypes, workshops, or proofs of concept. - Use a **decoupled data/render pattern** for production UX so the widget does not re-render on every tool call. Prefer the decoupled pattern for non-trivial apps: - Data tools return reusable `structuredContent`. - Render tools attach `_meta.ui.resourceUri` and optional `_meta["openai/outputTemplate"]`. - Render tool descriptions state prerequisites (for example, "Call `search` first"). ### 2a. Start From An Upstream Example When One Fits Default to upstream examples for greenfield work when they are close to the requested app. - Check the official OpenAI examples first for ChatGPT-facing apps, polished UI patterns, React components, file upload flows, modal flows, or apps that resemble the docs examples. - Use `@modelcontextprotocol/ext-apps` examples when the request is closer to raw MCP Apps bridge/server wiring, or when version-matched package patterns matter more than ChatGPT-specific polish. - Pick the smallest matching example and copy only the relevant files; do not transplant an entire showcase app unchanged. - After copying, reconcile the example with the current docs you fetched: tool names/descriptions, annotations, `_meta.ui.*`, CSP, URI versioning, and local run instructions. - State which example you chose and why in one sentence. Read `references/upstream-example-workflow.md` for the selection and adaptation rubric. ### 2b. Use the Starter Script When a Low-Dependency Fallback Helps Use `scripts/scaffold_node_ext_apps.mjs` only when the user wants a quick, greenfield Node starter and a vanilla HTML widget is acceptable, and no upstream example is a better starting point. - Run it only after fetching current docs, then reconcile the generated files with the docs you fetched. - If you choose the script instead of an upstream example, say why the fallback is better for that request. - Skip it when a close official example exists, when the user already has an existing app structure, when they need a non-Node stack, when they explicitly want React first, or when they only want a plan/review instead of code. - The script generates a minimal `@modelcontextprotocol/ext-apps` server plus a vanilla HTML widget that uses the MCP Apps bridge by default. - The generated widget keeps follow-up messaging on the standard `ui/message` bridge and only uses `window.openai` for optional host signals/extensions. - After running it, patch the generated output to match the current docs and the user request: adjust tool names/descriptions, annotations, resource metadata, URI versioning, and README/run instructions. ### 3. Scaffold the MCP Server Generate a server that: - Registers a widget resource/template with the MCP Apps UI MIME type (`text/html;profile=mcp-app`) or the SDK constant (`RESOURCE_MIME_TYPE`) when using `@modelcontextprotocol/ext-apps/server` - Registers tools with clear names, schemas, titles, and descriptions - Returns `structuredContent` (model + widget), `content` (model narration), and `_meta` (widget-only data) intentionally - Keeps handlers idempotent or documents non-idempotent behavior explicitly - Includes tool status strings (`openai/toolInvocation/*`) when helpful in ChatGPT Keep `structuredContent` concise. Move large or sensitive widget-only payloads to `_meta`. ### 4. Scaffold the Widget UI Use the MCP Apps bridge first for portability, then add ChatGPT-specific `window.openai` APIs when they materially improve UX. - Listen for `ui/notifications/tool-result` (JSON-RPC over `postMessage`) - Render from `structuredContent` - Use `tools/call` for component-initiated tool calls - Use `ui/update-model-context` only when UI state should change what the model sees Use `window.openai` for compatibility and extensions (file upload, modal, display mode, etc.), not as the only integration path for new apps. #### API Surface Guardrails - Some examples wrap the bridge with an `app` object (for example, `@modelcontextprotocol/ext-apps/react`) and expose helper names like `app.sendMessage()`, `app.callServerTool()`, `app.openLink()`, or host getter methods. - Treat those wrappers as implementation details or convenience layers, not the canonical public API to teach by default. - For ChatGPT-facing guidance, prefer the current documented surface: `window.openai.callTool(...)`, `window.openai.sendFollowUpMessage(...)`, `window.openai.openExternal(...)`, `window.openai.requestDisplayMode(...)`, and direct globals like `window.openai.theme`, `window.openai.locale`, `window.openai.displayMode`, `window.openai.toolInput`, `window.openai.toolOutput`, `window.openai.toolResponseMetadata`, and `window.openai.widgetState`. - If you reference wrapper helpers from repo examples, map them back to the documented `window.openai` or MCP Apps bridge primitives and call out that the wrapper is not the normative API surface. - Use `references/window-openai-patterns.md` for the wrapper-to-canonical mapping and for React helper extraction patterns. ### 5. Add Resource Metadata and Security Set resource metadata deliberately on the widget resource/template: - `_meta.ui.csp` with exact `connectDomains` and `resourceDomains` - `_meta.ui.domain` for app submission-ready deployments - `_meta.ui.prefersBorder` (or OpenAI compatibility alias when needed) - Optional `openai/widgetDescription` to reduce redundant narration Avoid `frameDomains` unless iframe embeds are core to the product. ### 5a. Enforce A Minimum Working Repo Contract Every generated repo should satisfy a small, stable contract before you consider it done. - The repo shape matches the chosen archetype. - The MCP server and tools are wired to a reachable `/mcp` endpoint. - Tools have clear descriptions, accurate annotations, and UI metadata where needed. - Connector-like, data-only, sync-oriented, and company-knowledge-style apps use the standard `search` and `fetch` tool shapes when relevant. - The widget uses the MCP Apps bridge correctly when a UI exists. - The repo includes enough scripts or commands for a user to run and check it locally. - The response explicitly says what validation was run and what was not run. Read `references/repo-contract-and-validation.md` for the detailed checklist and validation ladder. ### 6. Validate the Local Loop Validate against the minimum working repo contract, not just “did files get created.” - Run the lowest-cost checks first: - static contract review - syntax or compile checks when feasible - local `/mcp` health check when feasible - Then move up to runtime checks: - verify tool descriptors and widget rendering in MCP Inspector - test the app in ChatGPT developer mode through HTTPS tunneling - exercise retries and repeated tool calls to confirm idempotent behavior - check widget updates after host events and follow-up tool calls - If you are only delivering a scaffold and are not installing dependencies, still run low-cost checks and say exactly what you did not run. Read `references/repo-contract-and-validation.md` for the validation ladder. ### 7. Connect and Test in ChatGPT (Developer Mode) For local development, include explicit ChatGPT setup steps (not just code/run commands). - Run the MCP server locally on `http://localhost:/mcp` - Expose the local server with a public HTTPS tunnel (for example `ngrok http `) - Use the tunneled HTTPS URL plus `/mcp` path when connecting from ChatGPT - In ChatGPT, enable Developer Mode under **Settings → Apps & Connectors → Advanced settings** - In ChatGPT app settings, create a new app for the remote MCP server and paste the public MCP URL - Tell users to refresh the app after MCP tool/metadata changes so ChatGPT reloads the latest descriptors Note: Some docs/screenshots still use older "connector" terminology. Prefer current product wording ("app") while acknowledging both labels when giving step-by-step instructions. ### 8. Plan Production Hosting and Deployment When the user asks to deploy or prepare for launch, generate hosting guidance for the MCP server (and widget assets if hosted separately). - Host behind a stable public HTTPS endpoint (not a tunnel) with dependable TLS - Preserve low-latency streaming behavior on `/mcp` - Configure secrets outside the repo (environment variables / secret manager) - Add logging, request latency tracking, and error visibility for tool calls - Add basic observability (CPU, memory, request volume) and a troubleshooting path - Re-test the hosted endpoint in ChatGPT Developer Mode before submission ### 9. Prepare Submission and Publish (Public Apps Only) Only include these steps when the user intends a public directory listing. - Use `apps-sdk/deploy/submission` for the submission flow and `apps-sdk/app-submission-guidelines` for review requirements - Keep private/internal apps in Developer Mode instead of submitting - Confirm org verification and Owner-role prerequisites before submission work - Ensure the MCP server uses a public production endpoint (no localhost/testing URLs) and has submission-ready CSP configured - Prepare submission artifacts: app metadata, logo/screenshots, privacy policy URL, support contact, test prompts/responses, localization info - If auth is required, include review-safe demo credentials and test the login path end-to-end - Submit for review in the Platform dashboard, monitor review status, and publish only after approval ## Interactive State Guidance Read `references/interactive-state-sync-patterns.md` when the app has long-lived widget state, repeated interactions, or component-initiated tool calls (for example, games, boards, maps, dashboards, editors). Use it to choose patterns for: - State snapshots plus monotonic event tokens (`stateVersion`, `resetCount`, etc.) - Idempotent retry-safe handlers - `structuredContent` vs `_meta` partitioning - MCP Apps bridge-first update flows with optional `window.openai` compatibility - Decoupled data/render tool architecture for more complex interactive apps ## Output Expectations When using this skill to scaffold code, produce output in this order unless the user asks otherwise: - For direct scaffold requests, do not stop at the plan: give the brief plan, then create the files immediately. 1. Primary app archetype chosen and why 2. Tool plan and architecture choice (minimal vs decoupled) 3. Upstream starting point chosen (official example, ext-apps example, or local fallback scaffold) and why 4. Doc pages/URLs used from `$openai-docs` 5. File tree to create or modify 6. Implementation (server + widget) 7. Validation performed against the minimum working repo contract 8. Local run/test instructions (including tunnel + ChatGPT Developer Mode app setup) 9. Deployment/hosting guidance (if requested or implied) 10. Submission-readiness checklist (for public launch requests) 11. Risks, gaps, and follow-up improvements ## References - `references/app-archetypes.md` for classifying requests into a small number of supported app shapes - `references/apps-sdk-docs-workflow.md` for doc queries, page targets, and code-generation checklist - `references/interactive-state-sync-patterns.md` for reusable patterns for stateful or highly interactive widget apps - `references/repo-contract-and-validation.md` for the minimum working repo contract and lightweight validation ladder - `references/search-fetch-standard.md` for when and how to default to the standard `search` and `fetch` tools - `references/upstream-example-workflow.md` for choosing between official examples, ext-apps examples, and the local fallback scaffold - `references/window-openai-patterns.md` for ChatGPT-specific extensions, wrapper API translation, and React helper patterns - `scripts/scaffold_node_ext_apps.mjs` for a minimal Node + `@modelcontextprotocol/ext-apps` fallback starter scaffold ================================================ FILE: skills/.curated/chatgpt-apps/agents/openai.yaml ================================================ interface: display_name: "ChatGPT Apps" short_description: "Build and scaffold ChatGPT apps" default_prompt: "Use $chatgpt-apps to classify the app archetype first, fetch current OpenAI Apps SDK docs before generating code, default to the standard `search` and `fetch` tools when the app is connector-like or sync-oriented, adapt the closest upstream example when one fits, and only fall back to the local Node scaffold for minimal `@modelcontextprotocol/ext-apps` starters. Produce a working repo shape, then report what validation was actually run." dependencies: tools: - type: "mcp" value: "openaiDeveloperDocs" description: "OpenAI developer docs MCP server for current Apps SDK guidance" transport: "streamable_http" url: "https://developers.openai.com/mcp" policy: allow_implicit_invocation: true ================================================ FILE: skills/.curated/chatgpt-apps/references/app-archetypes.md ================================================ # App Archetypes Load this reference before choosing a starting point for a new ChatGPT app. The goal is to keep the skill inside a small number of supported app shapes instead of inventing a custom structure for every prompt. ## Rule Choose one primary archetype per request and state it. Do not combine several archetypes unless the user explicitly asks for a hybrid app and the extra complexity is necessary. ## Archetypes ### `tool-only` Use when: - The user does not need an in-ChatGPT UI - The task is mainly search, fetch, retrieval, or background actions Default shape: - MCP server only Best starting point: - Official docs and MCP server examples Validation emphasis: - `/mcp` route works - tool schemas and annotations are correct - no unnecessary UI resource is registered - if the app is connector-like or sync-oriented, `search` and `fetch` should be the default read-only tools ### `vanilla-widget` Use when: - The user wants a small demo, workshop starter, or simple inline widget - A single HTML widget is enough - The user wants the fastest path to a working repo Default shape: - Root-level server plus `public/` widget assets Best starting point: - Apps SDK quickstart first - Local fallback scaffold if the quickstart is not a good fit Validation emphasis: - bridge initialization - `ui/notifications/tool-result` - `tools/call` only when the widget is interactive ### `react-widget` Use when: - The user wants a polished UI - The UI is clearly component-based - The user mentions React, TypeScript frontend tooling, or richer design requirements Default shape: - Split `server/` + `web/` layout when the example already uses it Best starting point: - Official OpenAI examples Validation emphasis: - build output is wired into the server correctly - bundle references resolve - widget renders from `structuredContent` ### `interactive-decoupled` Use when: - The app has repeated user interaction - The widget should stay mounted while tools are called repeatedly - The app is a board, map, editor, game, dashboard, or other stateful experience Default shape: - Split `server/` + `web/` - data tools plus render tools Best starting point: - Official OpenAI examples plus `references/interactive-state-sync-patterns.md` Validation emphasis: - tool retries are safe - widget does not remount unnecessarily - state sync is intentional - UI tool calls work independently of model reruns ### `submission-ready` Use when: - The user asks for public launch, review readiness, or directory submission Default shape: - Smallest viable repo that still includes deployment and review requirements Best starting point: - Closest official example that matches the requested stack Validation emphasis: - `_meta.ui.domain` - accurate CSP - auth and review-safe flows - submission prerequisites and artifacts ## Selection Heuristic - If the prompt does not mention a UI, choose `tool-only`. - If the prompt is about a knowledge source, sync app, connector-like integration, or deep research, strongly prefer `tool-only` plus the standard `search` and `fetch` tools unless the user clearly needs a widget. - If the prompt asks for a simple demo or starter, choose `vanilla-widget`. - If the prompt asks for a polished UI or React, choose `react-widget`. - If the prompt implies long-lived client state or repeated interaction, choose `interactive-decoupled`. - Only choose `submission-ready` when the user explicitly asks for launch or review-readiness work. ================================================ FILE: skills/.curated/chatgpt-apps/references/apps-sdk-docs-workflow.md ================================================ # Apps SDK Docs Workflow Use this reference to keep code generation aligned with current OpenAI Apps SDK docs. ## Always Fetch These Pages (Baseline) - `https://developers.openai.com/apps-sdk/build/mcp-server/` - `https://developers.openai.com/apps-sdk/build/chatgpt-ui/` - `https://developers.openai.com/apps-sdk/build/examples/` - `https://developers.openai.com/apps-sdk/plan/tools/` - `https://developers.openai.com/apps-sdk/reference/` ## Fetch Conditionally (Greenfield / First Pass) - `https://developers.openai.com/apps-sdk/quickstart/` for first implementation scaffolds and happy-path wiring - `https://developers.openai.com/apps-sdk/deploy/` when the task includes local ChatGPT testing via tunnel, hosting, or production deployment planning - `https://developers.openai.com/apps-sdk/deploy/submission/` when the task includes public launch, app review, or publishing steps - `https://developers.openai.com/apps-sdk/app-submission-guidelines/` when the task includes submission readiness, policy/reliability checks, or review-risk reduction ## Suggested `openai-docs` / MCP Queries Use focused searches before fetching: - `ChatGPT Apps SDK build MCP server register resource template resourceUri outputTemplate` - `ChatGPT Apps SDK build ChatGPT UI MCP Apps bridge ui/notifications/tool-result` - `ChatGPT Apps SDK examples React widget upload modal Pizzaz` - `Apps SDK define tools annotations readOnlyHint destructiveHint openWorldHint` - `Apps SDK reference tool descriptor _meta ui.resourceUri openai/outputTemplate` - `ChatGPT Apps SDK quickstart build web component tools/call` - `ChatGPT app company knowledge compatibility search fetch tools` - `platform MCP search tool fetch tool schema` - `ChatGPT Apps SDK deploy app local development tunnel ngrok refresh connector` - `ChatGPT Apps SDK submit app review prerequisites app submission guidelines` ## Docs-Derived Checklist (Current Guidance) ### Archetype / Shape - Classify the request into one primary app archetype before choosing examples or scaffolds - Keep the repo shape consistent with that archetype instead of inventing a new structure for each prompt ### Server - Register the widget resource/template with the MCP Apps UI MIME type (`text/html;profile=mcp-app`) or `RESOURCE_MIME_TYPE` when using `@modelcontextprotocol/ext-apps/server` - Version template URIs when widget HTML or JS or CSS changes in a breaking way (treat URI as cache key) - Set `_meta.ui.resourceUri` on render tools; optionally mirror `_meta["openai/outputTemplate"]` for ChatGPT compatibility - Design tool handlers to be idempotent because the model may retry calls - Keep `structuredContent` concise and move widget-only payloads to `_meta` ### Tool Design - Plan one user intent per tool - Use action-oriented names and precise descriptions - Set tool impact hints accurately (`readOnlyHint`, `destructiveHint`, `openWorldHint`) - Split data and render tools so that the model can fetch the data and look at it before choosing to render the widget UI or not - Make the widget input a list of unique identifiers (e.g. `propertyIds` for a render property map widget that takes IDs returned from the fetch properties nearby tool) if you want to make sure the widget only renders 1p data; make the widget input semantically relevant if you want to allow the model to render the widget with generated data (e.g. `questionAndAnswerPairs` for a flashcards widget) - For connector-like, data-only, sync-oriented, or company-knowledge-style apps, prefer the standard `search` and `fetch` tools by default ### UI - Prefer the MCP Apps bridge (`ui/*` notifications + `tools/call`) for new apps - Prefer `ui/message` for follow-up messaging in baseline examples; treat `window.openai.sendFollowUpMessage` as optional ChatGPT-specific compatibility - Treat `window.openai` as compatibility plus optional ChatGPT extensions - Render from `structuredContent` and treat host-delivered data as untrusted input - Use `ui/update-model-context` only for UI state the model should reason about ### Starting Point Selection - Check `apps-sdk/build/examples` and the official examples repo before generating a greenfield scaffold from scratch - Prefer the smallest upstream example that matches the requested stack and interaction pattern - Use the local fallback scaffold only when upstream examples are a poor fit or undesirable for the request ### Resource Metadata / Security - Set `_meta.ui.csp.connectDomains` and `_meta.ui.csp.resourceDomains` exactly - Avoid `frameDomains` unless iframe embedding is central to the experience - Set `_meta.ui.domain` for submission-ready apps - Always set `openai/widgetDescription` to inform the model what the widget is to be used for ### Developer Mode / Local Testing - Run the MCP server locally on `http://localhost:/mcp` - Expose it with a public HTTPS tunnel for ChatGPT access during development - Use the public URL + `/mcp` when adding the app in ChatGPT settings - Include ChatGPT Developer Mode setup and app creation steps in implementation handoff - Remind users to refresh the app after MCP tool/metadata changes - Note terminology differences when relevant: some docs/screenshots may still say "connector" while product UI uses "app" ### Validation - Validate against a minimum working repo contract, not just file creation - Run the cheapest useful syntax or compile check first - If feasible, confirm the local `/mcp` route responds before calling the result “working” - If you cannot run a deeper check, say so explicitly - If the app is connector-like or sync-oriented, verify the `search` and `fetch` tool shapes against the standard ### Production Hosting / Deploy - Prefer a stable public HTTPS endpoint with reliable TLS and low-latency streaming `/mcp` - Document platform-specific secrets handling and environment variables - Include logging/metrics expectations for debugging production tool calls - Re-test the hosted endpoint in ChatGPT Developer Mode before submission ### Submission / Review - Read `deploy/submission` and `app-submission-guidelines` together (process + policy requirements) - Check org verification and Owner-role prerequisites before generating submission steps - Ensure the endpoint is public production infrastructure (not localhost/tunnel/testing URLs) - Ensure CSP is defined and accurate for submission - Prepare submission artifacts (metadata, screenshots, privacy policy/support contacts, test prompts/responses) - If auth is required, prepare review-safe demo credentials and validate them outside internal networks ## Generation Pattern 1. Classify the app archetype. 2. Fetch docs with `$openai-docs`. 3. Check official examples before inventing a scaffold from scratch. 4. Summarize relevant constraints and metadata keys. 5. Propose tool plan and architecture. 6. Adapt the closest example or use the local fallback scaffold. 7. Generate or patch the server scaffold. 8. Generate or patch the widget scaffold. 9. Validate the repo against the minimum working contract. 10. Add local run + tunnel + ChatGPT Developer Mode app setup instructions. 11. Add hosting/deployment guidance when the task implies go-live. 12. Add submission/readiness steps when the user intends public distribution. 13. Call out compatibility aliases vs MCP Apps standard fields. ## Starter Scaffold Script - Use `./scripts/scaffold_node_ext_apps.mjs --app-name ` only when the user wants a greenfield Node + `@modelcontextprotocol/ext-apps` starter and no upstream example is the better fit. - If the file is not executable in the current environment, fall back to `node scripts/scaffold_node_ext_apps.mjs --app-name `. - The script generates `package.json`, `tsconfig.json`, `public/widget.html`, and `src/server.ts`. - It intentionally uses the MCP Apps bridge by default, keeps follow-up messaging on `ui/message`, and limits `window.openai` to optional host signals/extensions. - After generation, compare the output against the docs you fetched and adjust package versions, metadata, transport details, or URI/versioning if the docs changed. ================================================ FILE: skills/.curated/chatgpt-apps/references/interactive-state-sync-patterns.md ================================================ # Interactive State Sync Patterns Use this reference when building ChatGPT apps with long-lived widget state, repeated interactions, or component-initiated tool calls (for example: games, boards, maps, dashboards, editors, or realtime-ish UIs). Do not load this file for simple read-only render apps unless state sync behavior is part of the task. ## When This Reference Helps Read this file when the app needs one or more of these patterns: - Repeated actions that may return similar data (retry, refresh, reset, reroll) - UI controls that trigger tool calls after the initial render - Local widget behavior that should also work outside ChatGPT during development - Multiple tool calls updating one mounted widget over time - Clear separation between model-visible state and widget-only state ## Reusable Patterns ### 1. Snapshot + Event Token Return a stable state snapshot in `structuredContent` and add a monotonic event token for repeated actions that may not change other fields. Examples: - `stateVersion` - `refreshCount` - `resetCount` - `lastMutationId` Use this when the widget must detect "same shape, new event" updates reliably. ### 2. Intent-Focused Tool Surface Prefer small, explicit tools that map to user-visible actions or data operations. - Keep names action-oriented - Use enums and bounded schemas where possible - Avoid kitchen-sink tools that mix unrelated reads and writes This improves model tool selection and reduces malformed calls. ### 3. Idempotent Handlers (or Explicitly Non-Idempotent) Design handlers to tolerate retries. If a tool is not idempotent, make the side effect explicit and confirm intent in the flow. - Reads and pure transforms should usually be idempotent - Writes should include clear impact hints and current-turn confirmation where needed - Repeated calls with the same input should not corrupt widget state ### 4. `structuredContent` / `_meta` Partitioning Partition payloads intentionally: - `structuredContent`: concise model-visible state the widget also uses - `content`: short narration/status text - `_meta`: large maps, caches, or sensitive widget-only hydration data Keep `structuredContent` small enough for follow-up reasoning and chaining. ### 5. MCP Apps Bridge First, `window.openai` Second For new scaffolds: - Prefer MCP Apps bridge notifications and `tools/call` (portable across hosts) - Use `window.openai` as a compatibility layer plus optional ChatGPT extensions This keeps the app portable while still enabling ChatGPT-specific capabilities when helpful. ### 6. Component-Initiated Tool Calls Without Remounting For interactive widgets, allow the UI to call data/action tools directly and update the existing widget state instead of forcing a full re-render/remount every time. This is especially useful for: - Refresh - Retry - Rerun - Toggle/filter actions - Incremental interactions inside one widget session ### 7. Standalone / No-Host Fallback Mode When feasible, make the widget usable without ChatGPT during development: - If host APIs are unavailable, apply local state directly - Preserve basic interactions in a normal browser This speeds up front-end iteration and reduces dependence on connector setup for every UI tweak. ### 8. Decouple Data Tools from Render Tools (When Complexity Grows) Use separate data and render tools when the app has multi-step reasoning or frequent updates. - Data tools fetch/compute/mutate and return reusable `structuredContent` - Render tools attach the widget template and focus on presentation This reduces unnecessary remounts and gives the model a chance to refine data before rendering. ## Common Anti-Patterns - Putting large widget-only blobs into `structuredContent` - Attaching a widget template to every tool when only one render tool needs it - Using hidden client-side state as the source of truth for critical actions - Depending only on `window.openai` APIs for baseline app behavior - Using ambiguous tool names that do not match user intent ## Example App Types That Benefit From These Patterns - Multiplayer or turn-based games - Collaborative boards / task views - Maps with filters and repeated searches - Dashboards with refresh and drill-down actions - Editors or builders with iterative tool calls ================================================ FILE: skills/.curated/chatgpt-apps/references/repo-contract-and-validation.md ================================================ # Repo Contract And Validation Load this reference when scaffolding or reviewing a generated ChatGPT app repo. The goal is not “files were created.” The goal is “the repo is plausibly runnable and follows a stable working-app contract.” ## Minimum Working Repo Contract Every generated repo should satisfy the relevant parts of this contract. ### 1. Shape - The repo shape matches the chosen archetype. - The repo structure is simple enough that a user can identify where the server and widget live. ### 2. Server - There is a clear MCP server entry point. - The server exposes `/mcp`. - The server registers tools intentionally. - If a UI exists, the server registers a resource/template with the MCP Apps UI MIME type. ### 3. Tools - Each tool maps to one user intent. - Descriptions help the model choose the tool. - Required annotations are present and accurate. - UI-linked tools use `_meta.ui.resourceUri`. - `_meta["openai/outputTemplate"]` is treated as optional compatibility, not the primary contract. - When the app is connector-like, data-only, sync-oriented, or intended for company knowledge or deep research, it implements standard `search` and `fetch` tools instead of custom substitutes. ### 4. Widget - The widget initializes the MCP Apps bridge when needed. - The widget can receive `ui/notifications/tool-result`. - The widget renders from `structuredContent`. - Interactive widgets use `tools/call`. - Baseline follow-up messaging uses `ui/message`. - `window.openai` is optional and additive. ### 5. Local Developer Experience - There is a clear way to start the app locally. - There is at least one low-cost check command when the stack supports it. - The response explains how to connect the app in ChatGPT Developer Mode when relevant. ## Validation Ladder Run the highest level you can without overfitting to a single stack. ### Level 0: Static contract review Check for: - chosen archetype is sensible - repo shape matches archetype - `/mcp` route is present - tool/resource/widget responsibilities are coherent - if the app is connector-like or sync-oriented, `search` and `fetch` are present with the expected standard shape ### Level 1: Syntax or compile checks Use the stack-appropriate cheapest check available, for example: - Python syntax check - TypeScript compile check - framework-specific lint or build sanity check if already installed ### Level 2: Local runtime sanity If feasible: - start the server - confirm the health route or `/mcp` endpoint responds ### Level 3: Host loop validation If feasible: - inspect with MCP Inspector - test through ChatGPT Developer Mode - confirm widget updates after tool results ## Reporting Rule Always say which validation level was reached and what was not run. That makes the skill more reliable because it separates: - “repo shape looks right” - “syntax is valid” - “server starts” - “host integration was actually exercised” ================================================ FILE: skills/.curated/chatgpt-apps/references/search-fetch-standard.md ================================================ # Search And Fetch Standard Load this reference when the app is connector-like, data-only, sync-oriented, or meant to work well with company knowledge or deep research. ## Default Rule If the app is primarily a read-only knowledge source, do not invent custom equivalents to `search` and `fetch`. Default to implementing the standard `search` and `fetch` tools exactly, then add other tools only if the use case clearly needs them. ## When This Applies Use the standard by default when the request is about: - a data-only app - a sync app - a company knowledge source - deep research compatibility - a connector-like integration over documents, tickets, wiki pages, CRM records, or similar read-only data ## Tool Requirements ### `search` - Read-only tool - Takes a single query string - Returns exactly one MCP content item with `type: "text"` - That text is a JSON-encoded object with: - `results` - each result has `id`, `title`, and `url` ### `fetch` - Read-only tool - Takes a single document/item id string - Returns exactly one MCP content item with `type: "text"` - That text is a JSON-encoded object with: - `id` - `title` - `text` - `url` - optional `metadata` ## Implementation Rules - Match the schema exactly when the app is intended for company knowledge or deep research compatibility. - Use canonical `url` values for citations. - Mark these tools as read-only. - Prefer these names exactly: `search` and `fetch`. - If you add other read-only tools, they should complement the standard rather than replace it. ## Validation Checks When `search` and `fetch` are relevant, verify: - both tools exist - they are read-only - their input shapes match the standard - their returned payloads are wrapped as one `content` item with JSON-encoded `text` - result URLs are canonical enough for citation use ## Source This standard is described in: - `https://developers.openai.com/apps-sdk/build/mcp-server/#company-knowledge-compatibility` - `https://platform.openai.com/docs/mcp` ================================================ FILE: skills/.curated/chatgpt-apps/references/upstream-example-workflow.md ================================================ # Upstream Example Workflow Load this reference when starting a greenfield ChatGPT app or when deciding whether to adapt an upstream example or use the local fallback scaffold. ## Default Order Prefer these starting points in order: 1. Official OpenAI Apps SDK examples 2. Version-matched `@modelcontextprotocol/ext-apps` examples 3. Local `scripts/scaffold_node_ext_apps.mjs` fallback This keeps the skill aligned with current docs and maintained example code while still preserving a low-dependency fallback when examples are not a good fit. ## Choose The Right Source ### 1. Official OpenAI examples Prefer these when: - The app is clearly ChatGPT-facing - The user wants a polished UI or React component - The task involves file upload, modal flows, display-mode changes, or other ChatGPT extensions - The docs/examples page already shows a similar interaction pattern Typical sources: - `https://developers.openai.com/apps-sdk/build/examples/` - `https://github.com/openai/openai-apps-sdk-examples` - `https://developers.openai.com/apps-sdk/quickstart/` for the smallest vanilla baseline ### 2. `@modelcontextprotocol/ext-apps` examples Prefer these when: - The user needs a lower-level MCP Apps baseline - Portability across MCP Apps-compatible hosts matters more than ChatGPT-specific polish - You want version-matched examples close to the installed `@modelcontextprotocol/ext-apps` package shape This follows the same basic idea as the upstream `create-mcp-app` skill: use maintained examples as the starting point, then adapt them. Typical examples from upstream flows: - `examples/demo-vanilla-html` - `examples/demo-react-simple` - `examples/demo-connectors-api` ### 3. Local fallback scaffold Use `scripts/scaffold_node_ext_apps.mjs` when: - No close upstream example exists - The user wants a tiny Node + vanilla HTML starter - Network/example retrieval is undesirable - You need a throwaway starter to patch quickly during a live coding task Do not prefer the local scaffold just because it is available. It is the fallback, not the default. ## Adaptation Rules - Copy the smallest matching example, not the entire showcase app. - Remove unrelated demo tools, assets, and routes immediately. - Keep the upstream file structure when it is already clean and docs-aligned. - Reconcile the copied example with the current docs before finishing: - tool names and descriptions - annotations (`readOnlyHint`, `destructiveHint`, `openWorldHint`, `idempotentHint` when true) - `_meta.ui.resourceUri` and optional `_meta["openai/outputTemplate"]` - resource `_meta.ui.csp`, `_meta.ui.domain`, and `openai/widgetDescription` - URI versioning for template changes - local run/test instructions - State which example you chose and why. - If you rely on upstream code, note the source repo and branch/tag/commit when practical; avoid silently depending on a floating example shape for long-lived work. ## Minimal Selection Heuristic - If the user asks for **React + polished UI**, start with official OpenAI examples. - If the user asks for **vanilla HTML + tiny demo**, start with the quickstart example; use the local fallback scaffold only if the quickstart is still too opinionated or unavailable. - If the user asks for **portable MCP Apps wiring**, start with `@modelcontextprotocol/ext-apps` examples. - If the user already has an app, adapt their code directly instead of importing a new example. ================================================ FILE: skills/.curated/chatgpt-apps/references/window-openai-patterns.md ================================================ # Window.openai Patterns Load this reference when a task needs ChatGPT-only widget features, when translating older examples that use an `app` wrapper, or when a React widget should read host globals safely. ## Core Rule - Build baseline widget behavior on the MCP Apps bridge: `ui/*` notifications, `tools/call`, `ui/message`, and `ui/update-model-context`. - Use `window.openai` only when the task specifically benefits from ChatGPT-only runtime conveniences. - Treat `window.openai` as additive. The app should still have a coherent baseline path on the MCP Apps standard when possible. ## Canonical `window.openai` Surface ### State And Data - `window.openai.toolInput`: tool arguments supplied by the host - `window.openai.toolOutput`: current `structuredContent` - `window.openai.toolResponseMetadata`: current `_meta` payload (widget-only) - `window.openai.widgetState`: persisted widget-local snapshot - `window.openai.setWidgetState(state)`: persist widget-local snapshot after meaningful UI changes ### Runtime APIs - `window.openai.callTool(name, args)`: call another MCP tool from the widget - `window.openai.sendFollowUpMessage({ prompt, scrollToBottom? })`: ask ChatGPT to post a widget-authored follow-up message - `window.openai.openExternal({ href, redirectUrl? })`: open an external URL through ChatGPT's vetted flow - `window.openai.requestDisplayMode({ mode })`: request `inline`, `pip`, or `fullscreen` - `window.openai.requestModal({ params, template? })`: open a host-owned modal - `window.openai.requestClose()`: ask ChatGPT to close the widget - `window.openai.uploadFile(file)`: upload a file from the widget - `window.openai.getFileDownloadUrl({ fileId })`: resolve a temporary download URL - `window.openai.notifyIntrinsicHeight(...)`: report dynamic height changes - `window.openai.setOpenInAppUrl({ href })`: override the fullscreen punch-out target ### Context Signals - `window.openai.theme` - `window.openai.displayMode` - `window.openai.maxHeight` - `window.openai.safeArea` - `window.openai.view` - `window.openai.userAgent` - `window.openai.locale` ## Mapping From Repo Wrapper Examples - `app.callServerTool({ name, arguments })`: Use `window.openai.callTool(name, args)` when you intentionally want the ChatGPT compatibility layer. Use `tools/call` over the bridge when you want the portable MCP Apps path. - `app.sendMessage(...)`: Use `ui/message` for portable bridge messaging. If the task is intentionally ChatGPT-specific, `window.openai.sendFollowUpMessage({ prompt })` is the closest supported path. - `app.updateModelContext(...)`: Use `ui/update-model-context` over the bridge. This is part of the standard bridge, not a `window.openai` feature. - `app.openLink({ url })`: Use `window.openai.openExternal({ href: url })` when you intentionally want ChatGPT's external navigation flow. - `app.requestDisplayMode({ mode })`: Use `window.openai.requestDisplayMode({ mode })`. - `app.getHostContext()`: Read the documented globals directly (`theme`, `displayMode`, `locale`, `maxHeight`, `safeArea`, `userAgent`). - `app.getHostCapabilities()` / `app.getHostVersion()`: These are wrapper-level convenience APIs. Prefer feature detection (`if (window.openai?.requestModal)`) and the documented globals instead of teaching these as the primary public surface. ## React Helper Extraction - The repo's `src/use-openai-global.ts` is a good baseline for subscribing to host global changes without scattering direct `window.openai` reads through components. - The repo's `src/use-widget-state.ts` is a good baseline for mirroring React state into `window.openai.setWidgetState(...)`. - The repo's `src/use-widget-props.ts` is a good baseline for reading typed `toolOutput` with a local fallback. - Keep these helpers optional. Do not force a React abstraction when a simple vanilla widget is enough. ================================================ FILE: skills/.curated/chatgpt-apps/scripts/scaffold_node_ext_apps.mjs ================================================ #!/usr/bin/env node import { mkdirSync, writeFileSync, existsSync, readdirSync, lstatSync } from "node:fs"; import path from "node:path"; function toSlug(value) { const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); return normalized || "example-chatgpt-app"; } function toToolName(value) { const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, ""); return normalized || "show_example"; } function toTitle(value) { const parts = value.split(/[-_]+/).filter(Boolean); return parts.map((part) => part[0].toUpperCase() + part.slice(1)).join(" ") || "Example"; } function fillTemplate(template, mapping) { let result = template; for (const [key, value] of Object.entries(mapping)) { result = result.replaceAll(key, value); } return result; } function writeFile(filePath, content) { mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, content, "utf8"); } function ensureTargetDir(targetPath, force) { if (existsSync(targetPath)) { if (!lstatSync(targetPath).isDirectory()) { throw new Error(`Output path exists and is not a directory: ${targetPath}`); } if (readdirSync(targetPath).length > 0 && !force) { throw new Error( `Refusing to write into non-empty directory: ${targetPath}\nRe-run with --force to overwrite generated files.` ); } } mkdirSync(targetPath, { recursive: true }); } function buildPackageJson(appSlug) { const packageJson = { name: appSlug, private: true, type: "module", scripts: { dev: "tsx watch src/server.ts", start: "tsx src/server.ts", check: "tsc --noEmit", }, dependencies: { "@modelcontextprotocol/ext-apps": "^1.0.1", "@modelcontextprotocol/sdk": "^1.20.2", zod: "^3.25.76", }, devDependencies: { "@types/node": "^24.3.0", tsx: "^4.19.4", typescript: "^5.9.2", }, }; return `${JSON.stringify(packageJson, null, 2)}\n`; } function buildTsconfig() { return `{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "types": ["node"], "outDir": "dist" }, "include": ["src/**/*.ts"] } `; } const WIDGET_TEMPLATE = ` __APP_TITLE__

__APP_TITLE__ starter

Waiting for tool output

Call the __TOOL_NAME__ tool to hydrate this widget.

This widget uses the MCP Apps bridge by default.
`; const SERVER_TEMPLATE = `import { createServer } from "node:http"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.resolve(__dirname, ".."); const WIDGET_URI = "__WIDGET_URI__"; const WIDGET_HTML = readFileSync( path.join(ROOT_DIR, "public", "widget.html"), "utf8" ); function createAppServer(): McpServer { const server = new McpServer({ name: "__APP_SLUG__", version: "0.1.0", }); registerAppResource( server, "main-widget", WIDGET_URI, {}, async () => ({ contents: [ { uri: WIDGET_URI, mimeType: RESOURCE_MIME_TYPE, text: WIDGET_HTML, _meta: { ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: [], }, }, "openai/widgetDescription": "__APP_TITLE__ starter widget rendered by the MCP server.", }, }, ], }) ); registerAppTool( server, "__TOOL_NAME__", { title: "__APP_TITLE__", description: "Use this when the user wants to render the __APP_TITLE__ starter widget or inspect a minimal Apps SDK tool result.", inputSchema: { message: z .string() .optional() .describe("Optional message to show inside the widget."), }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true, }, _meta: { ui: { resourceUri: WIDGET_URI }, "openai/toolInvocation/invoking": "Loading __APP_TITLE__", "openai/toolInvocation/invoked": "__APP_TITLE__ ready", }, }, async ({ message }) => { const resolvedMessage = message?.trim() || "This starter uses the MCP Apps bridge first, keeps follow-up messaging on ui/message, and limits window.openai to optional host signals."; return { content: [ { type: "text" as const, text: "Rendered the __APP_TITLE__ starter widget.", }, ], structuredContent: { headline: "__APP_TITLE__", message: resolvedMessage, source: "__TOOL_NAME__", themeHint: "Read window.openai.theme in the widget if you need ChatGPT theme information.", }, _meta: { "openai/outputTemplate": WIDGET_URI, }, }; } ); return server; } const port = Number(process.env.PORT ?? "__PORT__"); const MCP_PATH = "/mcp"; createServer(async (req, res) => { if (!req.url) { res.writeHead(400).end("Missing URL"); return; } const url = new URL(req.url, "http://" + (req.headers.host ?? "localhost")); const isMcpRoute = url.pathname === MCP_PATH || url.pathname.startsWith(MCP_PATH + "/"); if (req.method === "OPTIONS" && isMcpRoute) { res.writeHead(204, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, GET, DELETE, OPTIONS", "Access-Control-Allow-Headers": "content-type, mcp-session-id", "Access-Control-Expose-Headers": "Mcp-Session-Id", }); res.end(); return; } if (req.method === "GET" && url.pathname === "/") { res.writeHead(200, { "content-type": "text/plain" }).end("__APP_TITLE__ MCP server"); return; } const transportMethods = new Set(["GET", "POST", "DELETE"]); if (isMcpRoute && req.method && transportMethods.has(req.method)) { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id"); const server = createAppServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true, }); res.on("close", () => { transport.close(); server.close(); }); try { await server.connect(transport); await transport.handleRequest(req, res); } catch (error) { console.error("Failed to handle MCP request:", error); if (!res.headersSent) { res.writeHead(500).end("Internal server error"); } } return; } res.writeHead(404).end("Not Found"); }).listen(port, () => { console.log("__APP_TITLE__ MCP server listening on http://localhost:" + port + MCP_PATH); }); `; function buildWidgetHtml(appSlug, appTitle, toolName) { return fillTemplate(WIDGET_TEMPLATE, { "__APP_SLUG__": appSlug, "__APP_TITLE__": appTitle, "__TOOL_NAME__": toolName, }); } function buildServerTs(appSlug, appTitle, toolName, widgetUri, port) { return fillTemplate(SERVER_TEMPLATE, { "__APP_SLUG__": appSlug, "__APP_TITLE__": appTitle, "__TOOL_NAME__": toolName, "__WIDGET_URI__": widgetUri, "__PORT__": String(port), }); } function usage() { return [ "Generate a minimal Node + @modelcontextprotocol/ext-apps starter with a vanilla widget that uses the MCP Apps bridge by default.", "Prefer upstream examples first; use this scaffold as the fallback.", "", "Usage:", " ./scripts/scaffold_node_ext_apps.mjs [--app-name ] [--tool-name ] [--port ] [--force]", "", "If the executable bit is unavailable, run:", " node scripts/scaffold_node_ext_apps.mjs [--app-name ] [--tool-name ] [--port ] [--force]", ].join("\\n"); } function parseArgs(argv) { const args = { outputDir: null, appName: "example-chatgpt-app", toolName: null, port: 8787, force: false, }; const tokens = [...argv]; while (tokens.length > 0) { const token = tokens.shift(); if (!args.outputDir && !token.startsWith("--")) { args.outputDir = token; continue; } if (token === "--app-name") { args.appName = tokens.shift() ?? ""; continue; } if (token === "--tool-name") { args.toolName = tokens.shift() ?? ""; continue; } if (token === "--port") { const value = Number(tokens.shift()); if (!Number.isInteger(value) || value <= 0) { throw new Error("Expected a positive integer after --port"); } args.port = value; continue; } if (token === "--force") { args.force = true; continue; } if (token === "--help" || token === "-h") { console.log(usage()); process.exit(0); } throw new Error(`Unknown argument: ${token}`); } if (!args.outputDir) { throw new Error(`Missing required output directory.\\n\\n${usage()}`); } return args; } function main() { const args = parseArgs(process.argv.slice(2)); const appSlug = toSlug(args.appName); const toolName = toToolName(args.toolName || appSlug); const appTitle = toTitle(appSlug); const widgetUri = "ui://widget/main-v1.html"; const outputDir = path.resolve(args.outputDir); ensureTargetDir(outputDir, args.force); const files = new Map([ [path.join(outputDir, "package.json"), buildPackageJson(appSlug)], [path.join(outputDir, "tsconfig.json"), buildTsconfig()], [path.join(outputDir, "public", "widget.html"), buildWidgetHtml(appSlug, appTitle, toolName)], [path.join(outputDir, "src", "server.ts"), buildServerTs(appSlug, appTitle, toolName, widgetUri, args.port)], ]); for (const [filePath, content] of files) { writeFile(filePath, content); } console.log("Generated starter scaffold:"); for (const filePath of files.keys()) { console.log(" -", path.relative(outputDir, filePath)); } } try { main(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } ================================================ FILE: skills/.curated/cloudflare-deploy/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don\'t include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/cloudflare-deploy/SKILL.md ================================================ --- name: cloudflare-deploy description: Deploy applications and infrastructure to Cloudflare using Workers, Pages, and related platform services. Use when the user asks to deploy, host, publish, or set up a project on Cloudflare. --- # Cloudflare Deploy Consolidated skill for building on the Cloudflare platform. Use decision trees below to find the right product, then load detailed references. ## Prerequisites - When sandboxing blocks the deployment network calls, rerun with `sandbox_permissions=require_escalated`. - The deployment might take a few minutes. Use appropriate timeout values. ## Authentication (Required Before Deploy) Verify auth before `wrangler deploy`, `wrangler pages deploy`, or `npm run deploy`: ```bash npx wrangler whoami # Shows account if authenticated ``` Not authenticated? → `references/wrangler/auth.md` - Interactive/local: `wrangler login` (one-time OAuth) - CI/CD: Set `CLOUDFLARE_API_TOKEN` env var ## Quick Decision Trees ### "I need to run code" ``` Need to run code? ├─ Serverless functions at the edge → workers/ ├─ Full-stack web app with Git deploys → pages/ ├─ Stateful coordination/real-time → durable-objects/ ├─ Long-running multi-step jobs → workflows/ ├─ Run containers → containers/ ├─ Multi-tenant (customers deploy code) → workers-for-platforms/ ├─ Scheduled tasks (cron) → cron-triggers/ ├─ Lightweight edge logic (modify HTTP) → snippets/ ├─ Process Worker execution events (logs/observability) → tail-workers/ └─ Optimize latency to backend infrastructure → smart-placement/ ``` ### "I need to store data" ``` Need storage? ├─ Key-value (config, sessions, cache) → kv/ ├─ Relational SQL → d1/ (SQLite) or hyperdrive/ (existing Postgres/MySQL) ├─ Object/file storage (S3-compatible) → r2/ ├─ Message queue (async processing) → queues/ ├─ Vector embeddings (AI/semantic search) → vectorize/ ├─ Strongly-consistent per-entity state → durable-objects/ (DO storage) ├─ Secrets management → secrets-store/ ├─ Streaming ETL to R2 → pipelines/ └─ Persistent cache (long-term retention) → cache-reserve/ ``` ### "I need AI/ML" ``` Need AI? ├─ Run inference (LLMs, embeddings, images) → workers-ai/ ├─ Vector database for RAG/search → vectorize/ ├─ Build stateful AI agents → agents-sdk/ ├─ Gateway for any AI provider (caching, routing) → ai-gateway/ └─ AI-powered search widget → ai-search/ ``` ### "I need networking/connectivity" ``` Need networking? ├─ Expose local service to internet → tunnel/ ├─ TCP/UDP proxy (non-HTTP) → spectrum/ ├─ WebRTC TURN server → turn/ ├─ Private network connectivity → network-interconnect/ ├─ Optimize routing → argo-smart-routing/ ├─ Optimize latency to backend (not user) → smart-placement/ └─ Real-time video/audio → realtimekit/ or realtime-sfu/ ``` ### "I need security" ``` Need security? ├─ Web Application Firewall → waf/ ├─ DDoS protection → ddos/ ├─ Bot detection/management → bot-management/ ├─ API protection → api-shield/ ├─ CAPTCHA alternative → turnstile/ └─ Credential leak detection → waf/ (managed ruleset) ``` ### "I need media/content" ``` Need media? ├─ Image optimization/transformation → images/ ├─ Video streaming/encoding → stream/ ├─ Browser automation/screenshots → browser-rendering/ └─ Third-party script management → zaraz/ ``` ### "I need infrastructure-as-code" ``` Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API) ``` ## 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/` | ### 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/` | ### 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/` | ### 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/` | ### Security | Product | Reference | |---------|-----------| | WAF | `references/waf/` | | DDoS Protection | `references/ddos/` | | Bot Management | `references/bot-management/` | | API Shield | `references/api-shield/` | | Turnstile | `references/turnstile/` | ### Media & Content | Product | Reference | |---------|-----------| | Images | `references/images/` | | Stream | `references/stream/` | | Browser Rendering | `references/browser-rendering/` | | Zaraz | `references/zaraz/` | ### Real-Time Communication | Product | Reference | |---------|-----------| | RealtimeKit | `references/realtimekit/` | | Realtime SFU | `references/realtime-sfu/` | ### 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/` | ### Infrastructure as Code | Product | Reference | |---------|-----------| | Pulumi | `references/pulumi/` | | Terraform | `references/terraform/` | | API | `references/api/` | ### 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/` | ## Troubleshooting ### Escalated Network Access If deployment fails due to network issues (timeouts, DNS errors, connection resets), rerun the deploy with escalated permissions (use `sandbox_permissions=require_escalated`). The deploy requires escalated network access when sandbox networking blocks outbound requests. Example guidance to the user: ``` The deploy needs escalated network access to deploy to Cloudflare. I can rerun the command with escalated permissions—want me to proceed? ``` ================================================ FILE: skills/.curated/cloudflare-deploy/agents/openai.yaml ================================================ interface: display_name: "Cloudflare Deploy" short_description: "Deploy Workers, Pages, and platform services on Cloudflare" icon_small: "./assets/cloudflare-small.svg" icon_large: "./assets/cloudflare.png" default_prompt: "Deploy this app to Cloudflare (Workers or Pages) and return URL, config, and required env vars." ================================================ FILE: skills/.curated/cloudflare-deploy/references/agents-sdk/README.md ================================================ # Cloudflare Agents SDK Cloudflare Agents SDK enables building AI-powered agents on Durable Objects with state, WebSockets, SQL, scheduling, and AI integration. ## Core Value Build stateful, globally distributed AI agents with persistent memory, real-time connections, scheduled tasks, and async workflows. ## When to Use - Persistent state + memory required - Real-time WebSocket connections - Long-running workflows (minutes/hours) - Chat interfaces with AI models - Scheduled/recurring tasks with state - DB queries with agent state ## What Type of Agent? | Use Case | Class | Key Features | |----------|-------|--------------| | AI chat interface | `AIChatAgent` | Auto-streaming, tools, message history, resumable | | MCP tool provider | `Agent` + MCP | Expose tools to AI systems | | Custom logic/routing | `Agent` | Full control, WebSockets, email, SQL | | Real-time collaboration | `Agent` | WebSocket state, broadcasts | | Email processing | `Agent` | `onEmail()` handler | ## Quick Start **AI Chat Agent:** ```typescript import { AIChatAgent } from "agents"; import { openai } from "@ai-sdk/openai"; export class ChatAgent extends AIChatAgent { async onChatMessage(onFinish) { return this.streamText({ model: openai("gpt-4"), messages: this.messages, onFinish, }); } } ``` **Base Agent:** ```typescript import { Agent } from "agents"; export class MyAgent extends Agent { onStart() { this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`; } async onRequest(request: Request) { return Response.json({ state: this.state }); } } ``` ## Reading Order | Task | Files to Read | |------|---------------| | Quick start | README only | | Build chat agent | README → api.md (AIChatAgent) → patterns.md | | Setup project | README → configuration.md | | Add React frontend | README → api.md (Client Hooks) → patterns.md | | Build MCP server | api.md (MCP) → patterns.md | | Background tasks | api.md (Scheduling, Task Queue) → patterns.md | | Debug issues | gotchas.md | ## Package Entry Points | Import | Purpose | |--------|---------| | `agents` | Server-side Agent classes, lifecycle | | `agents/react` | `useAgent()` hook for WebSocket connections | | `agents/ai-react` | `useAgentChat()` hook for AI chat UIs | ## In This Reference - [configuration.md](./configuration.md) - SDK setup, wrangler config, routing - [api.md](./api.md) - Agent classes, lifecycle, client hooks - [patterns.md](./patterns.md) - Common workflows, best practices - [gotchas.md](./gotchas.md) - Common issues, limits ## See Also - durable-objects - Agent infrastructure - d1 - External database integration - workers-ai - AI model integration - vectorize - Vector search for RAG patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/agents-sdk/api.md ================================================ # API Reference ## Agent Classes ### AIChatAgent For AI chat with auto-streaming, message history, tools, resumable streaming. ```ts import { AIChatAgent } from "agents"; import { openai } from "@ai-sdk/openai"; export class ChatAgent extends AIChatAgent { async onChatMessage(onFinish) { return this.streamText({ model: openai("gpt-4"), messages: this.messages, // Auto-managed message history tools: { getWeather: { description: "Get weather", parameters: z.object({ city: z.string() }), execute: async ({ city }) => `Sunny, 72°F in ${city}` } }, onFinish, // Persist response to this.messages }); } } ``` ### Agent (Base Class) Full control for custom logic, WebSockets, email, and SQL. ```ts import { Agent } from "agents"; export class MyAgent extends Agent { // Lifecycle methods below } ``` **Type params:** `Agent` - Env bindings, agent state, connection state ## Lifecycle Hooks ```ts onStart() { // Init/restart this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)`; } async onRequest(req: Request) { // HTTP const {pathname} = new URL(req.url); if (pathname === "/users") return Response.json(this.sql<{id,name}>`SELECT * FROM users`); return new Response("Not found", {status: 404}); } async onConnect(conn: Connection, ctx: ConnectionContext) { // WebSocket conn.accept(); conn.setState({userId: ctx.request.headers.get("X-User-ID")}); conn.send(JSON.stringify({type: "connected", state: this.state})); } async onMessage(conn: Connection, msg: WSMessage) { // WS messages const m = JSON.parse(msg as string); this.setState({messages: [...this.state.messages, m]}); this.connections.forEach(c => c.send(JSON.stringify(m))); } async onEmail(email: AgentEmail) { // Email routing this.sql`INSERT INTO emails (from_addr,subject,body) VALUES (${email.from},${email.headers.get("subject")},${await email.text()})`; } ``` ## State, SQL, Scheduling ```ts // State this.setState({count: 42}); // Auto-syncs this.setState({...this.state, count: this.state.count + 1}); // SQL (parameterized queries prevent injection) this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)`; this.sql`INSERT INTO users (id,name) VALUES (${userId},${name})`; const users = this.sql<{id,name}>`SELECT * FROM users WHERE id = ${userId}`; // Scheduling await this.schedule(new Date("2026-12-25"), "sendGreeting", {msg:"Hi"}); // Date await this.schedule(60, "checkStatus", {}); // Delay (sec) await this.schedule("0 0 * * *", "dailyCleanup", {}); // Cron await this.cancelSchedule(scheduleId); ``` ## RPC Methods (@callable) ```ts import { Agent, callable } from "agents"; export class MyAgent extends Agent { @callable() async processTask(input: {text: string}): Promise<{result: string}> { return { result: await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt: input.text}) }; } } // Client: const result = await agent.processTask({ text: "Hello" }); // Must return JSON-serializable values ``` ## Connections & AI ```ts // Connections (type: Agent) this.connections.forEach(c => c.send(JSON.stringify(msg))); // Broadcast conn.setState({userId:"123"}); conn.close(1000, "Goodbye"); // Workers AI const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt}); // Manual streaming (prefer AIChatAgent) const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true}); for await (const chunk of stream) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content})); ``` **Type-safe state:** `Agent` - third param types `conn.state` ## MCP Integration Model Context Protocol for exposing tools: ```ts // Register & use MCP server await this.mcp.registerServer("github", { url: env.MCP_SERVER_URL, auth: { type: "oauth", clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET } }); const tools = await this.mcp.getAITools(["github"]); return this.streamText({ model: openai("gpt-4"), messages: this.messages, tools, onFinish }); ``` ## Task Queue ```ts await this.queue("processVideo", { videoId: "abc123" }); // Add task const tasks = await this.dequeue(10); // Process up to 10 ``` ## Context & Cleanup ```ts const agent = getCurrentAgent(); // Get current instance async destroy() { /* cleanup before agent destroyed */ } ``` ## AI Integration ```ts // Workers AI const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt}); // Manual streaming (prefer AIChatAgent for auto-streaming) const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true}); for await (const chunk of stream) { if (chunk.choices[0]?.delta?.content) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content})); } ``` ## Client Hooks (React) ```ts // useAgent() - WebSocket connection + RPC import { useAgent } from "agents/react"; const agent = useAgent({ agent: "MyAgent", name: "user-123" }); // name for idFromName const result = await agent.processTask({ text: "Hello" }); // Call @callable methods // agent.readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED // useAgentChat() - AI chat UI import { useAgentChat } from "agents/ai-react"; const agent = useAgent({ agent: "ChatAgent" }); const { messages, input, handleInputChange, handleSubmit, isLoading, stop, clearHistory } = useAgentChat({ agent, maxSteps: 5, // Max tool iterations resume: true, // Auto-resume on disconnect onToolCall: async (toolCall) => { // Client tools (human-in-the-loop) if (toolCall.toolName === "confirm") return { ok: window.confirm("Proceed?") }; } }); // status: "ready" | "submitted" | "streaming" | "error" ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/agents-sdk/configuration.md ================================================ # Configuration ## Wrangler Setup ```jsonc { "name": "my-agents-app", "durable_objects": { "bindings": [ {"name": "MyAgent", "class_name": "MyAgent"} ] }, "migrations": [ {"tag": "v1", "new_sqlite_classes": ["MyAgent"]} ], "ai": { "binding": "AI" } } ``` ## Environment Bindings **Type-safe pattern:** ```typescript interface Env { AI?: Ai; // Workers AI MyAgent?: DurableObjectNamespace; ChatAgent?: DurableObjectNamespace; DB?: D1Database; // D1 database KV?: KVNamespace; // KV storage R2?: R2Bucket; // R2 bucket OPENAI_API_KEY?: string; // Secrets GITHUB_CLIENT_ID?: string; // MCP OAuth credentials GITHUB_CLIENT_SECRET?: string; QUEUE?: Queue; // Queues } ``` **Best practice:** Define all DO bindings in Env interface for type safety. ## Deployment ```bash # Local dev npx wrangler dev # Deploy production npx wrangler deploy # Set secrets npx wrangler secret put OPENAI_API_KEY ``` ## Agent Routing **Recommended: Use route helpers** ```typescript import { routeAgent } from "agents"; export default { fetch(request: Request, env: Env) { return routeAgent(request, env); } } ``` Helper routes requests to agents automatically based on URL patterns. **Manual routing (advanced):** ```typescript export default { async fetch(request: Request, env: Env) { const url = new URL(request.url); // Named ID (deterministic) const id = env.MyAgent.idFromName("user-123"); // Random ID (from URL param) // const id = env.MyAgent.idFromString(url.searchParams.get("id")); const stub = env.MyAgent.get(id); return stub.fetch(request); } } ``` **Multi-agent setup:** ```typescript import { routeAgent } from "agents"; export default { fetch(request: Request, env: Env) { const url = new URL(request.url); // Route by path if (url.pathname.startsWith("/chat")) { return routeAgent(request, env, "ChatAgent"); } if (url.pathname.startsWith("/task")) { return routeAgent(request, env, "TaskAgent"); } return new Response("Not found", { status: 404 }); } } ``` ## Email Routing **Code setup:** ```typescript import { routeAgentEmail } from "agents"; export default { fetch: (req: Request, env: Env) => routeAgent(req, env), email: (message: ForwardableEmailMessage, env: Env) => { return routeAgentEmail(message, env); } } ``` **Dashboard setup:** Configure email routing in Cloudflare dashboard: ``` Destination: Workers with Durable Objects Worker: my-agents-app ``` Then handle in agent: ```typescript export class EmailAgent extends Agent { async onEmail(email: AgentEmail) { const text = await email.text(); // Process email } } ``` ## AI Gateway (Optional) ```typescript // Enable caching/routing through AI Gateway const response = await this.env.AI.run( "@cf/meta/llama-3.1-8b-instruct", { prompt }, { gateway: { id: "my-gateway-id", skipCache: false, cacheTtl: 3600 } } ); ``` ## MCP Configuration (Optional) For exposing tools via Model Context Protocol: ```typescript // wrangler.jsonc - Add MCP OAuth secrets { "vars": { "MCP_SERVER_URL": "https://mcp.example.com" } } // Set secrets via CLI // npx wrangler secret put GITHUB_CLIENT_ID // npx wrangler secret put GITHUB_CLIENT_SECRET ``` Then register in agent code (see api.md MCP section). ================================================ FILE: skills/.curated/cloudflare-deploy/references/agents-sdk/gotchas.md ================================================ # Gotchas & Best Practices ## Common Errors ### "setState() not syncing" **Cause:** Mutating state directly or not calling `setState()` after modifications **Solution:** Always use `setState()` with immutable updates: ```ts // ❌ this.state.count++ // ✅ this.setState({...this.state, count: this.state.count + 1}) ``` ### "Message history grows unbounded (AIChatAgent)" **Cause:** `this.messages` in `AIChatAgent` accumulates all messages indefinitely **Solution:** Manually trim old messages periodically: ```ts export class ChatAgent extends AIChatAgent { async onChatMessage(onFinish) { // Keep only last 50 messages if (this.messages.length > 50) { this.messages = this.messages.slice(-50); } return this.streamText({ model: openai("gpt-4"), messages: this.messages, onFinish }); } } ``` ### "SQL injection vulnerability" **Cause:** Direct string interpolation in SQL queries **Solution:** Use parameterized queries: ```ts // ❌ this.sql`...WHERE id = '${userId}'` // ✅ this.sql`...WHERE id = ${userId}` ``` ### "WebSocket connection timeout" **Cause:** Not calling `conn.accept()` in `onConnect` **Solution:** Always accept connections: ```ts async onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); conn.setState({userId: "123"}); } ``` ### "Schedule limit exceeded" **Cause:** More than 1000 scheduled tasks per agent **Solution:** Clean up old schedules and limit creation rate: ```ts async checkSchedules() { if ((await this.getSchedules()).length > 800) console.warn("Near limit!"); } ``` ### "AI Gateway unavailable" **Cause:** AI service timeout or quota exceeded **Solution:** Add error handling and fallbacks: ```ts try { return await this.env.AI.run(model, {prompt}); } catch (e) { console.error("AI error:", e); return {error: "Unavailable"}; } ``` ### "@callable method returns undefined" **Cause:** Method doesn't return JSON-serializable value, or has non-serializable types **Solution:** Ensure return values are plain objects/arrays/primitives: ```ts // ❌ Returns class instance @callable() async getData() { return new Date(); } // ✅ Returns serializable object @callable() async getData() { return { timestamp: Date.now() }; } ``` ### "Resumable stream not resuming" **Cause:** Stream ID must be deterministic for resumption to work **Solution:** Use AIChatAgent (automatic) or ensure consistent stream IDs: ```ts // AIChatAgent handles this automatically export class ChatAgent extends AIChatAgent { // Resumption works out of the box } ``` ### "MCP connection loss on hibernation" **Cause:** MCP server connections don't survive hibernation **Solution:** Re-register servers in `onStart()` or check connection status: ```ts onStart() { // Re-register MCP servers after hibernation await this.mcp.registerServer("github", { url: env.MCP_URL, auth: {...} }); } ``` ### "Agent not found" **Cause:** Durable Object binding missing or incorrect class name **Solution:** Verify DO binding in wrangler.jsonc and class name matches ## Rate Limits & Quotas | Resource/Limit | Value | Notes | |----------------|-------|-------| | CPU per request | 30s (std), 300s (max) | Set in wrangler.jsonc | | Memory per instance | 128MB | Shared with WebSockets | | Storage per agent | 10GB | SQLite storage | | Scheduled tasks | 1000 per agent | Monitor with `getSchedules()` | | WebSocket connections | Unlimited | Within memory limits | | SQL columns | 100 | Per table | | SQL row size | 2MB | Key + value | | WebSocket message | 32MiB | Max size | | DO requests/sec | ~1000 | Per unique DO instance; rate limit if needed | | AI Gateway (Workers AI) | Model-specific | Check dashboard for limits | | MCP requests | Depends on server | Implement retry/backoff | ## Best Practices ### State Management - Use immutable updates: `setState({...this.state, key: newValue})` - Trim unbounded arrays (messages, logs) periodically - Store large data in SQL, not state ### SQL Usage - Create tables in `onStart()`, not `onRequest()` - Use parameterized queries: `` sql`WHERE id = ${id}` `` (NOT `` sql`WHERE id = '${id}'` ``) - Index frequently queried columns ### Scheduling - Monitor schedule count: `await this.getSchedules()` - Cancel completed tasks to stay under 1000 limit - Use cron strings for recurring tasks ### WebSockets - Always call `conn.accept()` in `onConnect()` - Handle client disconnects gracefully - Broadcast to `this.connections` efficiently ### AI Integration - Use `AIChatAgent` for chat interfaces (auto-streaming, resumption) - Trim message history to avoid token limits - Handle AI errors with try/catch and fallbacks ### Production Deployment - **Rate limiting:** Implement request throttling for high-traffic agents (>1000 req/s) - **Monitoring:** Log critical errors, track schedule count, monitor storage usage - **Graceful degradation:** Handle AI service outages with fallbacks - **Message trimming:** Enforce max history length (e.g., 100 messages) in AIChatAgent - **MCP reliability:** Re-register servers on hibernation, implement retry logic ================================================ FILE: skills/.curated/cloudflare-deploy/references/agents-sdk/patterns.md ================================================ # Patterns & Use Cases ## AI Chat w/Tools **Server (AIChatAgent):** ```ts import { AIChatAgent } from "agents"; import { openai } from "@ai-sdk/openai"; import { tool } from "ai"; import { z } from "zod"; export class ChatAgent extends AIChatAgent { async onChatMessage(onFinish) { return this.streamText({ model: openai("gpt-4"), messages: this.messages, // Auto-managed tools: { getWeather: tool({ description: "Get current weather", parameters: z.object({ city: z.string() }), execute: async ({ city }) => `Weather in ${city}: Sunny, 72°F` }), searchDocs: tool({ description: "Search documentation", parameters: z.object({ query: z.string() }), execute: async ({ query }) => JSON.stringify( this.sql<{title, content}>`SELECT title, content FROM docs WHERE content LIKE ${'%' + query + '%'}` ) }) }, onFinish, }); } } ``` **Client (React):** ```tsx import { useAgent } from "agents/react"; import { useAgentChat } from "agents/ai-react"; function ChatUI() { const agent = useAgent({ agent: "ChatAgent" }); const { messages, input, handleInputChange, handleSubmit, isLoading } = useAgentChat({ agent }); return (
{messages.map(m =>
{m.role}: {m.content}
)}
); } ``` ## Human-in-the-Loop (Client Tools) Server defines tool, client executes: ```ts // Server export class ChatAgent extends AIChatAgent { async onChatMessage(onFinish) { return this.streamText({ model: openai("gpt-4"), messages: this.messages, tools: { confirmAction: tool({ description: "Ask user to confirm", parameters: z.object({ action: z.string() }), execute: "client", // Client-side execution }) }, onFinish, }); } } // Client const { messages } = useAgentChat({ agent, onToolCall: async (toolCall) => { if (toolCall.toolName === "confirmAction") { return { confirmed: window.confirm(`Confirm: ${toolCall.args.action}?`) }; } } }); ``` ## Task Queue & Scheduled Processing ```ts export class TaskAgent extends Agent { onStart() { this.schedule("*/5 * * * *", "processQueue", {}); // Every 5 min this.schedule("0 0 * * *", "dailyCleanup", {}); // Daily } async onRequest(req: Request) { await this.queue("processVideo", { videoId: (await req.json()).videoId }); return Response.json({ queued: true }); } async processQueue() { const tasks = await this.dequeue(10); for (const task of tasks) { if (task.name === "processVideo") await this.processVideo(task.data.videoId); } } async dailyCleanup() { this.sql`DELETE FROM logs WHERE created_at < ${Date.now() - 86400000}`; } } ``` ## Manual WebSocket Chat Custom protocols (non-AI): ```ts export class ChatAgent extends Agent { async onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); conn.setState({userId: ctx.request.headers.get("X-User-ID") || "anon"}); conn.send(JSON.stringify({type: "history", messages: this.state.messages})); } async onMessage(conn: Connection, msg: WSMessage) { const newMsg = {userId: conn.state.userId, text: JSON.parse(msg as string).text, timestamp: Date.now()}; this.setState({messages: [...this.state.messages, newMsg]}); this.connections.forEach(c => c.send(JSON.stringify(newMsg))); } } ``` ## Email Processing w/AI ```ts export class EmailAgent extends Agent { async onEmail(email: AgentEmail) { const [text, from, subject] = [await email.text(), email.from, email.headers.get("subject") || ""]; this.sql`INSERT INTO emails (from_addr, subject, body) VALUES (${from}, ${subject}, ${text})`; const { text: summary } = await generateText({ model: openai("gpt-4o-mini"), prompt: `Summarize: ${subject}\n\n${text}` }); this.connections.forEach(c => c.send(JSON.stringify({type: "new_email", from, summary}))); if (summary.includes("urgent")) await this.schedule(0, "sendAutoReply", { to: from }); } } ``` ## Real-time Collaboration ```ts export class GameAgent extends Agent { initialState = { players: [], gameStarted: false }; async onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); const playerId = ctx.request.headers.get("X-Player-ID") || crypto.randomUUID(); conn.setState({ playerId }); const newPlayer = { id: playerId, score: 0 }; this.setState({...this.state, players: [...this.state.players, newPlayer]}); this.connections.forEach(c => c.send(JSON.stringify({type: "player_joined", player: newPlayer}))); } async onMessage(conn: Connection, msg: WSMessage) { const m = JSON.parse(msg as string); if (m.type === "move") { this.setState({ ...this.state, players: this.state.players.map(p => p.id === conn.state.playerId ? {...p, score: p.score + m.points} : p) }); this.connections.forEach(c => c.send(JSON.stringify({type: "player_moved", playerId: conn.state.playerId}))); } if (m.type === "start" && this.state.players.length >= 2) { this.setState({...this.state, gameStarted: true}); this.connections.forEach(c => c.send(JSON.stringify({type: "game_started"}))); } } } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-gateway/README.md ================================================ # Cloudflare AI Gateway Expert guidance for implementing Cloudflare AI Gateway - a universal gateway for AI model providers with analytics, caching, rate limiting, and routing capabilities. ## When to Use This Reference - Setting up AI Gateway for any AI provider (OpenAI, Anthropic, Workers AI, etc.) - Implementing caching, rate limiting, or request retry/fallback - Configuring dynamic routing with A/B testing or model fallbacks - Managing provider API keys securely with BYOK - Adding security features (guardrails, DLP) - Setting up observability with logging and custom metadata - Debugging AI Gateway requests or optimizing configurations ## Quick Start **What's your setup?** - **Using Vercel AI SDK** → Pattern 1 (recommended) - see [sdk-integration.md](./sdk-integration.md) - **Using OpenAI SDK** → Pattern 2 - see [sdk-integration.md](./sdk-integration.md) - **Cloudflare Worker + Workers AI** → Pattern 3 - see [sdk-integration.md](./sdk-integration.md) - **Direct HTTP (any language)** → Pattern 4 - see [configuration.md](./configuration.md) - **Framework (LangChain, etc.)** → See [sdk-integration.md](./sdk-integration.md) ## Pattern 1: Vercel AI SDK (Recommended) Most modern pattern using official `ai-gateway-provider` package with automatic fallbacks. ```typescript import { createAiGateway } from 'ai-gateway-provider'; import { createOpenAI } from '@ai-sdk/openai'; import { generateText } from 'ai'; const gateway = createAiGateway({ accountId: process.env.CF_ACCOUNT_ID, gateway: process.env.CF_GATEWAY_ID, }); const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }); // Single model const { text } = await generateText({ model: gateway(openai('gpt-4o')), prompt: 'Hello' }); // Automatic fallback array const { text } = await generateText({ model: gateway([ openai('gpt-4o'), // Try first anthropic('claude-sonnet-4-5'), // Fallback ]), prompt: 'Hello' }); ``` **Install:** `npm install ai-gateway-provider ai @ai-sdk/openai @ai-sdk/anthropic` ## Pattern 2: OpenAI SDK Drop-in replacement for OpenAI API with multi-provider support. ```typescript import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/compat`, defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` // For authenticated gateways } }); // Switch providers by changing model format: {provider}/{model} const response = await client.chat.completions.create({ model: 'openai/gpt-4o', // or 'anthropic/claude-sonnet-4-5' messages: [{ role: 'user', content: 'Hello!' }] }); ``` ## Pattern 3: Workers AI Binding For Cloudflare Workers using Workers AI. ```typescript export default { async fetch(request, env, ctx) { const response = await env.AI.run( '@cf/meta/llama-3-8b-instruct', { messages: [{ role: 'user', content: 'Hello!' }] }, { gateway: { id: 'my-gateway', metadata: { userId: '123', team: 'engineering' } } } ); return Response.json(response); } }; ``` ## Headers Quick Reference | Header | Purpose | Example | Notes | |--------|---------|---------|-------| | `cf-aig-authorization` | Gateway auth | `Bearer {token}` | Required for authenticated gateways | | `cf-aig-metadata` | Tracking | `{"userId":"x"}` | Max 5 entries, flat structure | | `cf-aig-cache-ttl` | Cache duration | `3600` | Seconds, min 60, max 2592000 (30 days) | | `cf-aig-skip-cache` | Bypass cache | `true` | - | | `cf-aig-cache-key` | Custom cache key | `my-key` | Must be unique per response | | `cf-aig-collect-log` | Skip logging | `false` | Default: true | | `cf-aig-cache-status` | Cache hit/miss | Response only | `HIT` or `MISS` | ## In This Reference | File | Purpose | |------|---------| | [sdk-integration.md](./sdk-integration.md) | Vercel AI SDK, OpenAI SDK, Workers binding patterns | | [configuration.md](./configuration.md) | Dashboard setup, wrangler, API tokens | | [features.md](./features.md) | Caching, rate limits, guardrails, DLP, BYOK, unified billing | | [dynamic-routing.md](./dynamic-routing.md) | Fallbacks, A/B testing, conditional routing | | [troubleshooting.md](./troubleshooting.md) | Debugging, errors, observability, gotchas | ## Reading Order | Task | Files | |------|-------| | First-time setup | README + [configuration.md](./configuration.md) | | SDK integration | README + [sdk-integration.md](./sdk-integration.md) | | Enable caching | README + [features.md](./features.md) | | Setup fallbacks | README + [dynamic-routing.md](./dynamic-routing.md) | | Debug errors | README + [troubleshooting.md](./troubleshooting.md) | ## Architecture AI Gateway acts as a proxy between your application and AI providers: ``` Your App → AI Gateway → AI Provider (OpenAI, Anthropic, etc.) ↓ Analytics, Caching, Rate Limiting, Logging ``` **Key URL patterns:** - Unified API (OpenAI-compatible): `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/chat/completions` - Provider-specific: `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/{provider}/{endpoint}` - Dynamic routes: Use route name instead of model: `dynamic/{route-name}` ## Gateway Types 1. **Unauthenticated Gateway**: Open access (not recommended for production) 2. **Authenticated Gateway**: Requires `cf-aig-authorization` header with Cloudflare API token (recommended) ## Provider Authentication Options 1. **Unified Billing**: Use AI Gateway billing to pay for inference (keyless mode - no provider API key needed) 2. **BYOK (Store Keys)**: Store provider API keys in Cloudflare dashboard 3. **Request Headers**: Include provider API key in each request ## Related Skills - [Workers AI](../workers-ai/README.md) - For `env.AI.run()` details - [Agents SDK](../agents-sdk/README.md) - For stateful AI patterns - [Vectorize](../vectorize/README.md) - For RAG patterns with embeddings ## Resources - [Official Docs](https://developers.cloudflare.com/ai-gateway/) - [API Reference](https://developers.cloudflare.com/api/resources/ai_gateway/) - [Provider Guides](https://developers.cloudflare.com/ai-gateway/usage/providers/) - [Discord Community](https://discord.cloudflare.com) ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-gateway/configuration.md ================================================ # Configuration & Setup ## Creating a Gateway ### Dashboard AI > AI Gateway > Create Gateway > Configure (auth, caching, rate limiting, logging) ### API ```bash curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \ -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \ -d '{"id":"my-gateway","cache_ttl":3600,"rate_limiting_interval":60,"rate_limiting_limit":100,"collect_logs":true}' ``` **Naming:** lowercase alphanumeric + hyphens (e.g., `prod-api`, `dev-chat`) ## Wrangler Integration ```toml [ai] binding = "AI" [[ai.gateway]] id = "my-gateway" ``` ```bash wrangler secret put CF_API_TOKEN wrangler secret put OPENAI_API_KEY # If not using BYOK ``` ## Authentication ### Gateway Auth (protects gateway access) ```typescript const client = new OpenAI({ baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`, defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` } }); ``` ### Provider Auth Options **1. Unified Billing (keyless)** - pay through Cloudflare, no provider key: ```typescript const client = new OpenAI({ baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`, defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` } }); ``` Supports: OpenAI, Anthropic, Google AI Studio **2. BYOK** - store keys in dashboard (Provider Keys > Add), no key in code **3. Request Headers** - pass provider key per request: ```typescript const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`, defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` } }); ``` ## API Token Permissions - **Gateway management:** AI Gateway - Read + Edit - **Gateway access:** AI Gateway - Read (minimum) ## Gateway Management API ```bash # List curl https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \ -H "Authorization: Bearer $CF_API_TOKEN" # Get curl .../gateways/{gateway_id} # Update curl -X PUT .../gateways/{gateway_id} \ -d '{"cache_ttl":7200,"rate_limiting_limit":200}' # Delete curl -X DELETE .../gateways/{gateway_id} ``` ## Getting IDs - **Account ID:** Dashboard > Overview > Copy - **Gateway ID:** AI Gateway > Gateway name column ## Python Example ```python from openai import OpenAI import os client = OpenAI( api_key=os.environ.get("OPENAI_API_KEY"), base_url=f"https://gateway.ai.cloudflare.com/v1/{os.environ['CF_ACCOUNT_ID']}/{os.environ['GATEWAY_ID']}/openai", default_headers={"cf-aig-authorization": f"Bearer {os.environ['CF_API_TOKEN']}"} ) ``` ## Best Practices 1. **Always authenticate gateways in production** 2. **Use BYOK or unified billing** - secrets out of code 3. **Environment-specific gateways** - separate dev/staging/prod 4. **Set rate limits** - prevent runaway costs 5. **Enable logging** - track usage, debug issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-gateway/dynamic-routing.md ================================================ # Dynamic Routing Configure complex routing in dashboard without code changes. Use route names instead of model names. ## Usage ```typescript const response = await client.chat.completions.create({ model: 'dynamic/smart-chat', // Route name from dashboard messages: [{ role: 'user', content: 'Hello!' }] }); ``` ## Node Types | Node | Purpose | Use Case | |------|---------|----------| | **Conditional** | Branch on metadata | Paid vs free users, geo routing | | **Percentage** | A/B split traffic | Model testing, gradual rollouts | | **Rate Limit** | Enforce quotas | Per-user/team limits | | **Budget Limit** | Cost quotas | Per-user spending caps | | **Model** | Call provider | Final destination | ## Metadata Pass via header (max 5 entries, flat only): ```typescript headers: { 'cf-aig-metadata': JSON.stringify({ userId: 'user-123', tier: 'pro', region: 'us-east' }) } ``` ## Common Patterns **Multi-model fallback:** ``` Start → GPT-4 → On error: Claude → On error: Llama ``` **Tiered access:** ``` Conditional: tier == 'enterprise' → GPT-4 (no limit) Conditional: tier == 'pro' → Rate Limit 1000/hr → GPT-4o Conditional: tier == 'free' → Rate Limit 10/hr → GPT-4o-mini ``` **Gradual rollout:** ``` Percentage: 10% → New model, 90% → Old model ``` **Cost-based fallback:** ``` Budget Limit: $100/day per teamId < 80%: GPT-4 >= 80%: GPT-4o-mini >= 100%: Error ``` ## Version Management - Save changes as new version - Test with `model: 'dynamic/route@v2'` - Roll back by deploying previous version ## Monitoring Dashboard → Gateway → Dynamic Routes: - Request count per path - Success/error rates - Latency/cost by path ## Limitations - Max 5 metadata entries - Values: string/number/boolean/null only - No nested objects - Route names: alphanumeric + hyphens ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-gateway/features.md ================================================ # Features & Capabilities ## Caching Dashboard: Settings → Cache Responses → Enable ```typescript // Custom TTL (1 hour) headers: { 'cf-aig-cache-ttl': '3600' } // Skip cache headers: { 'cf-aig-skip-cache': 'true' } // Custom cache key headers: { 'cf-aig-cache-key': 'greeting-en' } ``` **Limits:** TTL 60s - 30 days. **Does NOT work with streaming.** ## Rate Limiting Dashboard: Settings → Rate-limiting → Enable - **Fixed window:** Resets at intervals - **Sliding window:** Rolling window (more accurate) - Returns `429` when exceeded ## Guardrails Dashboard: Settings → Guardrails → Enable Filter prompts/responses for inappropriate content. Actions: Flag (log) or Block (reject). ## Data Loss Prevention (DLP) Dashboard: Settings → DLP → Enable Detect PII (emails, SSNs, credit cards). Actions: Flag, Block, or Redact. ## Billing Modes | Mode | Description | Setup | |------|-------------|-------| | **Unified Billing** | Pay through Cloudflare, no provider keys | Use `cf-aig-authorization` header only | | **BYOK** | Store provider keys in dashboard | Add keys in Provider Keys section | | **Pass-through** | Send provider key with each request | Include provider's auth header | ## Zero Data Retention Dashboard: Settings → Privacy → Zero Data Retention No prompts/responses stored. Request counts and costs still tracked. ## Logging Dashboard: Settings → Logs → Enable (up to 10M logs) Each entry: prompt, response, provider, model, tokens, cost, duration, cache status, metadata. ```typescript // Skip logging for request headers: { 'cf-aig-collect-log': 'false' } ``` **Export:** Use Logpush to S3, GCS, Datadog, Splunk, etc. ## Custom Cost Tracking For models not in Cloudflare's pricing database: Dashboard: Gateway → Settings → Custom Costs Or via API: set `model`, `input_cost`, `output_cost`. ## Supported Providers (22+) | Provider | Unified API | Notes | |----------|-------------|-------| | OpenAI | `openai/gpt-4o` | Full support | | Anthropic | `anthropic/claude-sonnet-4-5` | Full support | | Google AI | `google-ai-studio/gemini-2.0-flash` | Full support | | Workers AI | `workersai/@cf/meta/llama-3` | Native | | Azure OpenAI | `azure-openai/*` | Deployment names | | AWS Bedrock | Provider endpoint only | `/bedrock/*` | | Groq | `groq/*` | Fast inference | | Mistral, Cohere, Perplexity, xAI, DeepSeek, Cerebras | Full support | - | ## Best Practices 1. Enable caching for deterministic prompts 2. Set rate limits to prevent abuse 3. Use guardrails for user-facing AI 4. Enable DLP for sensitive data 5. Use unified billing or BYOK for simpler key management 6. Enable logging for debugging 7. Use zero data retention when privacy required ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-gateway/sdk-integration.md ================================================ # AI Gateway SDK Integration ## Vercel AI SDK (Recommended) ```typescript import { createAiGateway } from 'ai-gateway-provider'; import { createOpenAI } from '@ai-sdk/openai'; import { generateText } from 'ai'; const gateway = createAiGateway({ accountId: process.env.CF_ACCOUNT_ID, gateway: process.env.CF_GATEWAY_ID, apiKey: process.env.CF_API_TOKEN // Optional for auth gateways }); const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }); // Single model const { text } = await generateText({ model: gateway(openai('gpt-4o')), prompt: 'Hello' }); // Automatic fallback array const { text } = await generateText({ model: gateway([ openai('gpt-4o'), anthropic('claude-sonnet-4-5'), openai('gpt-4o-mini') ]), prompt: 'Complex task' }); ``` ### Options ```typescript model: gateway(openai('gpt-4o'), { cacheKey: 'my-key', cacheTtl: 3600, metadata: { userId: 'u123', team: 'eng' }, // Max 5 entries retries: { maxAttempts: 3, backoff: 'exponential' } }) ``` ## OpenAI SDK ```typescript const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`, defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` } }); // Unified API - switch providers via model name model: 'openai/gpt-4o' // or 'anthropic/claude-sonnet-4-5' ``` ## Anthropic SDK ```typescript const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`, defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` } }); ``` ## Workers AI Binding ```toml # wrangler.toml [ai] binding = "AI" [[ai.gateway]] id = "my-gateway" ``` ```typescript await env.AI.run('@cf/meta/llama-3-8b-instruct', { messages: [...] }, { gateway: { id: 'my-gateway', metadata: { userId: '123' } } } ); ``` ## LangChain / LlamaIndex ```typescript // Use OpenAI SDK pattern with custom baseURL new ChatOpenAI({ configuration: { baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai` } }); ``` ## HTTP / cURL ```bash curl https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/chat/completions \ -H "Authorization: Bearer $OPENAI_KEY" \ -H "cf-aig-authorization: Bearer $CF_TOKEN" \ -H "cf-aig-metadata: {\"userId\":\"123\"}" \ -d '{"model":"gpt-4o","messages":[...]}' ``` ## Headers Reference | Header | Purpose | |--------|---------| | `cf-aig-authorization` | Gateway auth token | | `cf-aig-metadata` | JSON object (max 5 keys) | | `cf-aig-cache-ttl` | Cache TTL in seconds | | `cf-aig-skip-cache` | `true` to bypass cache | ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-gateway/troubleshooting.md ================================================ # AI Gateway Troubleshooting ## Common Errors | Error | Cause | Fix | |-------|-------|-----| | 401 | Missing `cf-aig-authorization` header | Add header with CF API token | | 403 | Invalid provider key / BYOK expired | Check provider key in dashboard | | 429 | Rate limit exceeded | Increase limit or implement backoff | ### 401 Fix ```typescript const client = new OpenAI({ baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`, defaultHeaders: { 'cf-aig-authorization': `Bearer ${CF_API_TOKEN}` } }); ``` ### 429 Retry Pattern ```typescript async function requestWithRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (e) { if (e.status === 429 && i < maxRetries - 1) { await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); continue; } throw e; } } } ``` ## Gotchas | Issue | Reality | |-------|---------| | Metadata limits | Max 5 entries, flat only (no nesting) | | Cache key collision | Use unique keys per expected response | | BYOK + Unified Billing | Mutually exclusive | | Rate limit scope | Per-gateway, not per-user (use dynamic routing for per-user) | | Log delay | 30-60 seconds normal | | Streaming + caching | **Incompatible** | | Model name (unified API) | Prefix required: `openai/gpt-4o`, not `gpt-4o` | ## Cache Not Working **Causes:** - Different request params (temperature, etc.) - Streaming enabled - Caching disabled in settings **Check:** `response.headers.get('cf-aig-cache-status')` → HIT or MISS ## Logs Not Appearing 1. Check logging enabled: Dashboard → Gateway → Settings 2. Remove `cf-aig-collect-log: false` header 3. Wait 30-60 seconds 4. Check log limit (10M default) ## Debugging ```bash # Test connectivity curl -v https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/models \ -H "Authorization: Bearer $OPENAI_KEY" \ -H "cf-aig-authorization: Bearer $CF_TOKEN" ``` ```typescript // Check response headers console.log('Cache:', response.headers.get('cf-aig-cache-status')); console.log('Request ID:', response.headers.get('cf-ray')); ``` ## Analytics Dashboard → AI Gateway → Select gateway **Metrics:** Requests, tokens, latency (p50/p95/p99), cache hit rate, costs **Log filters:** `status: error`, `provider: openai`, `cost > 0.01`, `duration > 1000` **Export:** Logpush to S3/GCS/Datadog/Splunk ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-search/README.md ================================================ # Cloudflare AI Search Reference Expert guidance for implementing Cloudflare AI Search (formerly AutoRAG), Cloudflare's managed semantic search and RAG service. ## Overview **AI Search** is a managed RAG (Retrieval-Augmented Generation) pipeline that combines: - Automatic semantic indexing of your content - Vector similarity search - Built-in LLM generation **Key value propositions:** - **Zero vector management** - No manual embedding, indexing, or storage - **Auto-indexing** - Content automatically re-indexed every 6 hours - **Built-in generation** - Optional AI response generation from retrieved context - **Multi-source** - Index from R2 buckets or website crawls **Data source options:** - **R2 bucket** - Index files from Cloudflare R2 (supports MD, TXT, HTML, PDF, DOC, CSV, JSON) - **Website** - Crawl and index website content (requires Cloudflare-hosted domain) **Indexing lifecycle:** - Automatic 6-hour refresh cycle - Manual "Force Sync" available (30s rate limit) - Not designed for real-time updates ## Quick Start **1. Create AI Search instance in dashboard:** - Go to Cloudflare Dashboard → AI Search → Create - Choose data source (R2 or website) - Configure instance name and settings **2. Configure Worker:** ```jsonc // wrangler.jsonc { "ai": { "binding": "AI" } } ``` **3. Use in Worker:** ```typescript export default { async fetch(request, env) { const answer = await env.AI.autorag("my-search-instance").aiSearch({ query: "How do I configure caching?", model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast" }); return Response.json({ answer: answer.response }); } }; ``` ## When to Use AI Search ### AI Search vs Vectorize | Factor | AI Search | Vectorize | |--------|-----------|-----------| | **Management** | Fully managed | Manual embedding + indexing | | **Use when** | Want zero-ops RAG pipeline | Need custom embeddings/control | | **Indexing** | Automatic (6hr cycle) | Manual via API | | **Generation** | Built-in optional | Bring your own LLM | | **Data sources** | R2 or website | Manual insert | | **Best for** | Docs, support, enterprise search | Custom ML pipelines, real-time | ### AI Search vs Direct Workers AI | Factor | AI Search | Workers AI (direct) | |--------|-----------|---------------------| | **Context** | Automatic retrieval | Manual context building | | **Use when** | Need RAG (search + generate) | Simple generation tasks | | **Indexing** | Built-in | Not applicable | | **Best for** | Knowledge bases, docs | Simple chat, transformations | ### search() vs aiSearch() | Method | Returns | Use When | |--------|---------|----------| | `search()` | Search results only | Building custom UI, need raw chunks | | `aiSearch()` | AI response + results | Need ready-to-use answer (chatbot, Q&A) | ### Real-time Updates Consideration **AI Search is NOT ideal if:** - Need real-time content updates (<6 hours) - Content changes multiple times per hour - Strict freshness requirements **AI Search IS ideal if:** - Content relatively stable (docs, policies, knowledge bases) - 6-hour refresh acceptable - Prefer zero-ops over real-time ## Platform Limits | Limit | Value | |-------|-------| | Max instances per account | 10 | | Max files per instance | 100,000 | | Max file size | 4 MB | | Index frequency | Every 6 hours | | Force Sync rate limit | Once per 30 seconds | | Filter nesting depth | 2 levels | | Filters per compound | 10 | | Score threshold range | 0.0 - 1.0 | ## Reading Order Navigate these references based on your task: | Task | Read | Est. Time | |------|------|-----------| | **Understand AI Search** | README only | 5 min | | **Implement basic search** | README → api.md | 10 min | | **Configure data source** | README → configuration.md | 10 min | | **Production patterns** | patterns.md | 15 min | | **Debug issues** | gotchas.md | 10 min | | **Full implementation** | README → api.md → patterns.md | 30 min | ## In This Reference - **[api.md](api.md)** - API endpoints, methods, TypeScript interfaces - **[configuration.md](configuration.md)** - Setup, data sources, wrangler config - **[patterns.md](patterns.md)** - Common patterns, decision guidance, code examples - **[gotchas.md](gotchas.md)** - Troubleshooting, code-level gotchas, limits ## See Also - [Cloudflare AI Search Docs](https://developers.cloudflare.com/ai-search/) - [Workers AI Docs](https://developers.cloudflare.com/workers-ai/) - [Vectorize Docs](https://developers.cloudflare.com/vectorize/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-search/api.md ================================================ # AI Search API Reference ## Workers Binding ```typescript const answer = await env.AI.autorag("instance-name").aiSearch(options); const results = await env.AI.autorag("instance-name").search(options); const instances = await env.AI.autorag("_").listInstances(); ``` ## aiSearch() Options ```typescript interface AiSearchOptions { query: string; // User query model: string; // Workers AI model ID system_prompt?: string; // LLM instructions rewrite_query?: boolean; // Fix typos (default: false) max_num_results?: number; // Max chunks (default: 10) ranking_options?: { score_threshold?: number }; // 0.0-1.0 (default: 0.3) reranking?: { enabled: boolean; model: string }; stream?: boolean; // Stream response (default: false) filters?: Filter; // Metadata filters page?: string; // Pagination token } ``` ## Response ```typescript interface AiSearchResponse { search_query: string; // Query used (rewritten if enabled) response: string; // AI-generated answer data: SearchResult[]; // Retrieved chunks has_more: boolean; next_page?: string; } interface SearchResult { id: string; score: number; content: string; metadata: { filename: string; folder: string; timestamp: number }; } ``` ## Filters ```typescript // Comparison { column: "folder", operator: "gte", value: "docs/" } // Compound { operator: "and", filters: [ { column: "folder", operator: "gte", value: "docs/" }, { column: "timestamp", operator: "gte", value: 1704067200 } ]} ``` **Operators:** `eq`, `ne`, `gt`, `gte`, `lt`, `lte` **Built-in metadata:** `filename`, `folder`, `timestamp` (Unix seconds) ## Streaming ```typescript const stream = await env.AI.autorag("docs").aiSearch({ query, model, stream: true }); return new Response(stream, { headers: { "Content-Type": "text/event-stream" } }); ``` ## Error Types | Error | Cause | |-------|-------| | `AutoRAGNotFoundError` | Instance doesn't exist | | `AutoRAGUnauthorizedError` | Invalid/missing token | | `AutoRAGValidationError` | Invalid parameters | ## REST API ```bash curl https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/autorag/rags/{NAME}/ai-search \ -H "Authorization: Bearer {TOKEN}" \ -d '{"query": "...", "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"}' ``` Requires Service API token with "AI Search - Read" permission. ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-search/configuration.md ================================================ # AI Search Configuration ## Worker Setup ```jsonc // wrangler.jsonc { "ai": { "binding": "AI" } } ``` ```typescript interface Env { AI: Ai; } const answer = await env.AI.autorag("my-instance").aiSearch({ query: "How do I configure caching?", model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast" }); ``` ## Data Sources ### R2 Bucket Dashboard: AI Search → Create Instance → Select R2 bucket **Supported formats:** `.md`, `.txt`, `.html`, `.pdf`, `.doc`, `.docx`, `.csv`, `.json` **Auto-indexed metadata:** `filename`, `folder`, `timestamp` ### Website Crawler Requirements: - Domain on Cloudflare - `sitemap.xml` at root - Bot protection must allow `CloudflareAISearch` user agent ## Path Filtering (R2) ``` docs/**/*.md # All .md in docs/ recursively **/*.draft.md # Exclude (use in exclude patterns) ``` ## Indexing - **Automatic:** Every 6 hours - **Force Sync:** Dashboard button (30s rate limit between syncs) - **Pause:** Settings → Pause Indexing (existing index remains searchable) ## Service API Token Dashboard: AI Search → Instance → Use AI Search → API → Create Token Permissions: - **Read** - search operations - **Edit** - instance management Store securely: ```bash wrangler secret put AI_SEARCH_TOKEN ``` ## Multi-Environment ```toml # wrangler.toml [env.production.vars] AI_SEARCH_INSTANCE = "prod-docs" [env.staging.vars] AI_SEARCH_INSTANCE = "staging-docs" ``` ```typescript const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({ query }); ``` ## Monitoring ```typescript const instances = await env.AI.autorag("_").listInstances(); console.log(instances.find(i => i.name === "docs")); ``` Dashboard shows: files indexed, status, last index time, storage usage. ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-search/gotchas.md ================================================ # AI Search Gotchas ## Type Safety **Timestamp precision:** Use seconds (10-digit), not milliseconds. ```typescript const nowInSeconds = Math.floor(Date.now() / 1000); // Correct ``` **Folder prefix matching:** Use `gte` for "starts with" on paths. ```typescript filters: { column: "folder", operator: "gte", value: "docs/api/" } // Matches nested ``` ## Filter Limitations | Limit | Value | |-------|-------| | Max nesting depth | 2 levels | | Filters per compound | 10 | | `or` operator | Same column, `eq` only | **OR restriction example:** ```typescript // ✅ Valid: same column, eq only { operator: "or", filters: [ { column: "folder", operator: "eq", value: "docs/" }, { column: "folder", operator: "eq", value: "guides/" } ]} ``` ## Indexing Issues | Problem | Cause | Solution | |---------|-------|----------| | File not indexed | Unsupported format or >4MB | Check format (.md/.txt/.html/.pdf/.doc/.csv/.json) | | Index out of sync | 6-hour index cycle | Wait or use "Force Sync" (30s rate limit) | | Empty results | Index incomplete | Check dashboard for indexing status | ## Auth Errors | Error | Cause | Fix | |-------|-------|-----| | `AutoRAGUnauthorizedError` | Invalid/missing token | Create Service API token with AI Search permissions | | `AutoRAGNotFoundError` | Wrong instance name | Verify exact name from dashboard | ## Performance **Slow responses (>3s):** ```typescript // Add score threshold + limit results ranking_options: { score_threshold: 0.5 }, max_num_results: 10 ``` **Empty results debug:** 1. Remove filters, test basic query 2. Lower `score_threshold` to 0.1 3. Check index is populated ## Limits | Resource | Limit | |----------|-------| | Instances per account | 10 | | Files per instance | 100,000 | | Max file size | 4 MB | | Index frequency | 6 hours | ## Anti-Patterns **Use env vars for instance names:** ```typescript const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({...}); ``` **Handle specific error types:** ```typescript if (error instanceof AutoRAGNotFoundError) { /* 404 */ } if (error instanceof AutoRAGUnauthorizedError) { /* 401 */ } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/ai-search/patterns.md ================================================ # AI Search Patterns ## search() vs aiSearch() | Use | Method | Returns | |-----|--------|---------| | Custom UI, analytics | `search()` | Raw chunks only (~100-300ms) | | Chatbots, Q&A | `aiSearch()` | AI response + chunks (~500-2000ms) | ## rewrite_query | Setting | Use When | |---------|----------| | `true` | User input (typos, vague queries) | | `false` | LLM-generated queries (already optimized) | ## Multitenancy (Folder-Based) ```typescript const answer = await env.AI.autorag("saas-docs").aiSearch({ query: "refund policy", model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", filters: { column: "folder", operator: "gte", // "starts with" pattern value: `tenants/${tenantId}/` } }); ``` ## Streaming ```typescript const stream = await env.AI.autorag("docs").aiSearch({ query, model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", stream: true }); return new Response(stream, { headers: { "Content-Type": "text/event-stream" } }); ``` ## Score Threshold | Threshold | Use | |-----------|-----| | 0.3 (default) | Broad recall, exploratory | | 0.5 | Balanced, production default | | 0.7 | High precision, critical accuracy | ## System Prompt Template ```typescript const systemPrompt = `You are a documentation assistant. - Answer ONLY based on provided context - If context doesn't contain answer, say "I don't have information" - Include code examples from context`; ``` ## Compound Filters ```typescript // OR: Multiple folders filters: { operator: "or", filters: [ { column: "folder", operator: "gte", value: "docs/api/" }, { column: "folder", operator: "gte", value: "docs/auth/" } ] } // AND: Folder + date filters: { operator: "and", filters: [ { column: "folder", operator: "gte", value: "docs/" }, { column: "timestamp", operator: "gte", value: oneWeekAgoSeconds } ] } ``` ## Reranking Enable for high-stakes use cases (adds ~300ms latency): ```typescript reranking: { enabled: true, model: "@cf/baai/bge-reranker-base" } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/analytics-engine/README.md ================================================ # Cloudflare Workers Analytics Engine Reference Expert guidance for implementing unlimited-cardinality analytics at scale using Cloudflare Workers Analytics Engine. ## What is Analytics Engine? Time-series analytics database designed for high-cardinality data (millions of unique dimensions). Write data points from Workers, query via SQL API. Use for: - Custom user-facing analytics dashboards - Usage-based billing & metering - Per-customer/per-feature monitoring - High-frequency instrumentation without performance impact **Key Capability:** Track metrics with unlimited unique values (e.g., millions of user IDs, API keys) without performance degradation. ## Core Concepts | Concept | Description | Example | |---------|-------------|---------| | **Dataset** | Logical table for related metrics | `api_requests`, `user_events` | | **Data Point** | Single measurement with timestamp | One API request's metrics | | **Blobs** | String dimensions (max 20) | endpoint, method, status, user_id | | **Doubles** | Numeric values (max 20) | latency_ms, request_count, bytes | | **Indexes** | Filtered blobs for efficient queries | customer_id, api_key | ## Reading Order | Task | Start Here | Then Read | |------|------------|-----------| | **First-time setup** | [configuration.md](configuration.md) → [api.md](api.md) → [patterns.md](patterns.md) | | | **Writing data** | [api.md](api.md) → [gotchas.md](gotchas.md) (sampling) | | | **Querying data** | [api.md](api.md) (SQL API) → [patterns.md](patterns.md) (examples) | | | **Debugging** | [gotchas.md](gotchas.md) → [api.md](api.md) (limits) | | | **Optimization** | [patterns.md](patterns.md) (anti-patterns) → [gotchas.md](gotchas.md) | | ## When to Use Analytics Engine ``` Need to track metrics? → Yes ↓ Millions of unique dimension values? → Yes ↓ Need real-time queries? → Yes ↓ Use Analytics Engine ✓ Alternative scenarios: - Low cardinality (<10k unique values) → Workers Analytics (free tier) - Complex joins/relations → D1 Database - Logs/debugging → Tail Workers (logpush) - External tools → Send to external analytics (Datadog, etc.) ``` ## Quick Start 1. Add binding to `wrangler.jsonc`: ```jsonc { "analytics_engine_datasets": [ { "binding": "ANALYTICS", "dataset": "my_events" } ] } ``` 2. Write data points (fire-and-forget, no await): ```typescript env.ANALYTICS.writeDataPoint({ blobs: ["/api/users", "GET", "200"], doubles: [145.2, 1], // latency_ms, count indexes: [customerId] }); ``` 3. Query via SQL API (HTTP): ```sql SELECT blob1, SUM(double2) AS total_requests FROM my_events WHERE index1 = 'customer_123' AND timestamp >= NOW() - INTERVAL '7' DAY GROUP BY blob1 ORDER BY total_requests DESC ``` ## In This Reference - **[configuration.md](configuration.md)** - Setup, bindings, TypeScript types, limits - **[api.md](api.md)** - `writeDataPoint()`, SQL API, query syntax - **[patterns.md](patterns.md)** - Use cases, examples, anti-patterns - **[gotchas.md](gotchas.md)** - Sampling, index selection, troubleshooting ## See Also - [Cloudflare Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/analytics-engine/api.md ================================================ # Analytics Engine API Reference ## Writing Data ### `writeDataPoint()` Fire-and-forget (returns `void`, not Promise). Writes happen asynchronously. ```typescript interface AnalyticsEngineDataPoint { blobs?: string[]; // Up to 20 strings (dimensions), 16KB each doubles?: number[]; // Up to 20 numbers (metrics) indexes?: string[]; // 1 indexed string for high-cardinality filtering } env.ANALYTICS.writeDataPoint({ blobs: ["/api/users", "GET", "200"], doubles: [145.2, 1], // latency_ms, count indexes: ["customer_abc123"] }); ``` **Behaviors:** No await needed, no error thrown (check tail logs), auto-sampled at high volumes, auto-timestamped. **Blob vs Index:** Blob for GROUP BY (<100k unique), Index for filter-only (millions unique). ### Full Example ```typescript export default { async fetch(request: Request, env: Env): Promise { const start = Date.now(); const url = new URL(request.url); try { const response = await handleRequest(request); env.ANALYTICS.writeDataPoint({ blobs: [url.pathname, request.method, response.status.toString()], doubles: [Date.now() - start, 1], indexes: [request.headers.get("x-api-key") || "anonymous"] }); return response; } catch (error) { env.ANALYTICS.writeDataPoint({ blobs: [url.pathname, request.method, "500"], doubles: [Date.now() - start, 1, 0], }); throw error; } } }; ``` ## SQL API (External Only) ```bash curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql \ -H "Authorization: Bearer $TOKEN" \ -d "SELECT blob1 AS endpoint, COUNT(*) AS requests FROM dataset WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY blob1" ``` ### Column References ```sql -- blob1..blob20, double1..double20, index1, timestamp SELECT blob1 AS endpoint, SUM(double1) AS latency, COUNT(*) AS requests FROM my_dataset WHERE index1 = 'customer_123' AND timestamp >= NOW() - INTERVAL '7' DAY GROUP BY blob1 HAVING COUNT(*) > 100 ORDER BY requests DESC LIMIT 100 ``` **Aggregations:** `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`, `quantile(0.95)()` **Time ranges:** `NOW() - INTERVAL '1' HOUR`, `BETWEEN '2026-01-01' AND '2026-01-31'` ### Query Examples ```sql -- Top endpoints SELECT blob1, COUNT(*) AS requests, AVG(double1) AS avg_latency FROM api_requests WHERE timestamp >= NOW() - INTERVAL '24' HOUR GROUP BY blob1 ORDER BY requests DESC LIMIT 20 -- Error rate SELECT blob1, COUNT(*) AS total, SUM(CASE WHEN blob3 LIKE '5%' THEN 1 ELSE 0 END) AS errors FROM api_requests WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY blob1 HAVING total > 50 -- P95 latency SELECT blob1, quantile(0.95)(double1) AS p95 FROM api_requests GROUP BY blob1 ``` ## Response Format ```json {"data": [{"endpoint": "/api/users", "requests": 1523}], "rows": 2} ``` ## Limits | Resource | Limit | |----------|-------| | Blobs/Doubles per point | 20 each | | Indexes per point | 1 | | Blob/Index size | 16KB | | Data retention | 90 days | | Query timeout | 30s | **Critical:** High write volumes (>1M/min) trigger automatic sampling. ================================================ FILE: skills/.curated/cloudflare-deploy/references/analytics-engine/configuration.md ================================================ # Analytics Engine Configuration ## Setup 1. Add binding to `wrangler.jsonc` 2. Deploy Worker 3. Dataset created automatically on first write 4. Query via SQL API ## wrangler.jsonc ```jsonc { "name": "my-worker", "analytics_engine_datasets": [ { "binding": "ANALYTICS", "dataset": "my_events" } ] } ``` Multiple datasets for separate concerns: ```jsonc { "analytics_engine_datasets": [ { "binding": "API_ANALYTICS", "dataset": "api_requests" }, { "binding": "USER_EVENTS", "dataset": "user_activity" } ] } ``` ## TypeScript ```typescript interface Env { ANALYTICS: AnalyticsEngineDataset; } export default { async fetch(request: Request, env: Env) { // No await - returns void, fire-and-forget env.ANALYTICS.writeDataPoint({ blobs: [pathname, method, status], // String dimensions (max 20) doubles: [latency, 1], // Numeric metrics (max 20) indexes: [apiKey] // High-cardinality filter (max 1) }); return response; } }; ``` ## Data Point Limits | Field | Limit | SQL Access | |-------|-------|------------| | blobs | 20 strings, 16KB each | `blob1`...`blob20` | | doubles | 20 numbers | `double1`...`double20` | | indexes | 1 string, 16KB | `index1` | ## Write Behavior | Scenario | Behavior | |----------|----------| | <1M writes/min | All accepted | | >1M writes/min | Automatic sampling | | Invalid data | Silent failure (check tail logs) | **Mitigate sampling:** Pre-aggregate, use multiple datasets, write only critical metrics. ## Query Limits | Resource | Limit | |----------|-------| | Query timeout | 30 seconds | | Data retention | 90 days (default) | | Result size | ~10MB | ## Cost **Free tier:** 10M writes/month, 1M reads/month **Paid:** $0.05 per 1M writes, $1.00 per 1M reads ## Environment-Specific ```jsonc { "analytics_engine_datasets": [ { "binding": "ANALYTICS", "dataset": "prod_events" } ], "env": { "staging": { "analytics_engine_datasets": [ { "binding": "ANALYTICS", "dataset": "staging_events" } ] } } } ``` ## Monitoring ```bash npx wrangler tail # Check for sampling/write errors ``` ```sql -- Check write activity SELECT DATE_TRUNC('hour', timestamp) AS hour, COUNT(*) AS writes FROM my_dataset WHERE timestamp >= NOW() - INTERVAL '24' HOUR GROUP BY hour ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/analytics-engine/gotchas.md ================================================ # Analytics Engine Gotchas ## Critical Issues ### Sampling at High Volumes **Problem:** Queries return fewer points than written at >1M writes/min. **Solution:** ```typescript // Pre-aggregate before writing let buffer = { count: 0, total: 0 }; buffer.count++; buffer.total += value; // Write once per second instead of per request if (Date.now() % 1000 === 0) { env.ANALYTICS.writeDataPoint({ doubles: [buffer.count, buffer.total] }); } ``` **Detection:** `npx wrangler tail` → look for "sampling enabled" ### writeDataPoint Returns void ```typescript // ❌ Pointless await await env.ANALYTICS.writeDataPoint({...}); // ✅ Fire-and-forget env.ANALYTICS.writeDataPoint({...}); ``` Writes can fail silently. Check tail logs. ### Index vs Blob | Cardinality | Use | Example | |-------------|-----|---------| | Millions | **Index** | user_id, api_key | | Hundreds | **Blob** | endpoint, status_code, country | ```typescript // ✅ Correct { blobs: [method, path, status], indexes: [userId] } ``` ### Can't Query from Workers Query API requires HTTP auth. Use external service or cache in KV/D1. ### No Custom Timestamps Auto-generated at write time. Store original in blob if needed. ## Common Errors | Error | Fix | |-------|-----| | Binding not found | Check wrangler.jsonc, redeploy | | No data in query | Wait 30s; check dataset name; check time range | | Query timeout | Add time filter; use index for filtering | ## Limits | Resource | Limit | |----------|-------| | Blobs per point | 20 | | Doubles per point | 20 | | Indexes per point | 1 | | Blob/Index size | 16KB | | Write rate (no sampling) | ~1M/min | | Retention | 90 days | | Query timeout | 30s | ## Best Practices ✅ Pre-aggregate at high volumes ✅ Use index for high-cardinality (millions) ✅ Always include time filter in queries ✅ Design schema before coding ❌ Don't await writeDataPoint ❌ Don't use index for low-cardinality ❌ Don't query without time range ❌ Don't assume all writes succeed ================================================ FILE: skills/.curated/cloudflare-deploy/references/analytics-engine/patterns.md ================================================ # Analytics Engine Patterns ## Use Cases | Use Case | Key Metrics | Index On | |----------|-------------|----------| | API Metering | requests, bytes, compute_units | api_key | | Feature Usage | feature, action, duration | user_id | | Error Tracking | error_type, endpoint, count | customer_id | | Performance | latency_ms, cache_status | endpoint | | A/B Testing | variant, conversions | user_id | ## API Metering (Billing) ```typescript env.ANALYTICS.writeDataPoint({ blobs: [pathname, method, status, tier], doubles: [1, computeUnits, bytes, latencyMs], indexes: [apiKey] }); // Query: Monthly usage by customer // SELECT index1 AS api_key, SUM(double2) AS compute_units // FROM usage WHERE timestamp >= DATE_TRUNC('month', NOW()) GROUP BY index1 ``` ## Error Tracking ```typescript env.ANALYTICS.writeDataPoint({ blobs: [endpoint, method, errorName, errorMessage.slice(0, 1000)], doubles: [1, timeToErrorMs], indexes: [customerId] }); ``` ## Performance Monitoring ```typescript env.ANALYTICS.writeDataPoint({ blobs: [pathname, method, cacheStatus, status], doubles: [latencyMs, 1], indexes: [userId] }); // Query: P95 latency by endpoint // SELECT blob1, quantile(0.95)(double1) AS p95_ms FROM perf GROUP BY blob1 ``` ## Anti-Patterns | ❌ Wrong | ✅ Correct | |----------|-----------| | `await writeDataPoint()` | `writeDataPoint()` (fire-and-forget) | | `indexes: [method]` (low cardinality) | `blobs: [method]`, `indexes: [userId]` | | `blobs: [JSON.stringify(obj)]` | Store ID in blob, full object in D1/KV | | Write every request at 10M/min | Pre-aggregate per second | | Query from Worker | Query from external service/API | ## Best Practices 1. **Design schema upfront** - Document blob/double/index assignments 2. **Always include count metric** - `doubles: [latency, 1]` for AVG calculations 3. **Use enums for blobs** - Consistent values like `Status.SUCCESS` 4. **Handle sampling** - Use ratios (avg_latency = SUM(latency)/SUM(count)) 5. **Test queries early** - Validate schema before heavy writes ## Schema Template ```typescript /** * Dataset: my_metrics * * Blobs: * blob1: endpoint, blob2: method, blob3: status * * Doubles: * double1: latency_ms, double2: count (always 1) * * Indexes: * index1: customer_id (high cardinality) */ ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/api/README.md ================================================ # Cloudflare API Integration Guide for working with Cloudflare's REST API - authentication, SDK usage, common patterns, and troubleshooting. ## Quick Decision Tree ``` How are you calling the Cloudflare API? ├─ From Workers runtime → Use bindings, not REST API (see ../bindings/) ├─ Server-side (Node/Python/Go) → Official SDK (see api.md) ├─ CLI/scripts → Wrangler or curl (see configuration.md) ├─ Infrastructure-as-code → See ../pulumi/ or ../terraform/ └─ One-off requests → curl examples (see api.md) ``` ## SDK Selection | Language | Package | Best For | Default Retries | |----------|---------|----------|-----------------| | TypeScript | `cloudflare` | Node.js, Bun, Next.js, Workers | 2 | | Python | `cloudflare` | FastAPI, Django, scripts | 2 | | Go | `cloudflare-go/v4` | CLI tools, microservices | 10 | All SDKs are Stainless-generated from OpenAPI spec (consistent APIs). ## Authentication Methods | Method | Security | Use Case | Scope | |--------|----------|----------|-------| | **API Token** ✓ | Scoped, rotatable | Production | Per-zone or account | | API Key + Email | Full account access | Legacy only | Everything | | User Service Key | Limited | Origin CA certs only | Origin CA | **Always use API tokens** for new projects. ## Rate Limits | Limit | Value | |-------|-------| | Per user/token | 1200 requests / 5 minutes | | Per IP | 200 requests / second | | GraphQL | 320 / 5 minutes (cost-based) | ## Reading Order | Task | Files to Read | |------|---------------| | Initialize SDK client | api.md | | Configure auth/timeout/retry | configuration.md | | Find usage patterns | patterns.md | | Debug errors/rate limits | gotchas.md | | Product-specific APIs | ../workers/, ../r2/, ../kv/, etc. | ## In This Reference - **[api.md](api.md)** - SDK client initialization, pagination, error handling, examples - **[configuration.md](configuration.md)** - Environment variables, SDK config, Wrangler setup - **[patterns.md](patterns.md)** - Real-world patterns, batch operations, workflows - **[gotchas.md](gotchas.md)** - Rate limits, SDK-specific issues, troubleshooting ## See Also - [Cloudflare API Docs](https://developers.cloudflare.com/api/) - [Bindings Reference](../bindings/) - Workers runtime bindings (preferred over REST API) - [Wrangler Reference](../wrangler/) - CLI tool for Cloudflare development ================================================ FILE: skills/.curated/cloudflare-deploy/references/api/api.md ================================================ # API Reference ## Client Initialization ### TypeScript ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN, }); ``` ### Python ```python from cloudflare import Cloudflare client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN")) # For async: from cloudflare import AsyncCloudflare client = AsyncCloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"]) ``` ### Go ```go import ( "github.com/cloudflare/cloudflare-go/v4" "github.com/cloudflare/cloudflare-go/v4/option" ) client := cloudflare.NewClient( option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")), ) ``` ## Authentication ### API Token (Recommended) **Create token**: Dashboard → My Profile → API Tokens → Create Token ```bash export CLOUDFLARE_API_TOKEN='your-token-here' curl "https://api.cloudflare.com/client/v4/zones" \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" ``` **Token scopes**: Always use minimal permissions (zone-specific, time-limited). ### API Key (Legacy) ```bash curl "https://api.cloudflare.com/client/v4/zones" \ --header "X-Auth-Email: user@example.com" \ --header "X-Auth-Key: $CLOUDFLARE_API_KEY" ``` **Not recommended:** Full account access, cannot scope permissions. ## Auto-Pagination All SDKs support automatic pagination for list operations. ```typescript // TypeScript: for await...of for await (const zone of client.zones.list()) { console.log(zone.id); } ``` ```python # Python: iterator protocol for zone in client.zones.list(): print(zone.id) ``` ```go // Go: ListAutoPaging iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{}) for iter.Next() { zone := iter.Current() fmt.Println(zone.ID) } ``` ## Error Handling ```typescript try { const zone = await client.zones.get({ zone_id: 'xxx' }); } catch (err) { if (err instanceof Cloudflare.NotFoundError) { // 404 } else if (err instanceof Cloudflare.RateLimitError) { // 429 - SDK auto-retries with backoff } else if (err instanceof Cloudflare.APIError) { console.log(err.status, err.message); } } ``` **Common Error Types:** - `AuthenticationError` (401) - Invalid token - `PermissionDeniedError` (403) - Insufficient scope - `NotFoundError` (404) - Resource not found - `RateLimitError` (429) - Rate limit exceeded - `InternalServerError` (≥500) - Cloudflare error ## Zone Management ```typescript // List zones const zones = await client.zones.list({ account: { id: 'account-id' }, status: 'active', }); // Create zone const zone = await client.zones.create({ account: { id: 'account-id' }, name: 'example.com', type: 'full', // or 'partial' }); // Update zone await client.zones.edit('zone-id', { paused: false, }); // Delete zone await client.zones.delete('zone-id'); ``` ```go // Go: requires cloudflare.F() wrapper zone, err := client.Zones.New(ctx, cloudflare.ZoneNewParams{ Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{ ID: cloudflare.F("account-id"), }), Name: cloudflare.F("example.com"), Type: cloudflare.F(cloudflare.ZoneNewParamsTypeFull), }) ``` ## DNS Management ```typescript // Create DNS record await client.dns.records.create({ zone_id: 'zone-id', type: 'A', name: 'subdomain.example.com', content: '192.0.2.1', ttl: 1, // auto proxied: true, // Orange cloud }); // List DNS records (with auto-pagination) for await (const record of client.dns.records.list({ zone_id: 'zone-id', type: 'A', })) { console.log(record.name, record.content); } // Update DNS record await client.dns.records.update({ zone_id: 'zone-id', dns_record_id: 'record-id', type: 'A', name: 'subdomain.example.com', content: '203.0.113.1', proxied: true, }); // Delete DNS record await client.dns.records.delete({ zone_id: 'zone-id', dns_record_id: 'record-id', }); ``` ```python # Python example client.dns.records.create( zone_id="zone-id", type="A", name="subdomain.example.com", content="192.0.2.1", ttl=1, proxied=True, ) ``` ## See Also - [configuration.md](./configuration.md) - SDK configuration, environment variables - [patterns.md](./patterns.md) - Real-world patterns and workflows - [gotchas.md](./gotchas.md) - Rate limits, troubleshooting ================================================ FILE: skills/.curated/cloudflare-deploy/references/api/configuration.md ================================================ # Configuration ## Environment Variables ### Set Variables | Platform | Command | |----------|---------| | Linux/macOS | `export CLOUDFLARE_API_TOKEN='token'` | | PowerShell | `$env:CLOUDFLARE_API_TOKEN = 'token'` | | Windows CMD | `set CLOUDFLARE_API_TOKEN=token` | **Security:** Never commit tokens. Use `.env` files (gitignored) or secret managers. ### .env File Pattern ```bash # .env (add to .gitignore) CLOUDFLARE_API_TOKEN=your-token-here CLOUDFLARE_ACCOUNT_ID=your-account-id ``` ```typescript // TypeScript import 'dotenv/config'; const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN, }); ``` ```python # Python from dotenv import load_dotenv load_dotenv() client = Cloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"]) ``` ## SDK Configuration ### TypeScript ```typescript const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN, timeout: 120000, // 2 min (default 60s), in milliseconds maxRetries: 5, // default 2 baseURL: 'https://...', // proxy (rare) }); // Per-request overrides await client.zones.get( { zone_id: 'zone-id' }, { timeout: 5000, maxRetries: 0 } ); ``` ### Python ```python client = Cloudflare( api_token=os.environ["CLOUDFLARE_API_TOKEN"], timeout=120, # seconds (default 60) max_retries=5, # default 2 base_url="https://...", # proxy (rare) ) # Per-request overrides client.with_options(timeout=5, max_retries=0).zones.get(zone_id="zone-id") ``` ### Go ```go client := cloudflare.NewClient( option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")), option.WithMaxRetries(5), // default 10 (higher than TS/Python) option.WithRequestTimeout(2 * time.Minute), // default 60s option.WithBaseURL("https://..."), // proxy (rare) ) // Per-request overrides client.Zones.Get(ctx, "zone-id", option.WithMaxRetries(0)) ``` ## Configuration Options | Option | TypeScript | Python | Go | Default | |--------|-----------|--------|-----|---------| | Timeout | `timeout` (ms) | `timeout` (s) | `WithRequestTimeout` | 60s | | Retries | `maxRetries` | `max_retries` | `WithMaxRetries` | 2 (Go: 10) | | Base URL | `baseURL` | `base_url` | `WithBaseURL` | api.cloudflare.com | **Note:** Go SDK has higher default retries (10) than TypeScript/Python (2). ## Timeout Configuration **When to increase:** - Large zone transfers - Bulk DNS operations - Worker script uploads ```typescript const client = new Cloudflare({ timeout: 300000, // 5 minutes }); ``` ## Retry Configuration **When to increase:** Rate-limit-heavy workflows, flaky network **When to decrease:** Fast-fail requirements, user-facing requests ```typescript // Increase retries for batch operations const client = new Cloudflare({ maxRetries: 10 }); // Disable retries for fast-fail const fastClient = new Cloudflare({ maxRetries: 0 }); ``` ## Wrangler CLI Integration ```bash # Configure authentication wrangler login # Or export CLOUDFLARE_API_TOKEN='token' # Common commands that use API wrangler deploy # Uploads worker via API wrangler kv:key put # KV operations wrangler r2 bucket create # R2 operations wrangler d1 execute # D1 operations wrangler pages deploy # Pages operations # Get API configuration wrangler whoami # Shows authenticated user ``` ### wrangler.toml ```toml name = "my-worker" main = "src/index.ts" compatibility_date = "2024-01-01" account_id = "your-account-id" # Can also use env vars: # CLOUDFLARE_ACCOUNT_ID # CLOUDFLARE_API_TOKEN ``` ## See Also - [api.md](./api.md) - Client initialization, authentication - [gotchas.md](./gotchas.md) - Rate limits, timeout errors - [Wrangler Reference](../wrangler/) - CLI tool details ================================================ FILE: skills/.curated/cloudflare-deploy/references/api/gotchas.md ================================================ # Gotchas & Troubleshooting ## Rate Limits & 429 Errors **Actual Limits:** - **1200 requests / 5 minutes** per user/token (global) - **200 requests / second** per IP address - **GraphQL: 320 / 5 minutes** (cost-based) **SDK Behavior:** - Auto-retry with exponential backoff (default 2 retries, Go: 10) - Respects `Retry-After` header - Throws `RateLimitError` after exhausting retries **Solution:** ```typescript // Increase retries for rate-limit-heavy workflows const client = new Cloudflare({ maxRetries: 5 }); // Add application-level throttling import pLimit from 'p-limit'; const limit = pLimit(10); // Max 10 concurrent requests ``` ## SDK-Specific Issues ### Go: Required Field Wrapper **Problem:** Go SDK requires `cloudflare.F()` wrapper for optional fields. ```go // ❌ WRONG - Won't compile or send field client.Zones.New(ctx, cloudflare.ZoneNewParams{ Name: "example.com", }) // ✅ CORRECT client.Zones.New(ctx, cloudflare.ZoneNewParams{ Name: cloudflare.F("example.com"), Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{ ID: cloudflare.F("account-id"), }), }) ``` **Why:** Distinguishes between zero value, null, and omitted fields. ### Python: Async vs Sync Clients **Problem:** Using sync client in async context or vice versa. ```python # ❌ WRONG - Can't await sync client from cloudflare import Cloudflare client = Cloudflare() await client.zones.list() # TypeError # ✅ CORRECT - Use AsyncCloudflare from cloudflare import AsyncCloudflare client = AsyncCloudflare() await client.zones.list() ``` ## Token Permission Errors (403) **Problem:** API returns 403 Forbidden despite valid token. **Cause:** Token lacks required permissions (scope). **Scopes Required:** | Operation | Required Scope | |-----------|----------------| | List zones | Zone:Read (zone-level or account-level) | | Create zone | Zone:Edit (account-level) | | Edit DNS | DNS:Edit (zone-level) | | Deploy Worker | Workers Script:Edit (account-level) | | Read KV | Workers KV Storage:Read | | Write KV | Workers KV Storage:Edit | **Solution:** Re-create token with correct permissions in Dashboard → My Profile → API Tokens. ## Pagination Truncation **Problem:** Only getting first 20 results (default page size). **Solution:** Use auto-pagination iterators. ```typescript // ❌ WRONG - Only first page (20 items) const page = await client.zones.list(); // ✅ CORRECT - All results const zones = []; for await (const zone of client.zones.list()) { zones.push(zone); } ``` ## Workers Subrequests **Problem:** Rate limit hit faster than expected in Workers. **Cause:** Workers subrequests count as separate API calls. **Solution:** Use bindings instead of REST API in Workers (see ../bindings/). ```typescript // ❌ WRONG - REST API in Workers (counts against rate limit) const client = new Cloudflare({ apiToken: env.CLOUDFLARE_API_TOKEN }); const zones = await client.zones.list(); // ✅ CORRECT - Use bindings (no rate limit) // Access via env.MY_BINDING ``` ## Authentication Errors (401) **Problem:** "Authentication failed" or "Invalid token" **Causes:** - Token expired - Token deleted/revoked - Token not set in environment - Wrong token format **Solution:** ```typescript // Verify token is set if (!process.env.CLOUDFLARE_API_TOKEN) { throw new Error('CLOUDFLARE_API_TOKEN not set'); } // Test token const user = await client.user.tokens.verify(); console.log('Token valid:', user.status); ``` ## Timeout Errors **Problem:** Request times out (default 60s). **Cause:** Large operations (bulk DNS, zone transfers). **Solution:** Increase timeout or split operations. ```typescript // Increase timeout const client = new Cloudflare({ timeout: 300000, // 5 minutes }); // Or split operations const batchSize = 100; for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); await processBatch(batch); } ``` ## Zone Not Found (404) **Problem:** Zone ID valid but returns 404. **Causes:** - Zone not in account associated with token - Zone deleted - Wrong zone ID format **Solution:** ```typescript // List all zones to find correct ID for await (const zone of client.zones.list()) { console.log(zone.id, zone.name); } ``` ## Limits Reference | Resource/Limit | Value | Notes | |----------------|-------|-------| | API rate limit | 1200/5min | Per user/token | | IP rate limit | 200/sec | Per IP | | GraphQL rate limit | 320/5min | Cost-based | | Parallel requests (recommended) | < 10 | Avoid overwhelming API | | Default page size | 20 | Use auto-pagination | | Max page size | 50 | Some endpoints | ## Best Practices **Security:** - Never commit tokens - Use minimal permissions - Rotate tokens regularly - Set token expiration **Performance:** - Batch operations - Use pagination wisely - Cache responses - Handle rate limits **Code Organization:** ```typescript // Create reusable client instance export const cfClient = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN, maxRetries: 5, }); // Wrap common operations export async function getZoneDetails(zoneId: string) { return await cfClient.zones.get({ zone_id: zoneId }); } ``` ## See Also - [api.md](./api.md) - Error types, authentication - [configuration.md](./configuration.md) - Timeout/retry configuration - [patterns.md](./patterns.md) - Error handling patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/api/patterns.md ================================================ # Common Patterns ## List All with Auto-Pagination **Problem:** API returns paginated results. Default page size is 20. **Solution:** Use SDK auto-pagination to iterate all results. ```typescript // TypeScript for await (const zone of client.zones.list()) { console.log(zone.name); } ``` ```python # Python for zone in client.zones.list(): print(zone.name) ``` ```go // Go iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{}) for iter.Next() { fmt.Println(iter.Current().Name) } ``` ## Error Handling with Retry **Problem:** Rate limits (429) and transient errors need retry. **Solution:** SDKs auto-retry with exponential backoff. Customize as needed. ```typescript // Increase retries for rate-limit-heavy operations const client = new Cloudflare({ maxRetries: 5 }); try { const zone = await client.zones.create({ /* ... */ }); } catch (err) { if (err instanceof Cloudflare.RateLimitError) { // Already retried 5 times with backoff const retryAfter = err.headers['retry-after']; console.log(`Rate limited. Retry after ${retryAfter}s`); } } ``` ## Batch Parallel Operations **Problem:** Need to create multiple resources quickly. **Solution:** Use `Promise.all()` for parallel requests (respect rate limits). ```typescript // Create multiple DNS records in parallel const records = ['www', 'api', 'cdn'].map(subdomain => client.dns.records.create({ zone_id: 'zone-id', type: 'A', name: `${subdomain}.example.com`, content: '192.0.2.1', }) ); await Promise.all(records); ``` **Controlled concurrency** (avoid rate limits): ```typescript import pLimit from 'p-limit'; const limit = pLimit(10); // Max 10 concurrent const subdomains = ['www', 'api', 'cdn', /* many more */]; const records = subdomains.map(subdomain => limit(() => client.dns.records.create({ zone_id: 'zone-id', type: 'A', name: `${subdomain}.example.com`, content: '192.0.2.1', })) ); await Promise.all(records); ``` ## Zone CRUD Workflow ```typescript // Create const zone = await client.zones.create({ account: { id: 'account-id' }, name: 'example.com', type: 'full', }); // Read const fetched = await client.zones.get({ zone_id: zone.id }); // Update await client.zones.edit(zone.id, { paused: false }); // Delete await client.zones.delete(zone.id); ``` ## DNS Bulk Update ```typescript // Fetch all A records const records = []; for await (const record of client.dns.records.list({ zone_id: 'zone-id', type: 'A', })) { records.push(record); } // Update all to new IP await Promise.all(records.map(record => client.dns.records.update({ zone_id: 'zone-id', dns_record_id: record.id, type: 'A', name: record.name, content: '203.0.113.1', // New IP proxied: record.proxied, ttl: record.ttl, }) )); ``` ## Filter and Collect Results ```typescript // Find all proxied A records const proxiedRecords = []; for await (const record of client.dns.records.list({ zone_id: 'zone-id', type: 'A', })) { if (record.proxied) { proxiedRecords.push(record); } } ``` ## Error Recovery Pattern ```typescript async function createZoneWithRetry(name: string, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await client.zones.create({ account: { id: 'account-id' }, name, type: 'full', }); } catch (err) { if (err instanceof Cloudflare.RateLimitError && attempt < maxAttempts) { const retryAfter = parseInt(err.headers['retry-after'] || '5'); console.log(`Rate limited, waiting ${retryAfter}s (retry ${attempt}/${maxAttempts})`); await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); } else { throw err; } } } } ``` ## Conditional Update Pattern ```typescript // Only update if zone is active const zone = await client.zones.get({ zone_id: 'zone-id' }); if (zone.status === 'active') { await client.zones.edit(zone.id, { paused: false }); } ``` ## Batch with Error Handling ```typescript // Process multiple zones, continue on errors const results = await Promise.allSettled( zoneIds.map(id => client.zones.get({ zone_id: id })) ); results.forEach((result, i) => { if (result.status === 'fulfilled') { console.log(`Zone ${i}: ${result.value.name}`); } else { console.error(`Zone ${i} failed:`, result.reason.message); } }); ``` ## See Also - [api.md](./api.md) - SDK client initialization, basic operations - [gotchas.md](./gotchas.md) - Rate limits, common errors - [configuration.md](./configuration.md) - SDK configuration options ================================================ FILE: skills/.curated/cloudflare-deploy/references/api-shield/README.md ================================================ # Cloudflare API Shield Reference Expert guidance for API Shield - comprehensive API security suite for discovery, protection, and monitoring. ## Reading Order | Task | Files to Read | |------|---------------| | Initial setup | README → configuration.md | | Implement JWT validation | configuration.md → api.md | | Add schema validation | configuration.md → patterns.md | | Detect API attacks | patterns.md → api.md | | Debug issues | gotchas.md | ## Feature Selection What protection do you need? ``` ├─ Validate request/response structure → Schema Validation 2.0 (configuration.md) ├─ Verify auth tokens → JWT Validation (configuration.md) ├─ Client certificates → mTLS (configuration.md) ├─ Detect BOLA attacks → BOLA Detection (patterns.md) ├─ Track auth coverage → Auth Posture (patterns.md) ├─ Stop volumetric abuse → Abuse Detection (patterns.md) └─ Discover shadow APIs → API Discovery (api.md) ``` ## 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 ## Quick Start API Shield: Enterprise-grade API security (Discovery, Schema Validation 2.0, JWT, mTLS, BOLA Detection, Auth Posture). Available as Enterprise add-on with preview access. ## See Also - [API Shield Docs](https://developers.cloudflare.com/api-shield/) - [API Reference](https://developers.cloudflare.com/api/resources/api_gateway/) - [OWASP API Security Top 10](https://owasp.org/www-project-api-security/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/api-shield/api.md ================================================ # API Reference Base: `/zones/{zone_id}/api_gateway` ## Endpoints ```bash GET /operations # List GET /operations/{op_id} # Get single POST /operations/item # Create: {endpoint,host,method} POST /operations # Bulk: {operations:[{endpoint,host,method}]} DELETE /operations/{op_id} # Delete DELETE /operations # Bulk delete: {operation_ids:[...]} ``` ## Discovery ```bash GET /discovery/operations # List discovered PATCH /discovery/operations/{op_id} # Update: {state:"saved"|"ignored"} PATCH /discovery/operations # Bulk: {operation_ids:{id:{state}}} GET /discovery # OpenAPI export ``` ## Config ```bash GET /configuration # Get session ID config PUT /configuration # Update: {auth_id_characteristics:[{name,type:"header"|"cookie"}]} ``` ## Token Validation ```bash GET /token_validation # List POST /token_validation # Create: {name,location:{header:"..."},jwks:"..."} POST /jwt_validation_rules # Rule: {name,hostname,token_validation_id,action:"block"} ``` ## Workers Integration ### Access JWT Claims ```js export default { async fetch(req, env) { // Access validated JWT payload const jwt = req.cf?.jwt?.payload?.[env.JWT_CONFIG_ID]?.[0]; if (jwt) { const userId = jwt.sub; const role = jwt.role; } } } ``` ### Access mTLS Info ```js export default { async fetch(req, env) { const tls = req.cf?.tlsClientAuth; if (tls?.certVerified === 'SUCCESS') { const fingerprint = tls.certFingerprintSHA256; // Authenticated client } } } ``` ### Dynamic JWKS Update ```js export default { async scheduled(event, env) { const jwks = await (await fetch('https://auth.example.com/.well-known/jwks.json')).json(); await fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/api_gateway/token_validation/${env.CONFIG_ID}`, { method: 'PATCH', headers: {'Authorization': `Bearer ${env.CF_API_TOKEN}`, 'Content-Type': 'application/json'}, body: JSON.stringify({jwks: JSON.stringify(jwks)}) }); } } ``` ## Firewall Fields ### Core Fields ```js cf.api_gateway.auth_id_present // Session ID present cf.api_gateway.request_violates_schema // Schema violation cf.api_gateway.fallthrough_triggered // No endpoint match cf.tls_client_auth.cert_verified // mTLS cert valid cf.tls_client_auth.cert_fingerprint_sha256 ``` ### JWT Validation (2026) ```js // Modern validation syntax is_jwt_valid(http.request.jwt.payload["{config_id}"][0]) // Legacy (still supported) cf.api_gateway.jwt_claims_valid // Extract claims lookup_json_string(http.request.jwt.payload["{config_id}"][0], "claim_name") ``` ### Risk Labels (2026) ```js // BOLA detection cf.api_gateway.cf-risk-bola-enumeration // Sequential resource access detected cf.api_gateway.cf-risk-bola-pollution // Parameter pollution detected // Authentication posture cf.api_gateway.cf-risk-missing-auth // Endpoint lacks authentication cf.api_gateway.cf-risk-mixed-auth // Inconsistent auth patterns ``` ## BOLA Detection ```bash GET /user_schemas/{schema_id}/bola # Get BOLA config PATCH /user_schemas/{schema_id}/bola # Update: {enabled:true} ``` ## Auth Posture ```bash GET /discovery/authentication_posture # List unprotected endpoints ``` ## GraphQL Protection ```bash GET /settings/graphql_protection # Get limits PUT /settings/graphql_protection # Set: {max_depth,max_size} ``` ## See Also - [configuration.md](configuration.md) - Setup guides for all features - [patterns.md](patterns.md) - Firewall rules and common patterns - [API Gateway API Docs](https://developers.cloudflare.com/api/resources/api_gateway/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/api-shield/configuration.md ================================================ # Configuration ## Schema Validation 2.0 Setup > ⚠️ **Classic Schema Validation deprecated.** Use Schema Validation 2.0. **Upload schema (Dashboard):** ``` Security > API Shield > Schema Validation > Add validation - Upload .yml/.yaml/.json (OpenAPI v3.0) - Endpoints auto-added to Endpoint Management - Action: Log | Block | None - Body inspection: JSON payloads ``` **Change validation action:** ``` Security > API Shield > Settings > Schema Validation Per-endpoint: Filter → ellipses → Change action Default action: Set global mitigation action ``` **Migration from Classic:** ``` 1. Export existing schema (if available) 2. Delete all Classic schema validation rules 3. Wait 5 min for cache clear 4. Re-upload via Schema Validation 2.0 interface 5. Verify in Security > Events ``` **Fallthrough rule** (catch-all unknown endpoints): ``` Security > API Shield > Settings > Fallthrough > Use Template - Select hostnames - Create rule with cf.api_gateway.fallthrough_triggered - Action: Log (discover) or Block (strict) ``` **Body inspection:** Supports `application/json`, `*/*`, `application/*`. Disable origin MIME sniffing to prevent bypasses. ## JWT Validation **Setup token config:** ``` Security > API Shield > Settings > JWT Settings > Add configuration - Name: "Auth0 JWT Config" - Location: Header/Cookie + name (e.g., "Authorization") - JWKS: Paste public keys from IdP ``` **Create validation rule:** ``` Security > API Shield > API Rules > Add rule - Hostname: api.example.com - Deselect endpoints to ignore - Token config: Select config - Enforce presence: Ignore or Mark as non-compliant - Action: Log/Block/Challenge ``` **Rate limit by JWT claim:** ```wirefilter lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub") ``` **Special cases:** - Two JWTs, different IdPs: Create 2 configs, select both, "Validate all" - IdP migration: 2 configs + 2 rules, adjust actions per state - Bearer prefix: API Shield handles with/without - Nested claims: Dot notation `user.email` ## Mutual TLS (mTLS) **Setup:** ``` SSL/TLS > Client Certificates > Create Certificate - Generate CF-managed CA (all plans) - Upload custom CA (Enterprise, max 5) ``` **Configure mTLS rule:** ``` Security > API Shield > mTLS - Select hostname(s) - Choose certificate(s) - Action: Block/Log/Challenge ``` **Test:** ```bash openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365 curl https://api.example.com/endpoint --cert client-cert.pem --key client-key.pem ``` ## Session Identifiers Critical for BOLA Detection, Sequence Mitigation, and analytics. Configure header/cookie that uniquely IDs API users. **Examples:** JWT sub claim, session token, API key, custom user ID header **Configure:** ``` Security > API Shield > Settings > Session Identifiers - Type: Header/Cookie - Name: "X-User-ID" or "Authorization" ``` ## BOLA Detection Detects Broken Object Level Authorization attacks (enumeration + parameter pollution). **Enable:** ``` Security > API Shield > Schema Validation > [Select Schema] > BOLA Detection - Enable detection - Threshold: Sensitivity level (Low/Medium/High) - Action: Log or Block ``` **Requirements:** - Schema Validation 2.0 enabled - Session identifiers configured - Minimum traffic: 1000+ requests/day per endpoint ## Authentication Posture Identifies unprotected or inconsistently protected endpoints. **View report:** ``` Security > API Shield > Authentication Posture - Shows endpoints lacking JWT/mTLS - Highlights mixed authentication patterns ``` **Remediate:** 1. Review flagged endpoints 2. Add JWT validation rules 3. Configure mTLS for sensitive endpoints 4. Monitor posture score ## Volumetric Abuse + GraphQL **Volumetric Abuse Detection:** `Security > API Shield > Settings > Volumetric Abuse Detection` - Enable per-endpoint monitoring, set thresholds, action: Log | Challenge | Block **GraphQL Protection:** `Security > API Shield > Settings > GraphQL Protection` - Max query depth: 10, max size: 100KB, block introspection (production) ## Terraform ```hcl # Session identifier resource "cloudflare_api_shield" "main" { zone_id = var.zone_id auth_id_characteristics { type = "header" name = "Authorization" } } # Add endpoint resource "cloudflare_api_shield_operation" "users_get" { zone_id = var.zone_id method = "GET" host = "api.example.com" endpoint = "/api/users/{id}" } # JWT validation rule resource "cloudflare_ruleset" "jwt_validation" { zone_id = var.zone_id name = "API JWT Validation" kind = "zone" phase = "http_request_firewall_custom" rules { action = "block" expression = "(http.host eq \"api.example.com\" and not is_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0]))" description = "Block invalid JWTs" } } ``` ## See Also - [api.md](api.md) - API endpoints and Workers integration - [patterns.md](patterns.md) - Firewall rules and deployment patterns - [gotchas.md](gotchas.md) - Troubleshooting and limits ================================================ FILE: skills/.curated/cloudflare-deploy/references/api-shield/gotchas.md ================================================ # Gotchas & Troubleshooting ## Common Errors ### "Schema Validation 2.0 not working after migration" **Cause:** Classic rules still active, conflicting with new system **Solution:** 1. Delete ALL Classic schema validation rules 2. Clear Cloudflare cache (wait 5 min) 3. Re-upload schema via new Schema Validation 2.0 interface 4. Verify in Security > Events 5. Check action is set (Log/Block) ### "Schema validation blocking valid requests" **Cause:** Schema too restrictive, missing fields, or incorrect types **Solution:** 1. Check Firewall Events for violation details 2. Review schema in Settings 3. Test schema in Swagger Editor 4. Use Log mode to validate before blocking 5. Update schema with correct specifications 6. Ensure Schema Validation 2.0 (not Classic) ### "JWT validation failing" **Cause:** JWKS mismatch with IdP, expired token, wrong header/cookie name, or clock skew **Solution:** 1. Verify JWKS matches IdP configuration 2. Check token `exp` claim is valid 3. Confirm header/cookie name matches config 4. Test token at jwt.io 5. Account for clock skew (±5 min tolerance) 6. Use modern syntax: `is_jwt_valid(http.request.jwt.payload["{config_id}"][0])` ### "BOLA detection false positives" **Cause:** Legitimate sequential access patterns, bulk operations, or sensitivity too high **Solution:** 1. Review BOLA events in Security > Events 2. Lower sensitivity threshold (High → Medium → Low) 3. Exclude legitimate bulk operations from detection 4. Ensure session identifiers uniquely identify users 5. Verify minimum traffic requirements met (1000+ req/day) ### "Risk labels not appearing in firewall rules" **Cause:** Feature not enabled, insufficient traffic, or missing session identifiers **Solution:** 1. Verify Schema Validation 2.0 enabled 2. Enable BOLA Detection in schema settings 3. Configure session identifiers (required for BOLA) 4. Wait 24-48h for ML model training 5. Check minimum traffic thresholds met ### "Endpoint discovery not finding APIs" **Cause:** Insufficient traffic (<500 reqs/10d), non-2xx responses, Worker direct requests, or incorrect session ID config **Solution:** Ensure 500+ requests in 10 days, 2xx responses from edge (not Workers direct), configure session IDs correctly. ML updates daily. ### "Sequence detection false positives" **Cause:** Lookback window issues, non-unique session IDs, or model sensitivity **Solution:** 1. Review lookback settings (10 reqs to managed endpoints, 10min window) 2. Ensure session ID uniqueness per user (not shared tokens) 3. Adjust positive/negative model balance 4. Exclude legitimate workflows from detection ### "GraphQL protection blocking valid queries" **Cause:** Query depth/size limits too restrictive, complex but legitimate queries **Solution:** 1. Review blocked query patterns in Security > Events 2. Increase max_depth (default: 10) if needed 3. Increase max_size (default: 100KB) for complex queries 4. Whitelist specific query signatures 5. Use Log mode to tune before blocking ### "Token invalid" **Cause:** Configuration error, JWKS mismatch, or expired token **Solution:** Verify config matches IdP, update JWKS, check token expiration ### "Schema violation" **Cause:** Missing required fields, wrong data types, or spec mismatch **Solution:** Review schema against actual requests, ensure all required fields present, validate types match spec ### "Fallthrough" **Cause:** Unknown endpoint or pattern mismatch **Solution:** Update schema with all endpoints, check path pattern matching ### "mTLS failed" **Cause:** Certificate untrusted/expired or wrong CA **Solution:** Verify cert chain, check expiration, confirm correct CA uploaded ## Limits (2026) | Resource/Limit | Value | Notes | |----------------|-------|-------| | OpenAPI version | v3.0.x only | No external refs, must be valid | | Schema operations | 10K (Enterprise) | Contact for higher limits | | JWT validation sources | Headers/cookies only | No query params/body | | Endpoint discovery | 500+ reqs/10d | Minimum for ML model | | Path normalization | Automatic | `/profile/238` → `/profile/{var1}` | | Schema parameters | No `content` field | No object param validation | | BOLA detection | 1000+ reqs/day/endpoint | Per-endpoint minimum | | Session ID uniqueness | Required | BOLA/Sequence need unique IDs | | GraphQL max depth | 1-50 | Default: 10 | | GraphQL max size | 1KB-1MB | Default: 100KB | | JWT claim nesting | 10 levels max | Use dot notation | | mTLS CA certificates | 5 custom max | CF-managed unlimited | | Schema upload size | 5MB max | Compressed OpenAPI spec | | Volumetric abuse baseline | 7 days training | Initial ML period | | Auth Posture refresh | Daily | Updated nightly | ## See Also - [configuration.md](configuration.md) - Setup guides to avoid common issues - [patterns.md](patterns.md) - Best practices and progressive rollout - [API Shield Docs](https://developers.cloudflare.com/api-shield/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/api-shield/patterns.md ================================================ # Patterns & Use Cases ## Protect API with Schema + JWT ```bash # 1. Upload OpenAPI schema POST /zones/{zone_id}/api_gateway/user_schemas # 2. Configure JWT validation POST /zones/{zone_id}/api_gateway/token_validation { "name": "Auth0", "location": {"header": "Authorization"}, "jwks": "{...}" } # 3. Create JWT rule POST /zones/{zone_id}/api_gateway/jwt_validation_rules # 4. Set schema validation action PUT /zones/{zone_id}/api_gateway/settings/schema_validation {"validation_default_mitigation_action": "block"} ``` ## Progressive Rollout ``` 1. Log mode: Observe false positives - Schema: Action = Log - JWT: Action = Log 2. Block subset: Protect critical endpoints - Change specific endpoint actions to Block - Monitor firewall events 3. Full enforcement: Block all violations - Change default action to Block - Handle fallthrough with custom rule ``` ## BOLA Detection ### Enumeration Detection Detects sequential resource access (e.g., `/users/1`, `/users/2`, `/users/3`). ```javascript // Block BOLA enumeration attempts (cf.api_gateway.cf-risk-bola-enumeration and http.host eq "api.example.com") // Action: Block or Challenge ``` ### Parameter Pollution Detects duplicate/excessive parameters in requests. ```javascript // Block parameter pollution (cf.api_gateway.cf-risk-bola-pollution and http.host eq "api.example.com") // Action: Block ``` ### Combined BOLA Protection ```javascript // Comprehensive BOLA rule (cf.api_gateway.cf-risk-bola-enumeration or cf.api_gateway.cf-risk-bola-pollution) and http.host eq "api.example.com" // Action: Block ``` ## Authentication Posture ### Detect Missing Auth ```javascript // Log endpoints lacking authentication (cf.api_gateway.cf-risk-missing-auth and http.host eq "api.example.com") // Action: Log (for audit) ``` ### Detect Mixed Auth ```javascript // Alert on inconsistent auth patterns (cf.api_gateway.cf-risk-mixed-auth and http.host eq "api.example.com") // Action: Log (review required) ``` ## Fallthrough Detection (Shadow APIs) ```javascript // WAF Custom Rule (cf.api_gateway.fallthrough_triggered and http.host eq "api.example.com") // Action: Log (discover unknown) or Block (strict) ``` ## Rate Limiting by User ```javascript // Rate Limiting Rule (modern syntax) (http.host eq "api.example.com" and is_jwt_valid(http.request.jwt.payload["{config_id}"][0])) // Rate: 100 req/60s // Counting expression: lookup_json_string(http.request.jwt.payload["{config_id}"][0], "sub") ``` ## Volumetric Abuse Response ```javascript // Detect abnormal traffic spikes (cf.api_gateway.volumetric_abuse_detected and http.host eq "api.example.com") // Action: Challenge or Rate Limit // Combined with rate limiting (cf.api_gateway.volumetric_abuse_detected or cf.threat_score gt 50) and http.host eq "api.example.com" // Action: JS Challenge ``` ## GraphQL Protection ```javascript // Block oversized queries (http.request.uri.path eq "/graphql" and cf.api_gateway.graphql_query_size gt 100000) // Action: Block // Block deep nested queries (http.request.uri.path eq "/graphql" and cf.api_gateway.graphql_query_depth gt 10) // Action: Block ``` ## Architecture Patterns **Public API:** Discovery + Schema Validation 2.0 + JWT + Rate Limiting + Bot Management **Partner API:** mTLS + Schema Validation + Sequence Mitigation **Internal API:** Discovery + Schema Learning + Auth Posture ## OWASP API Security Top 10 Mapping (2026) | OWASP Issue | API Shield Solutions | |-------------|---------------------| | API1:2023 Broken Object Level Authorization | **BOLA Detection** (enumeration + pollution), Sequence mitigation, Schema, JWT, Rate Limiting | | API2:2023 Broken Authentication | **Auth Posture**, mTLS, JWT validation, Bot Management | | API3:2023 Broken Object Property Auth | Schema validation, JWT validation | | API4:2023 Unrestricted Resource Access | Rate Limiting, **Volumetric Abuse Detection**, **GraphQL Protection**, Bot Management | | API5:2023 Broken Function Level Auth | Schema validation, JWT validation, Auth Posture | | API6:2023 Unrestricted Business Flows | Sequence mitigation, Bot Management | | API7:2023 SSRF | Schema validation, WAF managed rules | | API8:2023 Security Misconfiguration | **Schema Validation 2.0**, Auth Posture, WAF rules | | API9:2023 Improper Inventory Management | **API Discovery**, Schema learning, Auth Posture | | API10:2023 Unsafe API Consumption | JWT validation, Schema validation, WAF managed | ## Monitoring **Security Events:** `Security > Events` → Filter: Action = block, Service = API Shield **Firewall Analytics:** `Analytics > Security` → Filter by `cf.api_gateway.*` fields **Logpush fields:** APIGatewayAuthIDPresent, APIGatewayRequestViolatesSchema, APIGatewayFallthroughDetected, JWTValidationResult ## Availability (2026) | Feature | Availability | Notes | |---------|-------------|-------| | mTLS (CF-managed CA) | All plans | Self-service | | Endpoint Management | All plans | Limited operations | | Schema Validation 2.0 | All plans | Limited operations | | API Discovery | Enterprise | 10K+ ops | | JWT Validation | Enterprise add-on | Full validation | | BOLA Detection | Enterprise add-on | Requires session IDs | | Auth Posture | Enterprise add-on | Security audit | | Volumetric Abuse Detection | Enterprise add-on | Traffic analysis | | GraphQL Protection | Enterprise add-on | Query limits | | Sequence Mitigation | Enterprise (beta) | Contact team | | Full Suite | Enterprise add-on | All features | **Enterprise limits:** 10K operations (contact for higher). Preview access available for non-contract evaluation. ## See Also - [configuration.md](configuration.md) - Setup all features before creating rules - [api.md](api.md) - Firewall field reference and API endpoints - [gotchas.md](gotchas.md) - Common issues and limits ================================================ FILE: skills/.curated/cloudflare-deploy/references/argo-smart-routing/README.md ================================================ # Cloudflare Argo Smart Routing Skill Reference ## Overview Cloudflare Argo Smart Routing is a performance optimization service that detects real-time network issues and routes web traffic across the most efficient network path. It continuously monitors network conditions and intelligently routes traffic through the fastest, most reliable routes in Cloudflare's network. **Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product for enhanced DDoS protection and performance. Existing Argo customers maintain full functionality with gradual migration to Smart Shield features. ## Quick Start ### Enable via cURL ```bash curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "on"}' ``` ### Enable via TypeScript SDK ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN }); const result = await client.argo.smartRouting.edit({ zone_id: 'your-zone-id', value: 'on', }); console.log(`Argo enabled: ${result.value}`); ``` ## Core Concepts ### What It Does - **Intelligent routing**: Detects congestion, outages, packet loss in real-time - **Global optimization**: Routes across 300+ Cloudflare data centers - **Automatic failover**: Switches paths when issues detected (typically <1s) - **Works with existing setup**: No origin changes required ### Billing Model - Usage-based: Charged per GB of traffic (excluding DDoS/WAF mitigated traffic) - Requires billing configuration before enabling - Available on Enterprise+ plans (check zone eligibility) ### When to Use - **High-traffic production sites** with global user base - **Latency-sensitive applications** (APIs, real-time services) - **Sites behind Cloudflare proxy** (orange-clouded DNS records) - **Combined with Tiered Cache** for maximum performance gains ### When NOT to Use - Development/staging environments (cost control) - Low-traffic sites (<1TB/month) where cost may exceed benefit - Sites with primarily single-region traffic ## Should I Enable Argo? | Your Situation | Recommendation | |----------------|----------------| | Global production app, >1TB/month traffic | ✅ Enable - likely ROI positive | | Enterprise plan, latency-critical APIs | ✅ Enable - performance matters | | Regional site, <100GB/month traffic | ⚠️ Evaluate - cost may not justify | | Development/staging environment | ❌ Disable - use in production only | | Not yet configured billing | ❌ Configure billing first | ## Reading Order by Task | Your Goal | Start With | Then Read | |-----------|------------|-----------| | Enable Argo for first time | Quick Start above → [configuration.md](configuration.md) | [gotchas.md](gotchas.md) | | Use TypeScript/Python SDK | [api.md](api.md) | [patterns.md](patterns.md) | | Terraform/IaC setup | [configuration.md](configuration.md) | - | | Enable for Spectrum TCP app | [patterns.md](patterns.md) → Spectrum section | [api.md](api.md) | | Troubleshoot enablement issue | [gotchas.md](gotchas.md) | [api.md](api.md) | | Manage billing/usage | [patterns.md](patterns.md) → Billing section | [gotchas.md](gotchas.md) | ## In This Reference - **[api.md](api.md)** - API endpoints, SDK methods, error handling, Python/TypeScript examples - **[configuration.md](configuration.md)** - Terraform setup, environment config, billing configuration - **[patterns.md](patterns.md)** - Tiered Cache integration, Spectrum TCP apps, billing management, validation patterns - **[gotchas.md](gotchas.md)** - Common errors, permission issues, limits, best practices ## See Also - [Cloudflare Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/) - [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/) - [Spectrum Documentation](https://developers.cloudflare.com/spectrum/) - [Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/argo-smart-routing/api.md ================================================ ## API Reference **Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product. API endpoints remain stable; existing integrations continue to work without changes. ### Base Endpoint ``` https://api.cloudflare.com/client/v4 ``` ### Authentication Use API tokens with Zone:Argo Smart Routing:Edit permissions: ```bash # Headers required X-Auth-Email: user@example.com Authorization: Bearer YOUR_API_TOKEN ``` ### Get Argo Smart Routing Status **Endpoint:** `GET /zones/{zone_id}/argo/smart_routing` **Description:** Retrieves current Argo Smart Routing enablement status. **cURL Example:** ```bash curl -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" ``` **Response:** ```json { "result": { "id": "smart_routing", "value": "on", "editable": true, "modified_on": "2024-01-11T12:00:00Z" }, "success": true, "errors": [], "messages": [] } ``` **TypeScript SDK Example:** ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN }); const status = await client.argo.smartRouting.get({ zone_id: 'your-zone-id' }); console.log(`Argo status: ${status.value}, editable: ${status.editable}`); ``` **Python SDK Example:** ```python from cloudflare import Cloudflare client = Cloudflare(api_token=os.environ.get('CLOUDFLARE_API_TOKEN')) status = client.argo.smart_routing.get(zone_id='your-zone-id') print(f"Argo status: {status.value}, editable: {status.editable}") ``` ### Update Argo Smart Routing Status **Endpoint:** `PATCH /zones/{zone_id}/argo/smart_routing` **Description:** Enable or disable Argo Smart Routing for a zone. **Request Body:** ```json { "value": "on" // or "off" } ``` **cURL Example:** ```bash curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "on"}' ``` **TypeScript SDK Example:** ```typescript const result = await client.argo.smartRouting.edit({ zone_id: 'your-zone-id', value: 'on', }); console.log(`Updated: ${result.value} at ${result.modified_on}`); ``` **Python SDK Example:** ```python result = client.argo.smart_routing.edit( zone_id='your-zone-id', value='on' ) print(f"Updated: {result.value} at {result.modified_on}") ``` ## Checking Editability Before Updates **Critical:** Always check the `editable` field before attempting to enable/disable Argo. When `editable: false`, the zone has restrictions (billing not configured, insufficient permissions, or plan limitations). **Pattern:** ```typescript async function safelyEnableArgo(client: Cloudflare, zoneId: string): Promise { const status = await client.argo.smartRouting.get({ zone_id: zoneId }); if (!status.editable) { console.error('Cannot modify Argo: editable=false (check billing/permissions)'); return false; } if (status.value === 'on') { console.log('Argo already enabled'); return true; } await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }); console.log('Argo enabled successfully'); return true; } ``` **Python Pattern:** ```python def safely_enable_argo(client: Cloudflare, zone_id: str) -> bool: status = client.argo.smart_routing.get(zone_id=zone_id) if not status.editable: print('Cannot modify Argo: editable=false (check billing/permissions)') return False if status.value == 'on': print('Argo already enabled') return True client.argo.smart_routing.edit(zone_id=zone_id, value='on') print('Argo enabled successfully') return True ``` ## Error Handling The TypeScript SDK provides typed error classes for robust error handling: ```typescript import Cloudflare from 'cloudflare'; import { APIError, APIConnectionError, RateLimitError } from 'cloudflare'; async function enableArgoWithErrorHandling(client: Cloudflare, zoneId: string) { try { const result = await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on', }); return result; } catch (error) { if (error instanceof RateLimitError) { console.error('Rate limited. Retry after:', error.response?.headers.get('retry-after')); // Implement exponential backoff } else if (error instanceof APIError) { console.error('API error:', error.status, error.message); if (error.status === 403) { console.error('Permission denied - check API token scopes'); } else if (error.status === 400) { console.error('Bad request - verify zone_id and payload'); } } else if (error instanceof APIConnectionError) { console.error('Connection failed:', error.message); // Retry with exponential backoff } else { console.error('Unexpected error:', error); } throw error; } } ``` **Python Error Handling:** ```python from cloudflare import Cloudflare, APIError, RateLimitError def enable_argo_with_error_handling(client: Cloudflare, zone_id: str): try: result = client.argo.smart_routing.edit(zone_id=zone_id, value='on') return result except RateLimitError as e: print(f"Rate limited. Retry after: {e.response.headers.get('retry-after')}") raise except APIError as e: print(f"API error: {e.status} - {e.message}") if e.status == 403: print('Permission denied - check API token scopes') elif e.status == 400: print('Bad request - verify zone_id and payload') raise except Exception as e: print(f"Unexpected error: {e}") raise ``` ## Response Schema All Argo Smart Routing API responses follow this structure: ```typescript interface ArgoSmartRoutingResponse { result: { id: 'smart_routing'; value: 'on' | 'off'; editable: boolean; modified_on: string; // ISO 8601 timestamp }; success: boolean; errors: Array<{ code: number; message: string; }>; messages: Array; } ``` ## Key Response Fields | Field | Type | Description | |-------|------|-------------| | `value` | `"on" \| "off"` | Current enablement status | | `editable` | `boolean` | Whether changes are allowed (check before PATCH) | | `modified_on` | `string` | ISO timestamp of last modification | | `success` | `boolean` | Whether request succeeded | | `errors` | `Array` | Error details if `success: false` ================================================ FILE: skills/.curated/cloudflare-deploy/references/argo-smart-routing/configuration.md ================================================ ## Configuration Management **Note on Smart Shield Evolution:** Argo Smart Routing is being integrated into Smart Shield. Configuration methods below remain valid; Terraform and IaC patterns unchanged. ### Infrastructure as Code (Terraform) ```hcl # terraform/argo.tf # Note: Use Cloudflare Terraform provider resource "cloudflare_argo" "example" { zone_id = var.zone_id smart_routing = "on" tiered_caching = "on" } variable "zone_id" { description = "Cloudflare Zone ID" type = string } output "argo_enabled" { value = cloudflare_argo.example.smart_routing description = "Argo Smart Routing status" } ``` ### Environment-Based Configuration ```typescript // config/argo.ts interface ArgoEnvironmentConfig { enabled: boolean; tieredCache: boolean; monitoring: { usageAlerts: boolean; threshold: number; }; } const configs: Record = { production: { enabled: true, tieredCache: true, monitoring: { usageAlerts: true, threshold: 1000, // GB }, }, staging: { enabled: true, tieredCache: false, monitoring: { usageAlerts: false, threshold: 100, // GB }, }, development: { enabled: false, tieredCache: false, monitoring: { usageAlerts: false, threshold: 0, }, }, }; export function getArgoConfig(env: string): ArgoEnvironmentConfig { return configs[env] || configs.development; } ``` ### Pulumi Configuration ```typescript // pulumi/argo.ts import * as cloudflare from '@pulumi/cloudflare'; const zone = new cloudflare.Zone('example-zone', { zone: 'example.com', plan: 'enterprise', }); const argoSettings = new cloudflare.Argo('argo-config', { zoneId: zone.id, smartRouting: 'on', tieredCaching: 'on', }); export const argoEnabled = argoSettings.smartRouting; export const zoneId = zone.id; ``` ## Billing Configuration Before enabling Argo Smart Routing, ensure billing is configured for the account: **Prerequisites:** 1. Valid payment method on file 2. Enterprise or higher plan 3. Zone must have billing enabled **Check Billing Status via Dashboard:** 1. Navigate to Account → Billing 2. Verify payment method configured 3. Check zone subscription status **Note:** Attempting to enable Argo without billing configured will result in `editable: false` in API responses. ## Environment Variable Setup **Required Environment Variables:** ```bash # .env CLOUDFLARE_API_TOKEN=your_api_token_here CLOUDFLARE_ZONE_ID=your_zone_id_here CLOUDFLARE_ACCOUNT_ID=your_account_id_here # Optional ARGO_ENABLED=true ARGO_TIERED_CACHE=true ``` **TypeScript Configuration Loader:** ```typescript // config/env.ts import { z } from 'zod'; const envSchema = z.object({ CLOUDFLARE_API_TOKEN: z.string().min(1), CLOUDFLARE_ZONE_ID: z.string().min(1), CLOUDFLARE_ACCOUNT_ID: z.string().min(1), ARGO_ENABLED: z.string().optional().default('false'), ARGO_TIERED_CACHE: z.string().optional().default('false'), }); export const env = envSchema.parse(process.env); export const argoConfig = { enabled: env.ARGO_ENABLED === 'true', tieredCache: env.ARGO_TIERED_CACHE === 'true', }; ``` ## CI/CD Integration **GitHub Actions Example:** ```yaml # .github/workflows/deploy-argo.yml name: Deploy Argo Configuration on: push: branches: [main] paths: - 'terraform/argo.tf' jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Terraform uses: hashicorp/setup-terraform@v2 - name: Terraform Init run: terraform init working-directory: ./terraform - name: Terraform Apply run: terraform apply -auto-approve working-directory: ./terraform env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} TF_VAR_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }} ``` ## Enterprise Preview Program For early access to Argo Smart Routing features and Smart Shield integration: **Eligibility:** - Enterprise plan customers - Active Cloudflare support contract - Production traffic >100GB/month **How to Join:** 1. Contact Cloudflare account team or support 2. Request Argo/Smart Shield preview access 3. Receive preview zone configuration **Preview Features:** - Enhanced analytics and reporting - Smart Shield DDoS integration - Advanced routing policies - Priority support for routing issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/argo-smart-routing/gotchas.md ================================================ ## Best Practices Summary **Smart Shield Note:** Argo Smart Routing evolving into Smart Shield. Best practices below remain applicable; monitor Cloudflare changelog for Smart Shield updates. 1. **Always check editability** before attempting to enable/disable Argo 2. **Set up billing notifications** to avoid unexpected costs 3. **Combine with Tiered Cache** for maximum performance benefit 4. **Use in production only** - disable for dev/staging to control costs 5. **Monitor analytics** - require 500+ requests in 48h for detailed metrics 6. **Handle errors gracefully** - check for billing, permissions, zone compatibility 7. **Test configuration changes** in staging before production 8. **Use TypeScript SDK** for type safety and better developer experience 9. **Implement retry logic** for API calls in production systems 10. **Document zone-specific settings** for team visibility ## Common Errors ### "Argo unavailable" **Problem:** API returns error "Argo Smart Routing is unavailable for this zone" **Cause:** Zone not eligible or billing not set up **Solution:** 1. Verify zone has Enterprise or higher plan 2. Check billing is configured in Account → Billing 3. Ensure payment method is valid and current 4. Contact Cloudflare support if eligibility unclear ### "Cannot enable/disable" **Problem:** API call succeeds but status remains unchanged, or `editable: false` in GET response **Cause:** Insufficient permissions or zone restrictions **Solution:** 1. Check API token has `Zone:Argo Smart Routing:Edit` permission 2. Verify `editable: true` in GET response before attempting PATCH 3. If `editable: false`, check: - Billing configured for account - Zone plan includes Argo (Enterprise+) - No active zone holds or suspensions - API token has correct scopes ### `editable: false` Error **Problem:** GET request returns `"editable": false`, preventing enable/disable **Cause:** Zone-level restrictions from billing, plan, or permissions **Solution Pattern:** ```typescript const status = await client.argo.smartRouting.get({ zone_id: zoneId }); if (!status.editable) { // Don't attempt to modify - will fail console.error('Cannot modify Argo settings:'); console.error('- Check billing is configured'); console.error('- Verify zone has Enterprise+ plan'); console.error('- Confirm API token has Edit permission'); throw new Error('Argo is not editable for this zone'); } // Safe to proceed with enable/disable await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }); ``` ### Rate Limiting **Problem:** `429 Too Many Requests` error from API **Cause:** Exceeded API rate limits (typically 1200 requests per 5 minutes) **Solution:** ```typescript import { RateLimitError } from 'cloudflare'; try { await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }); } catch (error) { if (error instanceof RateLimitError) { const retryAfter = error.response?.headers.get('retry-after'); console.log(`Rate limited. Retry after ${retryAfter} seconds`); // Implement exponential backoff await new Promise(resolve => setTimeout(resolve, (retryAfter || 60) * 1000)); // Retry request } } ``` ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | Min requests for analytics | 500 in 48h | For detailed metrics via GraphQL | | Zones supported | Enterprise+ | Check zone plan in dashboard | | Billing requirement | Must be configured | Before enabling; verify payment method | | API rate limit | 1200 req / 5 min | Per API token across all endpoints | | Spectrum apps | No hard limit | Each app can enable Argo independently | | Traffic counting | Proxied only | Only orange-clouded DNS records count | | DDoS/WAF exemption | Yes | Mitigated traffic excluded from billing | | Analytics latency | 1-5 minutes | Real-time metrics not available | ## Additional Resources - [Official Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/) - [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/) - [API Authentication](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) - [Cloudflare TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript) - [Cloudflare Python SDK](https://github.com/cloudflare/cloudflare-python) ================================================ FILE: skills/.curated/cloudflare-deploy/references/argo-smart-routing/patterns.md ================================================ # Integration Patterns ## Enable Argo + Tiered Cache ```typescript async function enableOptimalPerformance(client: Cloudflare, zoneId: string) { await Promise.all([ client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }), client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }), ]); } ``` **Flow:** Visitor → Edge (Lower-Tier) → [Cache Miss] → Upper-Tier → [Cache Miss + Argo] → Origin **Impact:** Argo ~30% latency reduction + Tiered Cache 50-80% origin offload ## Usage Analytics (GraphQL) ```graphql query ArgoAnalytics($zoneTag: string!) { viewer { zones(filter: { zoneTag: $zoneTag }) { httpRequestsAdaptiveGroups(limit: 1000) { sum { argoBytes, bytes } } } } } ``` **Billing:** ~$0.10/GB. DDoS-mitigated and WAF-blocked traffic NOT charged. ## Spectrum TCP Integration Enable Argo for non-HTTP traffic (databases, game servers, IoT): ```typescript // Update existing app await client.spectrum.apps.update(appId, { zone_id: zoneId, argo_smart_routing: true }); // Create new app with Argo await client.spectrum.apps.create({ zone_id: zoneId, dns: { type: 'CNAME', name: 'tcp.example.com' }, origin_direct: ['tcp://origin.example.com:3306'], protocol: 'tcp/3306', argo_smart_routing: true, }); ``` **Use cases:** MySQL/PostgreSQL (3306/5432), game servers, MQTT (1883), SSH (22) ## Pre-Flight Validation ```typescript async function validateArgoEligibility(client: Cloudflare, zoneId: string) { const status = await client.argo.smartRouting.get({ zone_id: zoneId }); const zone = await client.zones.get({ zone_id: zoneId }); const issues: string[] = []; if (!status.editable) issues.push('Zone not editable'); if (['free', 'pro'].includes(zone.plan.legacy_id)) issues.push('Requires Business+ plan'); if (zone.status !== 'active') issues.push('Zone not active'); return { canEnable: issues.length === 0, issues }; } ``` ## Post-Enable Verification ```typescript async function verifyArgoEnabled(client: Cloudflare, zoneId: string): Promise { await new Promise(r => setTimeout(r, 2000)); // Wait for propagation const status = await client.argo.smartRouting.get({ zone_id: zoneId }); return status.value === 'on'; } ``` ## Full Setup Pattern ```typescript async function setupArgo(client: Cloudflare, zoneId: string) { // 1. Validate const { canEnable, issues } = await validateArgoEligibility(client, zoneId); if (!canEnable) throw new Error(issues.join(', ')); // 2. Enable both features await Promise.all([ client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }), client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }), ]); // 3. Verify const [argo, cache] = await Promise.all([ client.argo.smartRouting.get({ zone_id: zoneId }), client.argo.tieredCaching.get({ zone_id: zoneId }), ]); return { argo: argo.value === 'on', tieredCache: cache.value === 'on' }; } ``` **When to combine:** High-traffic sites (>1TB/mo), global users, cacheable content. ================================================ FILE: skills/.curated/cloudflare-deploy/references/bindings/README.md ================================================ # Cloudflare Bindings Skill Reference Expert guidance on Cloudflare Workers Bindings - the runtime APIs that connect Workers to Cloudflare platform resources. ## What Are Bindings? Bindings are how Workers access Cloudflare resources (storage, compute, services) via the `env` object. They're configured in `wrangler.jsonc`, type-safe via TypeScript, and zero-overhead at runtime. ## Reading Order 1. **This file** - Binding catalog and selection guide 2. **[api.md](api.md)** - TypeScript types and env access patterns 3. **[configuration.md](configuration.md)** - Complete wrangler.jsonc examples 4. **[patterns.md](patterns.md)** - Best practices and common patterns 5. **[gotchas.md](gotchas.md)** - Critical pitfalls and troubleshooting ## Binding Catalog ### Storage Bindings | Binding | Use Case | Access Pattern | |---------|----------|----------------| | **KV** | Key-value cache, CDN-backed reads | `env.MY_KV.get(key)` | | **R2** | Object storage (S3-compatible) | `env.MY_BUCKET.get(key)` | | **D1** | SQL database (SQLite) | `env.DB.prepare(sql).all()` | | **Durable Objects** | Coordination, real-time state | `env.MY_DO.get(id)` | | **Vectorize** | Vector embeddings search | `env.VECTORIZE.query(vector)` | | **Queues** | Async message processing | `env.MY_QUEUE.send(msg)` | ### Compute Bindings | Binding | Use Case | Access Pattern | |---------|----------|----------------| | **Service** | Worker-to-Worker RPC | `env.MY_SERVICE.fetch(req)` | | **Workers AI** | LLM inference | `env.AI.run(model, input)` | | **Browser Rendering** | Headless Chrome | `env.BROWSER.fetch(url)` | ### Platform Bindings | Binding | Use Case | Access Pattern | |---------|----------|----------------| | **Analytics Engine** | Custom metrics | `env.ANALYTICS.writeDataPoint(data)` | | **mTLS** | Client certificates | `env.MY_CERT` (string) | | **Hyperdrive** | Database pooling | `env.HYPERDRIVE.connectionString` | | **Rate Limiting** | Request throttling | `env.RATE_LIMITER.limit(id)` | | **Workflows** | Long-running workflows | `env.MY_WORKFLOW.create()` | ### Configuration Bindings | Binding | Use Case | Access Pattern | |---------|----------|----------------| | **Environment Variables** | Non-sensitive config | `env.API_URL` (string) | | **Secrets** | Sensitive values | `env.API_KEY` (string) | | **Text/Data Blobs** | Static files | `env.MY_BLOB` (string) | | **WASM** | WebAssembly modules | `env.MY_WASM` (WebAssembly.Module) | ## Quick Selection Guide **Need persistent storage?** - Key-value < 25MB → **KV** - Files/objects → **R2** - Relational data → **D1** - Real-time coordination → **Durable Objects** **Need AI/compute?** - LLM inference → **Workers AI** - Scraping/PDFs → **Browser Rendering** - Call another Worker → **Service binding** **Need async processing?** - Background jobs → **Queues** **Need config?** - Public values → **Environment Variables** - Secrets → **Secrets** (never commit) ## Quick Start 1. **Add binding to wrangler.jsonc:** ```jsonc { "kv_namespaces": [ { "binding": "MY_KV", "id": "your-kv-id" } ] } ``` 2. **Generate types:** ```bash npx wrangler types ``` 3. **Access in Worker:** ```typescript export default { async fetch(request, env, ctx) { await env.MY_KV.put('key', 'value'); return new Response('OK'); } } ``` ## Type Safety Bindings are fully typed via `wrangler types`. See [api.md](api.md) for details. ## Limits - 64 bindings max per Worker (all types combined) - See [gotchas.md](gotchas.md) for per-binding limits ## Key Concepts **Zero-overhead access:** Bindings compiled into Worker, no network calls to access **Type-safe:** Full TypeScript support via `wrangler types` **Per-environment:** Different IDs for dev/staging/production **Secrets vs Vars:** Secrets encrypted at rest, never in config files ## See Also - [Cloudflare Docs: Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) - [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/bindings/api.md ================================================ # Bindings API Reference ## TypeScript Types Cloudflare generates binding types via `npx wrangler types`. This creates `.wrangler/types/runtime.d.ts` with your Env interface. ### Generated Env Interface After running `wrangler types`, TypeScript knows your bindings: ```typescript interface Env { // From wrangler.jsonc bindings MY_KV: KVNamespace; MY_BUCKET: R2Bucket; DB: D1Database; MY_SERVICE: Fetcher; AI: Ai; // From vars API_URL: string; // From secrets (set via wrangler secret put) API_KEY: string; } ``` ### Binding Types | Config | TypeScript Type | Package | |--------|-----------------|---------| | `kv_namespaces` | `KVNamespace` | `@cloudflare/workers-types` | | `r2_buckets` | `R2Bucket` | `@cloudflare/workers-types` | | `d1_databases` | `D1Database` | `@cloudflare/workers-types` | | `durable_objects.bindings` | `DurableObjectNamespace` | `@cloudflare/workers-types` | | `vectorize` | `VectorizeIndex` | `@cloudflare/workers-types` | | `queues.producers` | `Queue` | `@cloudflare/workers-types` | | `services` | `Fetcher` | `@cloudflare/workers-types` | | `ai` | `Ai` | `@cloudflare/workers-types` | | `browser` | `Fetcher` | `@cloudflare/workers-types` | | `analytics_engine_datasets` | `AnalyticsEngineDataset` | `@cloudflare/workers-types` | | `hyperdrive` | `Hyperdrive` | `@cloudflare/workers-types` | | `rate_limiting` | `RateLimit` | `@cloudflare/workers-types` | | `workflows` | `Workflow` | `@cloudflare/workers-types` | | `mtls_certificates` / `vars` / `text_blobs` / `data_blobs` | `string` | Built-in | | `wasm_modules` | `WebAssembly.Module` | Built-in | ## Accessing Bindings ### Method 1: fetch() Handler (Recommended) ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const value = await env.MY_KV.get('key'); return new Response(value); } } ``` **Why:** Type-safe, aligns with Workers API, supports ctx for waitUntil/passThroughOnException. ### Method 2: Hono Framework ```typescript import { Hono } from 'hono'; const app = new Hono<{ Bindings: Env }>(); app.get('/', async (c) => { const value = await c.env.MY_KV.get('key'); return c.json({ value }); }); export default app; ``` **Why:** c.env auto-typed, ergonomic for routing-heavy apps. ### Method 3: Module Workers (Legacy) ```typescript export async function handleRequest(request: Request, env: Env): Promise { const value = await env.MY_KV.get('key'); return new Response(value); } addEventListener('fetch', (event) => { // env not directly available - requires workarounds }); ``` **Avoid:** Use fetch() handler instead (Method 1). ## Type Generation Workflow ### Initial Setup ```bash # Install wrangler npm install -D wrangler # Generate types from wrangler.jsonc npx wrangler types ``` ### After Changing Bindings ```bash # Added/modified binding in wrangler.jsonc npx wrangler types # TypeScript now sees updated Env interface ``` **Note:** `wrangler types` outputs to `.wrangler/types/runtime.d.ts`. TypeScript picks this up automatically if `@cloudflare/workers-types` is in `tsconfig.json` `"types"` array. ## Key Binding Methods **KV:** ```typescript await env.MY_KV.get(key, { type: 'json' }); // text|json|arrayBuffer|stream await env.MY_KV.put(key, value, { expirationTtl: 3600 }); await env.MY_KV.delete(key); await env.MY_KV.list({ prefix: 'user:' }); ``` **R2:** ```typescript await env.BUCKET.get(key); await env.BUCKET.put(key, value); await env.BUCKET.delete(key); await env.BUCKET.list({ prefix: 'images/' }); ``` **D1:** ```typescript await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); await env.DB.batch([stmt1, stmt2]); ``` **Service:** ```typescript await env.MY_SERVICE.fetch(new Request('https://fake/path')); ``` **Workers AI:** ```typescript await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' }); ``` **Queues:** ```typescript await env.MY_QUEUE.send({ userId: 123, action: 'process' }); ``` **Durable Objects:** ```typescript const id = env.MY_DO.idFromName('user-123'); const stub = env.MY_DO.get(id); await stub.fetch(new Request('https://fake/increment')); ``` ## Runtime vs Build-Time Types | Type Source | When Generated | Use Case | |-------------|----------------|----------| | `@cloudflare/workers-types` | npm install | Base Workers APIs (Request, Response, etc.) | | `wrangler types` | After config change | Your specific bindings (Env interface) | **Install both:** ```bash npm install -D @cloudflare/workers-types npx wrangler types ``` ## Type Safety Best Practices 1. **Never use `any` for env:** ```typescript // ❌ BAD async fetch(request: Request, env: any) { } // ✅ GOOD async fetch(request: Request, env: Env) { } ``` 2. **Run wrangler types after config changes:** ```bash # After editing wrangler.jsonc npx wrangler types ``` 3. **Check generated types match config:** ```bash # View generated Env interface cat .wrangler/types/runtime.d.ts ``` ## See Also - [Workers Types Package](https://www.npmjs.com/package/@cloudflare/workers-types) - [Wrangler Types Command](https://developers.cloudflare.com/workers/wrangler/commands/#types) ================================================ FILE: skills/.curated/cloudflare-deploy/references/bindings/configuration.md ================================================ # Binding Configuration Reference ## Storage Bindings ```jsonc { "kv_namespaces": [{ "binding": "MY_KV", "id": "..." }], "r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }], "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "..." }], "durable_objects": { "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }, "vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }], "queues": { "producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }] } } ``` **Create commands:** ```bash npx wrangler kv namespace create MY_KV npx wrangler r2 bucket create my-bucket npx wrangler d1 create my-db npx wrangler vectorize create my-index --dimensions=768 --metric=cosine npx wrangler queues create my-queue # List existing resources npx wrangler kv namespace list npx wrangler r2 bucket list npx wrangler d1 list npx wrangler vectorize list npx wrangler queues list ``` ## Compute Bindings ```jsonc { "services": [{ "binding": "MY_SERVICE", "service": "other-worker", "environment": "production" // Optional: target specific env }], "ai": { "binding": "AI" }, "browser": { "binding": "BROWSER" }, "workflows": [{ "binding": "MY_WORKFLOW", "name": "my-workflow" }] } ``` **Create workflows:** ```bash npx wrangler workflows create my-workflow ``` ## Platform Bindings ```jsonc { "analytics_engine_datasets": [{ "binding": "ANALYTICS" }], "mtls_certificates": [{ "binding": "MY_CERT", "certificate_id": "..." }], "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "..." }], "unsafe": { "bindings": [{ "name": "RATE_LIMITER", "type": "ratelimit", "namespace_id": "..." }] } } ``` ## Configuration Bindings ```jsonc { "vars": { "API_URL": "https://api.example.com", "MAX_RETRIES": "3" }, "text_blobs": { "MY_TEXT": "./data/template.html" }, "data_blobs": { "MY_DATA": "./data/config.bin" }, "wasm_modules": { "MY_WASM": "./build/module.wasm" } } ``` **Secrets (never in config):** ```bash npx wrangler secret put API_KEY ``` ## Environment-Specific Configuration ```jsonc { "name": "my-worker", "vars": { "ENV": "production" }, "kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }], "env": { "staging": { "vars": { "ENV": "staging" }, "kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }] } } } ``` **Deploy:** ```bash npx wrangler deploy # Production npx wrangler deploy --env staging ``` ## Local Development ```jsonc { "kv_namespaces": [{ "binding": "MY_KV", "id": "prod-id", "preview_id": "dev-id" // Used in wrangler dev }] } ``` **Or use remote:** ```bash npx wrangler dev --remote # Uses production bindings ``` ## Complete Example ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-app", "main": "src/index.ts", "compatibility_date": "2025-01-01", "vars": { "API_URL": "https://api.example.com" }, "kv_namespaces": [{ "binding": "CACHE", "id": "abc123" }], "r2_buckets": [{ "binding": "ASSETS", "bucket_name": "my-assets" }], "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }], "services": [{ "binding": "AUTH", "service": "auth-worker" }], "ai": { "binding": "AI" } } ``` ## Binding-Specific Configuration ### Durable Objects with Class Export ```jsonc { "durable_objects": { "bindings": [ { "name": "COUNTER", "class_name": "Counter", "script_name": "my-worker" } ] } } ``` ```typescript // In same Worker or script_name Worker export class Counter { constructor(private state: DurableObjectState, private env: Env) {} async fetch(request: Request) { /* ... */ } } ``` ### Queue Consumers ```jsonc { "queues": { "producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }], "consumers": [{ "queue": "my-queue", "max_batch_size": 10 }] } } ``` Queue consumer handler: `export default { async queue(batch, env) { /* process batch.messages */ } }` ## Key Points - **64 binding limit** (all types combined) - **Secrets**: Always use `wrangler secret put`, never commit - **Types**: Run `npx wrangler types` after config changes - **Environments**: Use `env` field for staging/production variants - **Development**: Use `preview_id` or `--remote` flag - **IDs vs Names**: Some bindings use `id` (KV, D1), others use `name` (R2, Queues) ## See Also - [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/bindings/gotchas.md ================================================ # Binding Gotchas and Troubleshooting ## Critical: Global Scope Mutation ### ❌ THE #1 GOTCHA: Caching env in Global Scope ```typescript // ❌ DANGEROUS - env cached at deploy time const apiKey = env.API_KEY; // ERROR: env not available in global scope export default { async fetch(request: Request, env: Env) { // Uses undefined or stale value! } } ``` **Why it breaks:** - `env` not available in global scope - If using workarounds, secrets may not update without redeployment - Leads to "Cannot read property 'X' of undefined" errors **✅ Always access env per-request:** ```typescript export default { async fetch(request: Request, env: Env) { const apiKey = env.API_KEY; // Fresh every request } } ``` ## Common Errors ### "env.MY_KV is undefined" **Cause:** Name mismatch or not configured **Solution:** Check wrangler.jsonc (case-sensitive), run `npx wrangler types`, verify `npx wrangler kv namespace list` ### "Property 'MY_KV' does not exist on type 'Env'" **Cause:** Types not generated **Solution:** `npx wrangler types` ### "preview_id is required for --remote" **Cause:** Missing preview binding **Solution:** Add `"preview_id": "dev-id"` or use `npx wrangler dev` (local mode) ### "Secret updated but Worker still uses old value" **Cause:** Cached in global scope or not redeployed **Solution:** Avoid global caching, redeploy after secret change ### "KV get() returns null for existing key" **Cause:** Eventual consistency (60s), wrong namespace, wrong environment **Solution:** ```bash # Check key exists npx wrangler kv key get --binding=MY_KV "your-key" # Verify namespace ID npx wrangler kv namespace list # Check environment npx wrangler deployments list ``` ### "D1 database not found" **Solution:** `npx wrangler d1 list`, verify ID in wrangler.jsonc ### "Service binding returns 'No such service'" **Cause:** Target Worker not deployed, name mismatch, environment mismatch **Solution:** ```bash # List deployed Workers npx wrangler deployments list --name=target-worker # Check service binding config cat wrangler.jsonc | grep -A2 services # Deploy target first cd ../target-worker && npx wrangler deploy ``` ### "Rate limit exceeded" on KV writes **Cause:** >1 write/second per key **Solution:** Use different keys, Durable Objects, or Queues ## Type Safety Gotchas ### Missing @cloudflare/workers-types **Error:** `Cannot find name 'Request'` **Solution:** `npm install -D @cloudflare/workers-types`, add to tsconfig.json `"types"` ### Binding Type Mismatches ```typescript // ❌ Wrong - KV returns string | null const value: string = await env.MY_KV.get('key'); // ✅ Handle null const value = await env.MY_KV.get('key'); if (!value) return new Response('Not found', { status: 404 }); ``` ## Environment Gotchas ### Wrong Environment Deployed **Solution:** Check `npx wrangler deployments list`, use `--env` flag ### Secrets Not Per-Environment **Solution:** Set per environment: `npx wrangler secret put API_KEY --env staging` ## Development Gotchas **wrangler dev vs deploy:** - dev: Uses `preview_id` or local bindings, secrets not available - deploy: Uses production `id`, secrets available **Access secrets in dev:** `npx wrangler dev --remote` **Persist local data:** `npx wrangler dev --persist` ## Performance Gotchas ### Sequential Binding Calls ```typescript // ❌ Slow const user = await env.DB.prepare('...').first(); const config = await env.MY_KV.get('config'); // ✅ Parallel const [user, config] = await Promise.all([ env.DB.prepare('...').first(), env.MY_KV.get('config') ]); ``` ## Security Gotchas **❌ Secrets in logs:** `console.log('Key:', env.API_KEY)` - visible in dashboard **✅** `console.log('Key:', env.API_KEY ? '***' : 'missing')` **❌ Exposing env:** `return Response.json(env)` - exposes all bindings **✅** Never return env object in responses ## Limits Reference | Resource | Limit | Impact | Plan | |----------|-------|--------|------| | **Bindings per Worker** | 64 total | All binding types combined | All | | **Environment variables** | 64 max, 5KB each | Per Worker | All | | **Secret size** | 1KB | Per secret | All | | **KV key size** | 512 bytes | UTF-8 encoded | All | | **KV value size** | 25 MB | Per value | All | | **KV writes per key** | 1/second | Per key; exceeding = 429 error | All | | **KV list() results** | 1000 keys | Per call; use cursor for more | All | | **KV operations** | 1000 reads/day | Free tier only | Free | | **R2 object size** | 5 TB | Per object | All | | **R2 operations** | 1M Class A/month free | Writes | All | | **D1 database size** | 10 GB | Per database | All | | **D1 rows per query** | 100,000 | Result set limit | All | | **D1 databases** | 10 | Free tier | Free | | **Queue batch size** | 100 messages | Per consumer batch | All | | **Queue message size** | 128 KB | Per message | All | | **Service binding calls** | Unlimited | Counts toward CPU time | All | | **Durable Objects** | 1M requests/month free | First 1M | Free | ## Debugging Tips ```bash # Check configuration npx wrangler deploy --dry-run # Validate config without deploying npx wrangler kv namespace list # List KV namespaces npx wrangler secret list # List secrets (not values) npx wrangler deployments list # Recent deployments # Inspect bindings npx wrangler kv key list --binding=MY_KV npx wrangler kv key get --binding=MY_KV "key-name" npx wrangler r2 object get my-bucket/file.txt npx wrangler d1 execute my-db --command="SELECT * FROM sqlite_master" # Test locally npx wrangler dev # Local mode npx wrangler dev --remote # Production bindings npx wrangler dev --persist # Persist data across restarts # Verify types npx wrangler types cat .wrangler/types/runtime.d.ts | grep "interface Env" # Debug specific binding issues npx wrangler tail # Stream logs in real-time npx wrangler tail --format=pretty # Formatted logs ``` ## See Also - [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/) - [Wrangler Commands](https://developers.cloudflare.com/workers/wrangler/commands/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/bindings/patterns.md ================================================ # Binding Patterns and Best Practices ## Service Binding Patterns ### RPC via Service Bindings ```typescript // auth-worker export default { async fetch(request: Request, env: Env) { const token = request.headers.get('Authorization'); return new Response(JSON.stringify({ valid: await validateToken(token) })); } } // api-worker const response = await env.AUTH_SERVICE.fetch( new Request('https://fake-host/validate', { headers: { 'Authorization': token } }) ); ``` **Why RPC?** Zero latency (same datacenter), no DNS, free, type-safe. **HTTP vs Service:** ```typescript // ❌ HTTP (slow, paid, cross-region latency) await fetch('https://auth-worker.example.com/validate'); // ✅ Service binding (fast, free, same isolate) await env.AUTH_SERVICE.fetch(new Request('https://fake-host/validate')); ``` **URL doesn't matter:** Service bindings ignore hostname/protocol, routing happens via binding name. ### Typed Service RPC ```typescript // shared-types.ts export interface AuthRequest { token: string; } export interface AuthResponse { valid: boolean; userId?: string; } // auth-worker export default { async fetch(request: Request): Promise { const body: AuthRequest = await request.json(); const response: AuthResponse = { valid: true, userId: '123' }; return Response.json(response); } } // api-worker const response = await env.AUTH_SERVICE.fetch( new Request('https://fake/validate', { method: 'POST', body: JSON.stringify({ token } satisfies AuthRequest) }) ); const data: AuthResponse = await response.json(); ``` ## Secrets Management ```bash # Set secret npx wrangler secret put API_KEY cat api-key.txt | npx wrangler secret put API_KEY npx wrangler secret put API_KEY --env staging ``` ```typescript // Use secret const response = await fetch('https://api.example.com', { headers: { 'Authorization': `Bearer ${env.API_KEY}` } }); ``` **Never commit secrets:** ```jsonc // ❌ NEVER { "vars": { "API_KEY": "sk_live_abc123" } } ``` ## Testing with Mock Bindings ### Vitest Mock ```typescript import { vi } from 'vitest'; const mockKV: KVNamespace = { get: vi.fn(async (key) => key === 'test' ? 'value' : null), put: vi.fn(async () => {}), delete: vi.fn(async () => {}), list: vi.fn(async () => ({ keys: [], list_complete: true, cursor: '' })), getWithMetadata: vi.fn(), } as unknown as KVNamespace; const mockEnv: Env = { MY_KV: mockKV }; const mockCtx: ExecutionContext = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), }; const response = await worker.fetch( new Request('http://localhost/test'), mockEnv, mockCtx ); ``` ## Binding Access Patterns ### Lazy Access ```typescript // ✅ Access only when needed if (url.pathname === '/cached') { const cached = await env.MY_KV.get('data'); if (cached) return new Response(cached); } ``` ### Parallel Access ```typescript // ✅ Parallelize independent calls const [user, config, cache] = await Promise.all([ env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(), env.MY_KV.get('config'), env.CACHE.get('data') ]); ``` ## Storage Selection ### KV: CDN-Backed Reads ```typescript const config = await env.MY_KV.get('app-config', { type: 'json' }); ``` **Use when:** Read-heavy, <25MB, global distribution, eventual consistency OK **Latency:** <10ms reads (cached), writes eventually consistent (60s) ### D1: Relational Queries ```typescript const results = await env.DB.prepare(` SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id `).all(); ``` **Use when:** Relational data, JOINs, ACID transactions **Limits:** 10GB database size, 100k rows per query ### R2: Large Objects ```typescript const object = await env.MY_BUCKET.get('large-file.zip'); return new Response(object.body); ``` **Use when:** Files >25MB, S3-compatible API needed **Limits:** 5TB per object, unlimited storage ### Durable Objects: Coordination ```typescript const id = env.COUNTER.idFromName('global'); const stub = env.COUNTER.get(id); await stub.fetch(new Request('https://fake/increment')); ``` **Use when:** Strong consistency, real-time coordination, WebSocket state **Guarantees:** Single-threaded execution, transactional storage ## Anti-Patterns **❌ Hardcoding credentials:** `const apiKey = 'sk_live_abc123'` **✅** `npx wrangler secret put API_KEY` **❌ Using REST API:** `fetch('https://api.cloudflare.com/.../kv/...')` **✅** `env.MY_KV.get('key')` **❌ Polling storage:** `setInterval(() => env.KV.get('config'), 1000)` **✅** Use Durable Objects for real-time state **❌ Large data in vars:** `{ "vars": { "HUGE_CONFIG": "..." } }` (5KB max) **✅** `env.MY_KV.put('config', data)` **❌ Caching env globally:** `const apiKey = env.API_KEY` outside fetch() **✅** Access `env.API_KEY` per-request inside fetch() ## See Also - [Service Bindings Docs](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) - [Miniflare Testing](https://miniflare.dev/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/bot-management/README.md ================================================ # Cloudflare Bot Management Enterprise-grade bot detection, protection, and mitigation using ML/heuristics, bot scores, JavaScript detections, and verified bot handling. ## Overview Bot Management provides multi-tier protection: - **Free (Bot Fight Mode)**: Auto-blocks definite bots, no config - **Pro/Business (Super Bot Fight Mode)**: Configurable actions, static resource protection, analytics groupings - **Enterprise (Bot Management)**: Granular 1-99 scores, WAF integration, JA3/JA4 fingerprinting, Workers API, Advanced Analytics ## Quick Start ```txt # Dashboard: Security > Bots # Enterprise: Deploy rule template (cf.bot_management.score eq 1 and not cf.bot_management.verified_bot) → Block (cf.bot_management.score le 29 and not cf.bot_management.verified_bot) → Managed Challenge ``` ## What Do You Need? ```txt ├─ Initial setup → configuration.md │ ├─ Free tier → "Bot Fight Mode" │ ├─ Pro/Business → "Super Bot Fight Mode" │ └─ Enterprise → "Bot Management for Enterprise" ├─ Workers API integration → api.md ├─ WAF rules → patterns.md ├─ Debugging → gotchas.md └─ Analytics → api.md#bot-analytics ``` ## Reading Order | Task | Files to Read | |------|---------------| | Enable bot protection | README → configuration.md | | Workers bot detection | README → api.md | | WAF rule templates | README → patterns.md | | Debug bot issues | gotchas.md | | Advanced analytics | api.md#bot-analytics | ## Core Concepts **Bot Scores**: 1-99 (1 = definitely automated, 99 = definitely human). Threshold: <30 indicates bot traffic. Enterprise gets granular 1-99; Pro/Business get groupings only. **Detection Engines**: Heuristics (known fingerprints, assigns score=1), ML (majority of detections, supervised learning on billions of requests), Anomaly Detection (optional, baseline traffic analysis), JavaScript Detections (headless browser detection). **Verified Bots**: Allowlisted good bots (search engines, AI crawlers) verified via reverse DNS or Web Bot Auth. Access via `cf.bot_management.verified_bot` or `cf.verified_bot_category`. ## Platform Limits | Plan | Bot Scores | JA3/JA4 | Custom Rules | Analytics Retention | |------|------------|---------|--------------|---------------------| | Free | No (auto-block only) | No | 5 | N/A (no analytics) | | Pro/Business | Groupings only | No | 20/100 | 30 days (72h at a time) | | Enterprise | 1-99 granular | Yes | 1,000+ | 30 days (1 week at a time) | ## Basic Patterns ```typescript // Workers: Check bot score export default { async fetch(request: Request): Promise { const botScore = request.cf?.botManagement?.score; if (botScore && botScore < 30 && !request.cf?.botManagement?.verifiedBot) { return new Response('Bot detected', { status: 403 }); } return fetch(request); } }; ``` ```txt # WAF: Block definite bots (cf.bot_management.score eq 1 and not cf.bot_management.verified_bot) # WAF: Protect sensitive endpoints (cf.bot_management.score lt 50 and http.request.uri.path in {"/login" "/checkout"} and not cf.bot_management.verified_bot) ``` ## In This Reference - [configuration.md](./configuration.md) - Product tiers, WAF rule setup, JavaScript Detections, ML auto-updates - [api.md](./api.md) - Workers BotManagement interface, WAF fields, JA4 Signals - [patterns.md](./patterns.md) - E-commerce, API protection, mobile app allowlisting, SEO-friendly handling - [gotchas.md](./gotchas.md) - False positives/negatives, score=0 issues, JSD limitations, CSP requirements ## See Also - [waf](../waf/) - WAF custom rules for bot enforcement - [workers](../workers/) - Workers request.cf.botManagement API - [api-shield](../api-shield/) - API-specific bot protection ================================================ FILE: skills/.curated/cloudflare-deploy/references/bot-management/api.md ================================================ # Bot Management API ## Workers: BotManagement Interface ```typescript interface BotManagement { score: number; // 1-99 (Enterprise), 0 if not computed verifiedBot: boolean; // Is verified bot staticResource: boolean; // Serves static resource ja3Hash: string; // JA3 fingerprint (Enterprise, HTTPS only) ja4: string; // JA4 fingerprint (Enterprise, HTTPS only) jsDetection?: { passed: boolean; // Passed JS detection (if enabled) }; detectionIds: number[]; // Heuristic detection IDs corporateProxy?: boolean; // From corporate proxy (Enterprise) } // DEPRECATED: Use botManagement.score instead // request.cf.clientTrustScore (legacy, duplicate of botManagement.score) // Access via request.cf import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; export default { async fetch(request: Request): Promise { const cf = request.cf as IncomingRequestCfProperties | undefined; const botMgmt = cf?.botManagement; if (!botMgmt) return fetch(request); if (botMgmt.verifiedBot) return fetch(request); // Allow verified bots if (botMgmt.score === 1) return new Response('Blocked', { status: 403 }); if (botMgmt.score < 30) return new Response('Challenge required', { status: 429 }); return fetch(request); } }; ``` ## WAF Fields Reference ```txt # Score fields cf.bot_management.score # 0-99 (0 = not computed) cf.bot_management.verified_bot # boolean cf.bot_management.static_resource # boolean cf.bot_management.ja3_hash # string (Enterprise) cf.bot_management.ja4 # string (Enterprise) cf.bot_management.detection_ids # array cf.bot_management.js_detection.passed # boolean cf.bot_management.corporate_proxy # boolean (Enterprise) cf.verified_bot_category # string # Workers equivalent request.cf.botManagement.score request.cf.botManagement.verifiedBot request.cf.botManagement.ja3Hash request.cf.botManagement.ja4 request.cf.botManagement.jsDetection.passed request.cf.verifiedBotCategory ``` ## JA4 Signals (Enterprise) ```typescript import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; interface JA4Signals { // Ratios (0.0-1.0) heuristic_ratio_1h?: number; // Fraction flagged by heuristics browser_ratio_1h?: number; // Fraction from real browsers cache_ratio_1h?: number; // Fraction hitting cache h2h3_ratio_1h?: number; // Fraction using HTTP/2 or HTTP/3 // Ranks (relative position in distribution) uas_rank_1h?: number; // User-Agent diversity rank paths_rank_1h?: number; // Path diversity rank reqs_rank_1h?: number; // Request volume rank ips_rank_1h?: number; // IP diversity rank // Quantiles (0.0-1.0, percentile in distribution) reqs_quantile_1h?: number; // Request volume quantile ips_quantile_1h?: number; // IP count quantile } export default { async fetch(request: Request): Promise { const cf = request.cf as IncomingRequestCfProperties | undefined; const ja4Signals = cf?.ja4Signals as JA4Signals | undefined; if (!ja4Signals) return fetch(request); // Not available for HTTP or Worker routing // Check for anomalous behavior // High heuristic_ratio or low browser_ratio = suspicious const heuristicRatio = ja4Signals.heuristic_ratio_1h ?? 0; const browserRatio = ja4Signals.browser_ratio_1h ?? 0; if (heuristicRatio > 0.5 || browserRatio < 0.3) { return new Response('Suspicious traffic', { status: 403 }); } return fetch(request); } }; ``` ## Common Patterns See [patterns.md](./patterns.md) for Workers examples: mobile app allowlisting, corporate proxy exemption, datacenter detection, conditional delay, and more. ## Bot Analytics ### Access Locations - Dashboard: Security > Bots (old) or Security > Analytics > Bot analysis (new) - GraphQL API for programmatic access - Security Events & Security Analytics - Logpush/Logpull ### Available Data - **Enterprise BM**: Bot scores (1-99), bot score source, distribution - **Pro/Business**: Bot groupings (automated, likely automated, likely human) - Top attributes: IPs, paths, user agents, countries - Detection sources: Heuristics, ML, AD, JSD - Verified bot categories ### Time Ranges - **Enterprise BM**: Up to 1 week at a time, 30 days history - **Pro/Business**: Up to 72 hours at a time, 30 days history - Real-time in most cases, adaptive sampling (1-10% depending on volume) ## Logpush Fields ```txt BotScore # 1-99 or 0 if not computed BotScoreSrc # Detection engine (ML, Heuristics, etc.) BotTags # Classification tags BotDetectionIDs # Heuristic detection IDs ``` **BotScoreSrc values:** - `"Heuristics"` - Known fingerprint - `"Machine Learning"` - ML model - `"Anomaly Detection"` - Baseline anomaly - `"JS Detection"` - JavaScript check - `"Cloudflare Service"` - Zero Trust - `"Not Computed"` - Score = 0 Access via Logpush (stream to cloud storage/SIEM), Logpull (API to fetch logs), or GraphQL API (query analytics data). ## Testing with Miniflare Miniflare provides mock botManagement data for local development: **Default values:** - `score: 99` (human) - `verifiedBot: false` - `corporateProxy: false` - `ja3Hash: "25b4882c2bcb50cd6b469ff28c596742"` - `staticResource: false` - `detectionIds: []` **Override in tests:** ```typescript import { getPlatformProxy } from 'wrangler'; const { cf, dispose } = await getPlatformProxy(); // cf.botManagement is frozen mock object expect(cf.botManagement.score).toBe(99); ``` For custom test data, mock request.cf in your test setup. ================================================ FILE: skills/.curated/cloudflare-deploy/references/bot-management/configuration.md ================================================ # Bot Management Configuration ## Product Tiers **Note:** Dashboard paths differ between old and new UI: - **New:** Security > Settings > Filter "Bot traffic" - **Old:** Security > Bots Both UIs access same settings. ### Bot Score Groupings (Pro/Business) Pro/Business users see bot score groupings instead of granular 1-99 scores: | Score | Grouping | Meaning | |-------|----------|---------| | 0 | Not computed | Bot Management didn't run | | 1 | Automated | Definite bot (heuristic match) | | 2-29 | Likely automated | Probably bot (ML detection) | | 30-99 | Likely human | Probably human | | N/A | Verified bot | Allowlisted good bot | Enterprise plans get granular 1-99 scores for custom thresholds. ### Bot Fight Mode (Free) - Auto-blocks definite bots (score=1), excludes verified bots by default - JavaScript Detections always enabled, no configuration options ### Super Bot Fight Mode (Pro/Business) ```txt Dashboard: Security > Bots > Configure - Definitely automated: Block/Challenge - Likely automated: Challenge/Allow - Verified bots: Allow (recommended) - Static resource protection: ON (may block mail clients) - JavaScript Detections: Optional ``` ### Bot Management for Enterprise ```txt Dashboard: Security > Bots > Configure > Auto-updates: ON (recommended) # Template 1: Block definite bots (cf.bot_management.score eq 1 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource) Action: Block # Template 2: Challenge likely bots (cf.bot_management.score ge 2 and cf.bot_management.score le 29 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource) Action: Managed Challenge ``` ## JavaScript Detections Setup ### Enable via Dashboard ```txt Security > Bots > Configure Bot Management > JS Detections: ON Update CSP: script-src 'self' /cdn-cgi/challenge-platform/; ``` ### Manual JS Injection (API) ```html ``` **Use API for**: Selective deployment on specific pages **Don't combine**: Zone-wide toggle + manual injection ### WAF Rules for JSD ```txt # NEVER use on first page visit (needs HTML page first) (not cf.bot_management.js_detection.passed and http.request.uri.path eq "/api/user/create" and http.request.method eq "POST" and not cf.bot_management.verified_bot) Action: Managed Challenge (always use Managed Challenge, not Block) ``` ### Limitations - First request won't have JSD data (needs HTML page first) - Strips ETags from HTML responses - Not supported with CSP via `` tags - Websocket endpoints not supported - Native mobile apps won't pass - cf_clearance cookie: 15-minute lifespan, max 4096 bytes ## __cf_bm Cookie Cloudflare sets `__cf_bm` cookie to smooth bot scores across user sessions: - **Purpose:** Reduces false positives from score volatility - **Scope:** Per-domain, HTTP-only - **Lifespan:** Session duration - **Privacy:** No PII—only session classification - **Automatic:** No configuration required Bot scores for repeat visitors consider session history via this cookie. ## Static Resource Protection **File Extensions**: ico, jpg, png, jpeg, gif, css, js, tif, tiff, bmp, pict, webp, svg, svgz, class, jar, txt, csv, doc, docx, xls, xlsx, pdf, ps, pls, ppt, pptx, ttf, otf, woff, woff2, eot, eps, ejs, swf, torrent, midi, mid, m3u8, m4a, mp3, ogg, ts **Plus**: `/.well-known/` path (all files) ```txt # Exclude static resources from bot rules (cf.bot_management.score lt 30 and not cf.bot_management.static_resource) ``` **WARNING**: May block mail clients fetching static images ## JA3/JA4 Fingerprinting (Enterprise) ```txt # Block specific attack fingerprint (cf.bot_management.ja3_hash eq "8b8e3d5e3e8b3d5e") # Allow mobile app by fingerprint (cf.bot_management.ja4 eq "your_mobile_app_fingerprint") ``` Only available for HTTPS/TLS traffic. Missing for Worker-routed traffic or HTTP requests. ## Verified Bot Categories ```txt # Allow search engines only (cf.verified_bot_category eq "Search Engine Crawler") # Block AI crawlers (cf.verified_bot_category eq "AI Crawler") Action: Block # Or use dashboard: Security > Settings > Bot Management > Block AI Bots ``` | Category | String Value | Example | |----------|--------------|---------| | AI Crawler | `AI Crawler` | GPTBot, Claude-Web | | AI Assistant | `AI Assistant` | Perplexity-User, DuckAssistBot | | AI Search | `AI Search` | OAI-SearchBot | | Accessibility | `Accessibility` | Accessible Web Bot | | Academic Research | `Academic Research` | Library of Congress | | Advertising & Marketing | `Advertising & Marketing` | Google Adsbot | | Aggregator | `Aggregator` | Pinterest, Indeed | | Archiver | `Archiver` | Internet Archive, CommonCrawl | | Feed Fetcher | `Feed Fetcher` | RSS/Podcast updaters | | Monitoring & Analytics | `Monitoring & Analytics` | Uptime monitors | | Page Preview | `Page Preview` | Facebook/Slack link preview | | SEO | `Search Engine Optimization` | Google Lighthouse | | Security | `Security` | Vulnerability scanners | | Social Media Marketing | `Social Media Marketing` | Brandwatch | | Webhooks | `Webhooks` | Payment processors | | Other | `Other` | Uncategorized bots | ## Best Practices - **ML Auto-Updates**: Enable on Enterprise for latest models - **Start with Managed Challenge**: Test before blocking - **Always exclude verified bots**: Use `not cf.bot_management.verified_bot` - **Exempt corporate proxies**: For B2B traffic via `cf.bot_management.corporate_proxy` - **Use static resource exception**: Improves performance, reduces overhead ================================================ FILE: skills/.curated/cloudflare-deploy/references/bot-management/gotchas.md ================================================ # Bot Management Gotchas ## Common Errors ### "Bot Score = 0" **Cause:** Bot Management didn't run (internal Cloudflare request, Worker routing to zone (Orange-to-Orange), or request handled before BM (Redirect Rules, etc.)) **Solution:** Check request flow and ensure Bot Management runs in request lifecycle ### "JavaScript Detections Not Working" **Cause:** `js_detection.passed` always false or undefined due to: CSP headers don't allow `/cdn-cgi/challenge-platform/`, using on first page visit (needs HTML page first), ad blockers or disabled JS, JSD not enabled in dashboard, or using Block action (must use Managed Challenge) **Solution:** Add CSP header `Content-Security-Policy: script-src 'self' /cdn-cgi/challenge-platform/;` and ensure JSD is enabled with Managed Challenge action ### "False Positives (Legitimate Users Blocked)" **Cause:** Bot detection incorrectly flagging legitimate users **Solution:** Check Bot Analytics for affected IPs/paths, identify detection source (ML, Heuristics, etc.), create exception rule like `(cf.bot_management.score lt 30 and http.request.uri.path eq "/problematic-path")` with Action: Skip (Bot Management), or allowlist by IP/ASN/country ### "False Negatives (Bots Not Caught)" **Cause:** Bots bypassing detection **Solution:** Lower score threshold (30 → 50), enable JavaScript Detections, add JA3/JA4 fingerprinting rules, or use rate limiting as fallback ### "Verified Bot Blocked" **Cause:** Search engine bot blocked by WAF Managed Rules (not just Bot Management) **Solution:** Create WAF exception for specific rule ID and verify bot via reverse DNS ### "Yandex Bot Blocked During IP Update" **Cause:** Yandex updates bot IPs; new IPs unrecognized for 48h during propagation **Solution:** 1. Check Security Events for specific WAF rule ID blocking Yandex 2. Create WAF exception: ```txt (http.user_agent contains "YandexBot" and ip.src in {}) Action: Skip (WAF Managed Ruleset) ``` 3. Monitor Bot Analytics for 48h 4. Remove exception after propagation completes Issue resolves automatically after 48h. Contact Cloudflare Support if persists. ### "JA3/JA4 Missing" **Cause:** Non-HTTPS traffic, Worker routing traffic, Orange-to-Orange traffic via Worker, or Bot Management skipped **Solution:** JA3/JA4 only available for HTTPS/TLS traffic; check request routing **JA3/JA4 Not User-Unique:** Same browser/library version = same fingerprint - Don't use for user identification - Use for client profiling only - Fingerprints change with browser updates ## Bot Verification Methods Cloudflare verifies bots via: 1. **Reverse DNS (IP validation):** Traditional method—bot IP resolves to expected domain 2. **Web Bot Auth:** Modern cryptographic verification—faster propagation When `verifiedBot=true`, bot passed at least one method. **Inactive verified bots:** IPs removed after 24h of no traffic. ## Detection Engine Behavior | Engine | Score | Timing | Plan | Notes | |--------|-------|--------|------|-------| | Heuristics | Always 1 | Immediate | All | Known fingerprints—overrides ML | | ML | 1-99 | Immediate | All | Majority of detections | | Anomaly Detection | Influences | After baseline | Enterprise | Optional, baseline analysis | | JavaScript Detections | Pass/fail | After JS | Pro+ | Headless browser detection | | Cloudflare Service | N/A | N/A | Enterprise | Zero Trust internal source | **Priority:** Heuristics > ML—if heuristic matches, score=1 regardless of ML. ## Limits | Limit | Value | Notes | |-------|-------|-------| | Bot Score = 0 | Means not computed | Not score = 100 | | First request JSD data | May not be available | JSD data appears on subsequent requests | | Score accuracy | Not 100% guaranteed | False positives/negatives possible | | JSD on first HTML page visit | Not supported | Requires subsequent page load | | JSD requirements | JavaScript-enabled browser | Won't work with JS disabled or ad blockers | | JSD ETag stripping | Strips ETags from HTML responses | May affect caching behavior | | JSD CSP compatibility | Requires specific CSP | Not compatible with some CSP configurations | | JSD meta CSP tags | Not supported | Must use HTTP headers | | JSD WebSocket support | Not supported | WebSocket endpoints won't work with JSD | | JSD mobile app support | Native apps won't pass | Only works in browsers | | JA3/JA4 traffic type | HTTPS/TLS only | Not available for non-HTTPS traffic | | JA3/JA4 Worker routing | Missing for Worker-routed traffic | Check request routing | | JA3/JA4 uniqueness | Not unique per user | Shared by clients with same browser/library | | JA3/JA4 stability | Can change with updates | Browser/library updates affect fingerprints | | WAF custom rules (Free) | 5 | Varies by plan | | WAF custom rules (Pro) | 20 | Varies by plan | | WAF custom rules (Business) | 100 | Varies by plan | | WAF custom rules (Enterprise) | 1,000+ | Varies by plan | | Workers CPU time | Varies by plan | Applies to bot logic | | Bot Analytics sampling | 1-10% adaptive | High-volume zones sampled more aggressively | | Bot Analytics history | 30 days max | Historical data retention limit | | CSP requirements for JSD | Must allow `/cdn-cgi/challenge-platform/` | Required for JSD to function | ### Plan Restrictions | Feature | Free | Pro/Business | Enterprise | |---------|------|--------------|------------| | Granular scores (1-99) | No | No | Yes | | JA3/JA4 | No | No | Yes | | Anomaly Detection | No | No | Yes | | Corporate Proxy detection | No | No | Yes | | Verified bot categories | Limited | Limited | Full | | Custom WAF rules | 5 | 20/100 | 1,000+ | ================================================ FILE: skills/.curated/cloudflare-deploy/references/bot-management/patterns.md ================================================ # Bot Management Patterns ## E-commerce Protection ```txt # High security for checkout (cf.bot_management.score lt 50 and http.request.uri.path in {"/checkout" "/cart/add"} and not cf.bot_management.verified_bot and not cf.bot_management.corporate_proxy) Action: Managed Challenge ``` ## API Protection ```txt # Protect API with JS detection + score (http.request.uri.path matches "^/api/" and (cf.bot_management.score lt 30 or not cf.bot_management.js_detection.passed) and not cf.bot_management.verified_bot) Action: Block ``` ## SEO-Friendly Bot Handling ```txt # Allow search engine crawlers (cf.bot_management.score lt 30 and not cf.verified_bot_category in {"Search Engine Crawler"}) Action: Managed Challenge ``` ## Block AI Scrapers ```txt # Block training crawlers only (allow AI assistants/search) (cf.verified_bot_category eq "AI Crawler") Action: Block # Block all AI-related bots (training + assistants + search) (cf.verified_bot_category in {"AI Crawler" "AI Assistant" "AI Search"}) Action: Block # Allow AI Search, block AI Crawler and AI Assistant (cf.verified_bot_category in {"AI Crawler" "AI Assistant"}) Action: Block # Or use dashboard: Security > Settings > Bot Management > Block AI Bots ``` ## Rate Limiting by Bot Score ```txt # Stricter limits for suspicious traffic (cf.bot_management.score lt 50) Rate: 10 requests per 10 seconds (cf.bot_management.score ge 50) Rate: 100 requests per 10 seconds ``` ## Mobile App Allowlisting ```txt # Identify mobile app by JA3/JA4 (cf.bot_management.ja4 in {"fingerprint1" "fingerprint2"}) Action: Skip (all remaining rules) ``` ## Datacenter Detection ```typescript import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; // Low score + not corporate proxy = likely datacenter bot export default { async fetch(request: Request): Promise { const cf = request.cf as IncomingRequestCfProperties | undefined; const botMgmt = cf?.botManagement; if (botMgmt?.score && botMgmt.score < 30 && !botMgmt.corporateProxy && !botMgmt.verifiedBot) { return new Response('Datacenter traffic blocked', { status: 403 }); } return fetch(request); } }; ``` ## Conditional Delay (Tarpit) ```typescript import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; // Add delay proportional to bot suspicion export default { async fetch(request: Request): Promise { const cf = request.cf as IncomingRequestCfProperties | undefined; const botMgmt = cf?.botManagement; if (botMgmt?.score && botMgmt.score < 50 && !botMgmt.verifiedBot) { // Delay: 0-2 seconds for scores 50-0 const delayMs = Math.max(0, (50 - botMgmt.score) * 40); await new Promise(r => setTimeout(r, delayMs)); } return fetch(request); } }; ``` ## Layered Defense ```txt 1. Bot Management (score-based) 2. JavaScript Detections (for JS-capable clients) 3. Rate Limiting (fallback protection) 4. WAF Managed Rules (OWASP, etc.) ``` ## Progressive Enhancement ```txt Public content: High threshold (score < 10) Authenticated: Medium threshold (score < 30) Sensitive: Low threshold (score < 50) + JSD ``` ## Zero Trust for Bots ```txt 1. Default deny (all scores < 30) 2. Allowlist verified bots 3. Allowlist mobile apps (JA3/JA4) 4. Allowlist corporate proxies 5. Allowlist static resources ``` ## Workers: Score + JS Detection ```typescript import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; export default { async fetch(request: Request): Promise { const cf = request.cf as IncomingRequestCfProperties | undefined; const botMgmt = cf?.botManagement; const url = new URL(request.url); if (botMgmt?.staticResource) return fetch(request); // Skip static // API endpoints: require JS detection + good score if (url.pathname.startsWith('/api/')) { const jsDetectionPassed = botMgmt?.jsDetection?.passed ?? false; const score = botMgmt?.score ?? 100; if (!jsDetectionPassed || score < 30) { return new Response('Unauthorized', { status: 401 }); } } return fetch(request); } }; ``` ## Rate Limiting by JWT Claim + Bot Score ```txt # Enterprise: Combine bot score with JWT validation Rate limiting > Custom rules - Field: lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub") - Matches: user ID claim - Additional condition: cf.bot_management.score lt 50 ``` ## WAF Integration Points - **WAF Custom Rules**: Primary enforcement mechanism - **Rate Limiting Rules**: Bot score as dimension, stricter limits for low scores - **Transform Rules**: Pass score to origin via custom header - **Workers**: Programmatic bot logic, custom scoring algorithms - **Page Rules / Configuration Rules**: Zone-level overrides, path-specific settings ## See Also - [gotchas.md](./gotchas.md) - Common errors, false positives/negatives, limitations ================================================ FILE: skills/.curated/cloudflare-deploy/references/browser-rendering/README.md ================================================ # Cloudflare Browser Rendering Skill Reference **Description**: Expert knowledge for Cloudflare Browser Rendering - control headless Chrome on Cloudflare's global network for browser automation, screenshots, PDFs, web scraping, testing, and content generation. **When to use**: Any task involving Cloudflare Browser Rendering including: taking screenshots, generating PDFs, web scraping, browser automation, testing web applications, extracting structured data, capturing page metrics, or automating browser interactions. ## Decision Tree ### REST API vs Workers Bindings **Use REST API when:** - One-off, stateless tasks (screenshot, PDF, content fetch) - No Workers infrastructure yet - Simple integrations from external services - Need quick prototyping without deployment **Use Workers Bindings when:** - Complex browser automation workflows - Need session reuse for performance - Multiple page interactions per request - Custom scripting and logic required - Building production applications ### Puppeteer vs Playwright | Feature | Puppeteer | Playwright | |---------|-----------|------------| | API Style | Chrome DevTools Protocol | High-level abstractions | | Selectors | CSS, XPath | CSS, text, role, test-id | | Best for | Advanced control, CDP access | Quick automation, testing | | Learning curve | Steeper | Gentler | **Use Puppeteer:** Need CDP protocol access, Chrome-specific features, migration from existing Puppeteer code **Use Playwright:** Modern selector APIs, cross-browser patterns, faster development ## Tier Limits Summary | Limit | Free Tier | Paid Tier | |-------|-----------|-----------| | Daily browser time | 10 minutes | Unlimited* | | Concurrent sessions | 3 | 30 | | Requests per minute | 6 | 180 | *Subject to fair-use policy. See [gotchas.md](gotchas.md) for details. ## Reading Order **New to Browser Rendering:** 1. [configuration.md](configuration.md) - Setup and deployment 2. [patterns.md](patterns.md) - Common use cases with examples 3. [api.md](api.md) - API reference 4. [gotchas.md](gotchas.md) - Avoid common pitfalls **Specific task:** - **Setup/deployment** → [configuration.md](configuration.md) - **API reference/endpoints** → [api.md](api.md) - **Example code/patterns** → [patterns.md](patterns.md) - **Debugging/troubleshooting** → [gotchas.md](gotchas.md) **REST API users:** - Start with [api.md](api.md) REST API section - Check [gotchas.md](gotchas.md) for rate limits **Workers users:** - Start with [configuration.md](configuration.md) - Review [patterns.md](patterns.md) for session management - Reference [api.md](api.md) for Workers Bindings ## In This Reference - **[configuration.md](configuration.md)** - Setup, deployment, wrangler config, compatibility - **[api.md](api.md)** - REST API endpoints + Workers Bindings (Puppeteer/Playwright) - **[patterns.md](patterns.md)** - Common patterns, use cases, real examples - **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, tier limits, common errors ## See Also - [Cloudflare Docs](https://developers.cloudflare.com/browser-rendering/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/browser-rendering/api.md ================================================ # Browser Rendering API ## REST API **Base:** `https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering` **Auth:** `Authorization: Bearer ` (Browser Rendering - Edit permission) ### Endpoints | Endpoint | Description | Key Options | |----------|-------------|-------------| | `/content` | Get rendered HTML | `url`, `waitUntil` | | `/screenshot` | Capture image | `screenshotOptions: {type, fullPage, clip}` | | `/pdf` | Generate PDF | `pdfOptions: {format, landscape, margin}` | | `/snapshot` | HTML + inlined resources | `url` | | `/scrape` | Extract by selectors | `selectors: ["h1", ".price"]` | | `/json` | AI-structured extraction | `schema: {name: "string", price: "number"}` | | `/links` | Get all links | `url` | | `/markdown` | Convert to markdown | `url` | ```bash curl -X POST '.../browser-rendering/screenshot' \ -H "Authorization: Bearer $TOKEN" \ -d '{"url":"https://example.com","screenshotOptions":{"fullPage":true}}' ``` ## Workers Binding ```jsonc // wrangler.jsonc { "browser": { "binding": "MYBROWSER" } } ``` ## Puppeteer ```typescript import puppeteer from "@cloudflare/puppeteer"; const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 }); const page = await browser.newPage(); await page.goto('https://example.com', { waitUntil: 'networkidle0' }); // Content const html = await page.content(); const title = await page.title(); // Screenshot/PDF await page.screenshot({ fullPage: true, type: 'png' }); await page.pdf({ format: 'A4', printBackground: true }); // Interaction await page.click('#button'); await page.type('#input', 'text'); await page.evaluate(() => document.querySelector('h1')?.textContent); // Session management const sessions = await puppeteer.sessions(env.MYBROWSER); const limits = await puppeteer.limits(env.MYBROWSER); await browser.close(); ``` ## Playwright ```typescript import { launch, connect } from "@cloudflare/playwright"; const browser = await launch(env.MYBROWSER, { keep_alive: 600000 }); const page = await browser.newPage(); await page.goto('https://example.com', { waitUntil: 'networkidle' }); // Modern selectors await page.locator('.button').click(); await page.getByText('Submit').click(); await page.getByTestId('search').fill('query'); // Context for isolation const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: 'custom' }); await browser.close(); ``` ## Session Management ```typescript // List sessions await puppeteer.sessions(env.MYBROWSER); // Connect to existing await puppeteer.connect(env.MYBROWSER, sessionId); // Check limits await puppeteer.limits(env.MYBROWSER); // { remaining: ms, total: ms, concurrent: n } ``` ## Key Options | Option | Values | |--------|--------| | `waitUntil` | `load`, `domcontentloaded`, `networkidle0`, `networkidle2` | | `keep_alive` | Max 600000ms (10 min) | | `screenshot.type` | `png`, `jpeg` | | `pdf.format` | `A4`, `Letter`, `Legal` | ================================================ FILE: skills/.curated/cloudflare-deploy/references/browser-rendering/configuration.md ================================================ # Configuration & Setup ## Installation ```bash npm install @cloudflare/puppeteer # or @cloudflare/playwright ``` **Use Cloudflare packages** - standard `puppeteer`/`playwright` won't work in Workers. ## wrangler.json ```json { "name": "browser-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", "compatibility_flags": ["nodejs_compat"], "browser": { "binding": "MYBROWSER" } } ``` **Required:** `nodejs_compat` flag and `browser.binding`. ## TypeScript ```typescript interface Env { MYBROWSER: Fetcher; } export default { async fetch(request: Request, env: Env): Promise { // ... } } satisfies ExportedHandler; ``` ## Development ```bash wrangler dev --remote # --remote required for browser binding ``` **Local mode does NOT support Browser Rendering** - must use `--remote`. ## REST API No wrangler config needed. Get API token with "Browser Rendering - Edit" permission. ```bash curl -X POST \ 'https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering/screenshot' \ -H 'Authorization: Bearer TOKEN' \ -d '{"url": "https://example.com"}' --output screenshot.png ``` ## Requirements | Requirement | Value | |-------------|-------| | Node.js compatibility | `nodejs_compat` flag | | Compatibility date | 2023-03-01+ | | Module format | ES modules only | | Browser | Chromium 119+ (no Firefox/Safari) | **Not supported:** WebGL, WebRTC, extensions, `file://` protocol, Service Worker syntax. ## Troubleshooting | Error | Solution | |-------|----------| | `MYBROWSER is undefined` | Use `wrangler dev --remote` | | `nodejs_compat not enabled` | Add to `compatibility_flags` | | `Module not found` | `npm install @cloudflare/puppeteer` | | `Browser Rendering not available` | Enable in dashboard | ================================================ FILE: skills/.curated/cloudflare-deploy/references/browser-rendering/gotchas.md ================================================ # Browser Rendering Gotchas ## Tier Limits | Limit | Free | Paid | |-------|------|------| | Daily browser time | 10 min | Unlimited* | | Concurrent sessions | 3 | 30 | | Requests/minute | 6 | 180 | | Session keep-alive | 10 min max | 10 min max | *Subject to fair-use policy. **Check quota:** ```typescript const limits = await puppeteer.limits(env.MYBROWSER); // { remaining: 540000, total: 600000, concurrent: 2 } ``` ## Always Close Browsers ```typescript const browser = await puppeteer.launch(env.MYBROWSER); try { const page = await browser.newPage(); await page.goto("https://example.com"); return new Response(await page.content()); } finally { await browser.close(); // ALWAYS in finally } ``` **Workers vs REST:** REST auto-closes after timeout. Workers must call `close()` or session stays open until `keep_alive` expires. ## Optimize Concurrency ```typescript // ❌ 3 sessions (hits free tier limit) const browser1 = await puppeteer.launch(env.MYBROWSER); const browser2 = await puppeteer.launch(env.MYBROWSER); // ✅ 1 session, multiple pages const browser = await puppeteer.launch(env.MYBROWSER); const page1 = await browser.newPage(); const page2 = await browser.newPage(); ``` ## Common Errors | Error | Cause | Fix | |-------|-------|-----| | Session limit exceeded | Too many concurrent | Close unused browsers, use pages not browsers | | Page navigation timeout | Slow page or `networkidle` on busy page | Increase timeout, use `waitUntil: "load"` | | Session not found | Expired session | Catch error, launch new session | | Evaluation failed | DOM element missing | Use `?.` optional chaining | | Protocol error: Target closed | Page closed during operation | Await all ops before closing | ## page.evaluate() Gotchas ```typescript // ❌ Outer scope not available const selector = "h1"; await page.evaluate(() => document.querySelector(selector)); // ✅ Pass as argument await page.evaluate((sel) => document.querySelector(sel)?.textContent, selector); ``` ## Performance **waitUntil options (fastest to slowest):** 1. `domcontentloaded` - DOM ready 2. `load` - load event (default) 3. `networkidle0` - no network for 500ms **Block unnecessary resources:** ```typescript await page.setRequestInterception(true); page.on("request", (req) => { if (["image", "stylesheet", "font"].includes(req.resourceType())) { req.abort(); } else { req.continue(); } }); ``` **Session reuse:** Cold start ~1-2s, warm connect ~100-200ms. Store sessionId in KV for reuse. ================================================ FILE: skills/.curated/cloudflare-deploy/references/browser-rendering/patterns.md ================================================ # Browser Rendering Patterns ## Basic Worker ```typescript import puppeteer from "@cloudflare/puppeteer"; export default { async fetch(request, env) { const browser = await puppeteer.launch(env.MYBROWSER); try { const page = await browser.newPage(); await page.goto("https://example.com"); return new Response(await page.content()); } finally { await browser.close(); // ALWAYS in finally } } }; ``` ## Session Reuse Keep sessions alive for performance: ```typescript let sessionId = await env.SESSION_KV.get("browser-session"); if (sessionId) { browser = await puppeteer.connect(env.MYBROWSER, sessionId); } else { browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 }); await env.SESSION_KV.put("browser-session", browser.sessionId(), { expirationTtl: 600 }); } // Don't close browser to keep session alive ``` ## Common Operations | Task | Code | |------|------| | Screenshot | `await page.screenshot({ type: "png", fullPage: true })` | | PDF | `await page.pdf({ format: "A4", printBackground: true })` | | Extract data | `await page.evaluate(() => document.querySelector('h1').textContent)` | | Fill form | `await page.type('#input', 'value'); await page.click('button')` | | Wait nav | `await Promise.all([page.waitForNavigation(), page.click('a')])` | ## Parallel Scraping ```typescript const pages = await Promise.all(urls.map(() => browser.newPage())); await Promise.all(pages.map((p, i) => p.goto(urls[i]))); const titles = await Promise.all(pages.map(p => p.title())); ``` ## Playwright Selectors ```typescript import { launch } from "@cloudflare/playwright"; const browser = await launch(env.MYBROWSER); await page.getByRole("button", { name: "Sign in" }).click(); await page.getByLabel("Email").fill("user@example.com"); await page.getByTestId("submit-button").click(); ``` ## Incognito Contexts Isolated sessions without multiple browsers: ```typescript const ctx1 = await browser.createIncognitoBrowserContext(); const ctx2 = await browser.createIncognitoBrowserContext(); // Each has isolated cookies/storage ``` ## Quota Check ```typescript const limits = await puppeteer.limits(env.MYBROWSER); if (limits.remaining < 60000) return new Response("Quota low", { status: 429 }); ``` ## Error Handling ```typescript try { await page.goto(url, { timeout: 30000, waitUntil: "networkidle0" }); } catch (e) { if (e.message.includes("timeout")) return new Response("Timeout", { status: 504 }); if (e.message.includes("Session limit")) return new Response("Too many sessions", { status: 429 }); } finally { if (browser) await browser.close(); } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/c3/README.md ================================================ # C3 (create-cloudflare) Official CLI for scaffolding Cloudflare Workers and Pages projects with templates, TypeScript, and instant deployment. ## Quick Start ```bash # Interactive (recommended for first-time) npm create cloudflare@latest my-app # Worker (API/WebSocket/Cron) npm create cloudflare@latest my-api -- --type=hello-world --ts # Pages (static/SSG/full-stack) npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages ``` ## Platform Decision Tree ``` What are you building? ├─ API / WebSocket / Cron / Email handler │ └─ Workers (default) - no --platform flag needed │ npm create cloudflare@latest my-api -- --type=hello-world ├─ Static site / SSG / Documentation │ └─ Pages - requires --platform=pages │ npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages ├─ Full-stack app (Next.js/Remix/SvelteKit) │ ├─ Need Durable Objects, Queues, or Workers-only features? │ │ └─ Workers (default) │ └─ Otherwise use Pages for git integration and branch previews │ └─ Add --platform=pages └─ Convert existing project └─ npm create cloudflare@latest . -- --type=pre-existing --existing-script=./src/worker.ts ``` **Critical:** Pages projects require `--platform=pages` flag. Without it, C3 defaults to Workers. ## Interactive Flow When run without flags, C3 prompts in this order: 1. **Project name** - Directory to create (defaults to current dir with `.`) 2. **Application type** - `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` 3. **Platform** - `workers` (default) or `pages` (for web apps only) 4. **Framework** - If web-app: `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, etc. 5. **TypeScript** - `yes` (recommended) or `no` 6. **Git** - Initialize repository? `yes` or `no` 7. **Deploy** - Deploy now? `yes` or `no` (requires `wrangler login`) ## Installation Methods ```bash # NPM npm create cloudflare@latest # Yarn yarn create cloudflare # PNPM pnpm create cloudflare@latest ``` ## In This Reference | File | Purpose | Use When | |------|---------|----------| | **api.md** | Complete CLI flag reference | Scripting, CI/CD, advanced usage | | **configuration.md** | Generated files, bindings, types | Understanding output, customization | | **patterns.md** | Workflows, CI/CD, monorepos | Real-world integration | | **gotchas.md** | Troubleshooting failures | Deployment blocked, errors | ## Reading Order | Task | Read | |------|------| | Create first project | README only | | Set up CI/CD | README → api → patterns | | Debug failed deploy | gotchas | | Understand generated files | configuration | | Full CLI reference | api | | Create custom template | patterns → configuration | | Convert existing project | README → patterns | ## Post-Creation ```bash cd my-app # Local dev with hot reload npm run dev # Generate TypeScript types for bindings npm run cf-typegen # Deploy to Cloudflare npm run deploy ``` ## See Also - **workers/README.md** - Workers runtime, bindings, APIs - **workers-ai/README.md** - AI/ML models - **pages/README.md** - Pages-specific features - **wrangler/README.md** - Wrangler CLI beyond initial setup - **d1/README.md** - SQLite database - **r2/README.md** - Object storage ================================================ FILE: skills/.curated/cloudflare-deploy/references/c3/api.md ================================================ # C3 CLI Reference ## Invocation ```bash npm create cloudflare@latest [name] [-- flags] # NPM requires -- yarn create cloudflare [name] [flags] pnpm create cloudflare@latest [name] [-- flags] ``` ## Core Flags | Flag | Values | Description | |------|--------|-------------| | `--type` | `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` | Application type | | `--platform` | `workers` (default), `pages` | Target platform | | `--framework` | `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, `qwik`, `vue`, `angular`, `hono` | Web framework (requires `--type=web-app`) | | `--lang` | `ts`, `js`, `python` | Language (for `--type=hello-world`) | | `--ts` / `--no-ts` | - | TypeScript for web apps | ## Deployment Flags | Flag | Description | |------|-------------| | `--deploy` / `--no-deploy` | Deploy immediately (prompts interactive, skips in CI) | | `--git` / `--no-git` | Initialize git (default: yes) | | `--open` | Open browser after deploy | ## Advanced Flags | Flag | Description | |------|-------------| | `--template=user/repo` | GitHub template or local path | | `--existing-script=./src/worker.ts` | Existing script (requires `--type=pre-existing`) | | `--category=ai\|database\|realtime` | Demo filter (requires `--type=demo`) | | `--experimental` | Enable experimental features | | `--wrangler-defaults` | Skip wrangler prompts | ## Environment Variables ```bash CLOUDFLARE_API_TOKEN=xxx # For deployment CLOUDFLARE_ACCOUNT_ID=xxx # Account ID CF_TELEMETRY_DISABLED=1 # Disable telemetry ``` ## Exit Codes `0` success, `1` user abort, `2` error ## Examples ```bash # TypeScript Worker npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-deploy # Next.js on Pages npm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts # Astro blog npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --ts --deploy # CI: non-interactive npm create cloudflare@latest my-app -- --type=web-app --framework=next --ts --no-git --no-deploy # GitHub template npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi # Convert existing project npm create cloudflare@latest . -- --type=pre-existing --existing-script=./build/worker.js ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/c3/configuration.md ================================================ # C3 Generated Configuration ## Output Structure ``` my-app/ ├── src/index.ts # Worker entry point ├── wrangler.jsonc # Cloudflare config ├── package.json # Scripts ├── tsconfig.json └── .gitignore ``` ## wrangler.jsonc ```jsonc { "$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json", "name": "my-app", "main": "src/index.ts", "compatibility_date": "2026-01-27" } ``` ## Binding Placeholders C3 generates **placeholder IDs** that must be replaced before deploy: ```jsonc { "kv_namespaces": [{ "binding": "MY_KV", "id": "placeholder_kv_id" }], "d1_databases": [{ "binding": "DB", "database_id": "00000000-..." }] } ``` **Replace with real IDs:** ```bash npx wrangler kv namespace create MY_KV # Returns real ID npx wrangler d1 create my-database # Returns real database_id ``` **Deployment error if not replaced:** ``` Error: Invalid KV namespace ID "placeholder_kv_id" ``` ## Scripts ```json { "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", "cf-typegen": "wrangler types" } } ``` ## Type Generation Run after adding bindings: ```bash npm run cf-typegen ``` Generates `.wrangler/types/runtime.d.ts`: ```typescript interface Env { MY_KV: KVNamespace; DB: D1Database; } ``` ## Post-Creation Checklist 1. Review `wrangler.jsonc` - check name, compatibility_date 2. Replace placeholder binding IDs with real resource IDs 3. Run `npm run cf-typegen` 4. Test: `npm run dev` 5. Deploy: `npm run deploy` 6. Add secrets: `npx wrangler secret put SECRET_NAME` ================================================ FILE: skills/.curated/cloudflare-deploy/references/c3/gotchas.md ================================================ # C3 Troubleshooting ## Deployment Issues ### Placeholder IDs **Error:** "Invalid namespace ID" **Fix:** Replace placeholders in wrangler.jsonc with real IDs: ```bash npx wrangler kv namespace create MY_KV # Get real ID ``` ### Authentication **Error:** "Not authenticated" **Fix:** `npx wrangler login` or set `CLOUDFLARE_API_TOKEN` ### Name Conflict **Error:** "Worker already exists" **Fix:** Change `name` in wrangler.jsonc ## Platform Selection | Need | Platform | |------|----------| | Git integration, branch previews | `--platform=pages` | | Durable Objects, D1, Queues | Workers (default) | Wrong platform? Recreate with correct `--platform` flag. ## TypeScript Issues **"Cannot find name 'KVNamespace'"** ```bash npm run cf-typegen # Regenerate types # Restart TS server in editor ``` **Missing types after config change:** Re-run `npm run cf-typegen` ## Package Manager **Multiple lockfiles causing issues:** ```bash rm pnpm-lock.yaml # If using npm rm package-lock.json # If using pnpm ``` ## CI/CD **CI hangs on prompts:** ```bash npm create cloudflare@latest my-app -- \ --type=hello-world --lang=ts --no-git --no-deploy ``` **Auth in CI:** ```yaml env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} ``` ## Framework-Specific | Framework | Issue | Fix | |-----------|-------|-----| | Next.js | create-next-app failed | `npm cache clean --force`, retry | | Astro | Adapter missing | Install `@astrojs/cloudflare` | | Remix | Module errors | Update `@remix-run/cloudflare*` | ## Compatibility Date **"Feature X requires compatibility_date >= ..."** **Fix:** Update `compatibility_date` in wrangler.jsonc to today's date ## Node.js Version **"Node.js version not supported"** **Fix:** Install Node.js 18+ (`nvm install 20`) ## Quick Reference | Error | Cause | Fix | |-------|-------|-----| | Invalid namespace ID | Placeholder binding | Create resource, update config | | Not authenticated | No login | `npx wrangler login` | | Cannot find KVNamespace | Missing types | `npm run cf-typegen` | | Worker already exists | Name conflict | Change `name` | | CI hangs | Missing flags | Add --type, --lang, --no-deploy | | Template not found | Bad name | Check cloudflare/templates | ================================================ FILE: skills/.curated/cloudflare-deploy/references/c3/patterns.md ================================================ # C3 Usage Patterns ## Quick Workflows ```bash # TypeScript API Worker npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --deploy # Next.js on Pages npm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts --deploy # Astro static site npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --platform=pages --ts ``` ## CI/CD (GitHub Actions) ```yaml - name: Deploy run: npm run deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} ``` **Non-interactive requires:** ```bash --type= # Required --no-git # Recommended (CI already in git) --no-deploy # Deploy separately with secrets --framework= # For web-app --ts / --no-ts # Required ``` ## Monorepo C3 detects workspace config (`package.json` workspaces or `pnpm-workspace.yaml`). ```bash cd packages/ npm create cloudflare@latest my-worker -- --type=hello-world --lang=ts --no-deploy ``` ## Custom Templates ```bash # GitHub repo npm create cloudflare@latest -- --template=username/repo npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi # Local path npm create cloudflare@latest my-app -- --template=../my-template ``` **Template requires `c3.config.json`:** ```json { "name": "my-template", "category": "hello-world", "copies": [{ "path": "src/" }, { "path": "wrangler.jsonc" }], "transforms": [{ "path": "package.json", "jsonc": { "name": "{{projectName}}" }}] } ``` ## Existing Projects ```bash # Add Cloudflare to existing Worker npm create cloudflare@latest . -- --type=pre-existing --existing-script=./dist/index.js # Add to existing framework app npm create cloudflare@latest . -- --type=web-app --framework=next --platform=pages --ts ``` ## Post-Creation Checklist 1. Review `wrangler.jsonc` - set `compatibility_date`, verify `name` 2. Create bindings: `wrangler kv namespace create`, `wrangler d1 create`, `wrangler r2 bucket create` 3. Generate types: `npm run cf-typegen` 4. Test: `npm run dev` 5. Deploy: `npm run deploy` 6. Set secrets: `wrangler secret put SECRET_NAME` ================================================ FILE: skills/.curated/cloudflare-deploy/references/cache-reserve/README.md ================================================ # Cloudflare Cache Reserve **Persistent cache storage built on R2 for long-term content retention** ## Smart Shield Integration Cache Reserve is part of **Smart Shield**, Cloudflare's comprehensive security and performance suite: - **Smart Shield Advanced tier**: Includes 2TB Cache Reserve storage - **Standalone purchase**: Available separately if not using Smart Shield - **Migration**: Existing standalone customers can migrate to Smart Shield bundles **Decision**: Already on Smart Shield Advanced? Cache Reserve is included. Otherwise evaluate standalone purchase vs Smart Shield upgrade. ## Overview Cache Reserve is Cloudflare's persistent, large-scale cache storage layer built on R2. It acts as the ultimate upper-tier cache, storing cacheable content for extended periods (30+ days) to maximize cache hits, reduce origin egress fees, and shield origins from repeated requests for long-tail content. ## Core Concepts ### What is Cache Reserve? - **Persistent storage layer**: Built on R2, sits above tiered cache hierarchy - **Long-term retention**: 30-day default retention, extended on each access - **Automatic operation**: Works seamlessly with existing CDN, no code changes required - **Origin shielding**: Dramatically reduces origin egress by serving cached content longer - **Usage-based pricing**: Pay only for storage + read/write operations ### Cache Hierarchy ``` Visitor Request ↓ Lower-Tier Cache (closest to visitor) ↓ (on miss) Upper-Tier Cache (closest to origin) ↓ (on miss) Cache Reserve (R2 persistent storage) ↓ (on miss) Origin Server ``` ### How It Works 1. **On cache miss**: Content fetched from origin �� written to Cache Reserve + edge caches simultaneously 2. **On edge eviction**: Content may be evicted from edge cache but remains in Cache Reserve 3. **On subsequent request**: If edge cache misses but Cache Reserve hits → content restored to edge caches 4. **Retention**: Assets remain in Cache Reserve for 30 days since last access (configurable via TTL) ## When to Use Cache Reserve ``` Need persistent caching? ├─ High origin egress costs → Cache Reserve ✓ ├─ Long-tail content (archives, media libraries) → Cache Reserve ✓ ├─ Already using Smart Shield Advanced → Included! ✓ ├─ Video streaming with seeking (range requests) → ✗ Not supported ├─ Dynamic/personalized content → ✗ Use edge cache only ├─ Need per-request cache control from Workers → ✗ Use R2 directly └─ Frequently updated content (< 10hr lifetime) → ✗ Not eligible ``` ## Asset Eligibility Cache Reserve only stores assets meeting **ALL** criteria: - Cacheable per Cloudflare's standard rules - Minimum 10-hour TTL (36000 seconds) - `Content-Length` header present - Original files only (not transformed images) ### Eligibility Checklist Use this checklist to verify if an asset is eligible: - [ ] Zone has Cache Reserve enabled - [ ] Zone has Tiered Cache enabled (required) - [ ] Asset TTL ≥ 10 hours (36,000 seconds) - [ ] `Content-Length` header present on origin response - [ ] No `Set-Cookie` header (or uses private directive) - [ ] `Vary` header is NOT `*` (can be `Accept-Encoding`) - [ ] Not an image transformation variant (original images OK) - [ ] Not a range request (no HTTP 206 support) - [ ] Not O2O (Orange-to-Orange) proxied request **All boxes must be checked for Cache Reserve eligibility.** ### Not Eligible - Assets with TTL < 10 hours - Responses without `Content-Length` header - Image transformation variants (original images are eligible) - Responses with `Set-Cookie` headers - Responses with `Vary: *` header - Assets from R2 public buckets on same zone - O2O (Orange-to-Orange) setup requests - **Range requests** (video seeking, partial content downloads) ## Quick Start ```bash # Enable via Dashboard https://dash.cloudflare.com/caching/cache-reserve # Click "Enable Storage Sync" or "Purchase" button ``` **Prerequisites:** - Paid Cache Reserve plan or Smart Shield Advanced required - Tiered Cache required for optimal performance ## Essential Commands ```bash # Check Cache Reserve status curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \ -H "Authorization: Bearer $API_TOKEN" # Enable Cache Reserve curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \ -H "Authorization: Bearer $API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "on"}' # Check asset cache status curl -I https://example.com/asset.jpg | grep -i cache ``` ## In This Reference | Task | Files | |------|-------| | Evaluate if Cache Reserve fits your use case | README.md (this file) | | Enable Cache Reserve for your zone | README.md + [configuration.md](./configuration.md) | | Use with Workers (understand limitations) | [api.md](./api.md) | | Setup via SDKs or IaC (TypeScript, Python, Terraform) | [configuration.md](./configuration.md) | | Optimize costs and debug issues | [patterns.md](./patterns.md) + [gotchas.md](./gotchas.md) | | Understand eligibility and troubleshoot | [gotchas.md](./gotchas.md) → [patterns.md](./patterns.md) | **Files:** - [configuration.md](./configuration.md) - Setup, API, SDKs, and Cache Rules - [api.md](./api.md) - Purging, monitoring, Workers integration - [patterns.md](./patterns.md) - Best practices, cost optimization, debugging - [gotchas.md](./gotchas.md) - Common issues, limitations, troubleshooting ## See Also - [r2](../r2/) - Cache Reserve built on R2 storage - [workers](../workers/) - Workers integration with Cache API ================================================ FILE: skills/.curated/cloudflare-deploy/references/cache-reserve/api.md ================================================ # Cache Reserve API ## Workers Integration ``` ┌────────────────────────────────────────────────────────────────┐ │ CRITICAL: Workers Cache API ≠ Cache Reserve │ │ │ │ • Workers caches.default / cache.put() → edge cache ONLY │ │ • Cache Reserve → zone-level setting, automatic, no per-req │ │ • You CANNOT selectively write to Cache Reserve from Workers │ │ • Cache Reserve works with standard fetch(), not cache.put() │ └────────────────────────────────────────────────────────────────┘ ``` Cache Reserve is a **zone-level configuration**, not a per-request API. It works automatically when enabled for the zone: ### Standard Fetch (Recommended) ```typescript // Cache Reserve works automatically via standard fetch export default { async fetch(request: Request, env: Env): Promise { // Standard fetch uses Cache Reserve automatically return await fetch(request); } }; ``` ### Cache API Limitations **IMPORTANT**: `cache.put()` is **NOT compatible** with Cache Reserve or Tiered Cache. ```typescript // ❌ WRONG: cache.put() bypasses Cache Reserve const cache = caches.default; let response = await cache.match(request); if (!response) { response = await fetch(request); await cache.put(request, response.clone()); // Bypasses Cache Reserve! } // ✅ CORRECT: Use standard fetch for Cache Reserve compatibility return await fetch(request); // ✅ CORRECT: Use Cache API only for custom cache namespaces const customCache = await caches.open('my-custom-cache'); let response = await customCache.match(request); if (!response) { response = await fetch(request); await customCache.put(request, response.clone()); // Custom cache OK } ``` ## Purging and Cache Management ### Purge by URL (Instant) ```typescript // Purge specific URL from Cache Reserve immediately const purgeCacheReserveByURL = async ( zoneId: string, apiToken: string, urls: string[] ) => { const response = await fetch( `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ files: urls }) } ); return await response.json(); }; // Example usage await purgeCacheReserveByURL('zone123', 'token456', [ 'https://example.com/image.jpg', 'https://example.com/video.mp4' ]); ``` ### Purge by Tag/Host/Prefix (Revalidation) ```typescript // Purge by cache tag - forces revalidation, not immediate removal await fetch( `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ tags: ['tag1', 'tag2'] }) } ); ``` **Purge behavior:** - **By URL**: Immediate removal from Cache Reserve + edge cache - **By tag/host/prefix**: Revalidation only, assets remain in storage (costs continue) ### Clear All Cache Reserve Data ```typescript // Requires Cache Reserve OFF first await fetch( `https://api.cloudflare.com/client/v4/zones/${zoneId}/cache/cache_reserve_clear`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}` } } ); // Check status: GET same endpoint returns { state: "In-progress" | "Completed" } ``` **Process**: Disable Cache Reserve → Call clear endpoint → Wait up to 24hr → Re-enable ## Monitoring and Analytics ### Dashboard Analytics Navigate to **Caching > Cache Reserve** to view: - **Egress Savings**: Total bytes served from Cache Reserve vs origin egress cost saved - **Requests Served**: Cache Reserve hits vs misses breakdown - **Storage Used**: Current GB stored in Cache Reserve (billed monthly) - **Operations**: Class A (writes) and Class B (reads) operation counts - **Cost Tracking**: Estimated monthly costs based on current usage ### Logpush Integration ```typescript // Logpush field: CacheReserveUsed (boolean) - filter for Cache Reserve hits // Query Cache Reserve hits in analytics const logpushQuery = ` SELECT ClientRequestHost, COUNT(*) as requests, SUM(EdgeResponseBytes) as bytes_served, COUNT(CASE WHEN CacheReserveUsed = true THEN 1 END) as cache_reserve_hits, COUNT(CASE WHEN CacheReserveUsed = false THEN 1 END) as cache_reserve_misses FROM http_requests WHERE Timestamp >= NOW() - INTERVAL '24 hours' GROUP BY ClientRequestHost ORDER BY requests DESC `; // Filter only Cache Reserve hits const crHitsQuery = ` SELECT ClientRequestHost, COUNT(*) as requests, SUM(EdgeResponseBytes) as bytes FROM http_requests WHERE CacheReserveUsed = true AND Timestamp >= NOW() - INTERVAL '7 days' GROUP BY ClientRequestHost ORDER BY bytes DESC `; ``` ### GraphQL Analytics ```graphql query CacheReserveAnalytics($zoneTag: string, $since: string, $until: string) { viewer { zones(filter: { zoneTag: $zoneTag }) { httpRequests1dGroups( filter: { datetime_geq: $since, datetime_leq: $until } limit: 1000 ) { dimensions { date } sum { cachedBytes cachedRequests bytes requests } } } } } ``` ## Pricing ```typescript // Storage: $0.015/GB-month | Class A (writes): $4.50/M | Class B (reads): $0.36/M // Cache miss: 1A + 1B | Cache hit: 1B | Assets >1GB: proportionally more ops ``` ## See Also - [README](./README.md) - Overview and core concepts - [Configuration](./configuration.md) - Setup and Cache Rules - [Patterns](./patterns.md) - Best practices and optimization - [Gotchas](./gotchas.md) - Common issues and troubleshooting ================================================ FILE: skills/.curated/cloudflare-deploy/references/cache-reserve/configuration.md ================================================ # Cache Reserve Configuration ## Dashboard Setup **Minimum steps to enable:** ```bash # Navigate to dashboard https://dash.cloudflare.com/caching/cache-reserve # Click "Enable Storage Sync" or "Purchase" button ``` **Prerequisites:** - Paid Cache Reserve plan or Smart Shield Advanced required - Tiered Cache **required** for Cache Reserve to function optimally ## API Configuration ### REST API ```bash # Enable curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \ -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \ -d '{"value": "on"}' # Check status curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \ -H "Authorization: Bearer $API_TOKEN" ``` ### TypeScript SDK ```bash npm install cloudflare ``` ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN, }); // Enable Cache Reserve await client.cache.cacheReserve.edit({ zone_id: 'abc123', value: 'on', }); // Get Cache Reserve status const status = await client.cache.cacheReserve.get({ zone_id: 'abc123', }); console.log(status.value); // 'on' or 'off' ``` ### Python SDK ```bash pip install cloudflare ``` ```python from cloudflare import Cloudflare client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN")) # Enable Cache Reserve client.cache.cache_reserve.edit( zone_id="abc123", value="on" ) # Get Cache Reserve status status = client.cache.cache_reserve.get(zone_id="abc123") print(status.value) # 'on' or 'off' ``` ### Terraform ```hcl terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" version = "~> 4.0" } } } provider "cloudflare" { api_token = var.cloudflare_api_token } resource "cloudflare_zone_cache_reserve" "example" { zone_id = var.zone_id enabled = true } # Tiered Cache is required for Cache Reserve resource "cloudflare_tiered_cache" "example" { zone_id = var.zone_id cache_type = "smart" } ``` ### Pulumi ```typescript import * as cloudflare from "@pulumi/cloudflare"; // Enable Cache Reserve const cacheReserve = new cloudflare.ZoneCacheReserve("example", { zoneId: zoneId, enabled: true, }); // Enable Tiered Cache (required) const tieredCache = new cloudflare.TieredCache("example", { zoneId: zoneId, cacheType: "smart", }); ``` ### Required API Token Permissions - `Zone Settings Read` - `Zone Settings Write` - `Zone Read` - `Zone Write` ## Cache Rules Integration Control Cache Reserve eligibility via Cache Rules: ```typescript // Enable for static assets { action: 'set_cache_settings', action_parameters: { cache_reserve: { eligible: true, minimum_file_ttl: 86400 }, edge_ttl: { mode: 'override_origin', default: 86400 }, cache: true }, expression: '(http.request.uri.path matches "\\.(jpg|png|webp|pdf|zip)$")' } // Disable for APIs { action: 'set_cache_settings', action_parameters: { cache_reserve: { eligible: false } }, expression: '(http.request.uri.path matches "^/api/")' } // Create via API: PUT to zones/{zone_id}/rulesets/phases/http_request_cache_settings/entrypoint ``` ## Wrangler Integration Cache Reserve works automatically with Workers deployed via Wrangler. No special wrangler.jsonc configuration needed - enable Cache Reserve via Dashboard or API for the zone. ## See Also - [README](./README.md) - Overview and core concepts - [API Reference](./api.md) - Purging and monitoring APIs - [Patterns](./patterns.md) - Best practices and optimization - [Gotchas](./gotchas.md) - Common issues and troubleshooting ================================================ FILE: skills/.curated/cloudflare-deploy/references/cache-reserve/gotchas.md ================================================ # Cache Reserve Gotchas ## Common Errors ### "Assets Not Being Cached in Cache Reserve" **Cause:** Asset is not cacheable, TTL < 10 hours, Content-Length header missing, or blocking headers present (Set-Cookie, Vary: *) **Solution:** Ensure minimum TTL of 10+ hours (`Cache-Control: public, max-age=36000`), add Content-Length header, remove Set-Cookie header, and set `Vary: Accept-Encoding` (not *) ### "Range Requests Not Working" (Video Seeking Fails) **Cause:** Cache Reserve does **NOT** support range requests (HTTP 206 Partial Content) **Solution:** Range requests bypass Cache Reserve entirely. For video streaming with seeking: - Use edge cache only (shorter TTLs) - Consider R2 with direct access for range-heavy workloads - Accept that seekable content won't benefit from Cache Reserve persistence ### "Origin Bandwidth Higher Than Expected" **Cause:** Cache Reserve fetches **uncompressed** content from origin, even though it serves compressed to visitors **Solution:** - If origin charges by bandwidth, factor in uncompressed transfer costs - Cache Reserve compresses for visitors automatically (saves visitor bandwidth) - Compare: origin egress savings vs higher uncompressed fetch costs ### "Cloudflare Images Not Caching with Cache Reserve" **Cause:** Cloudflare Images with `Vary: Accept` header (format negotiation) is incompatible with Cache Reserve **Solution:** - Cache Reserve silently skips images with Vary for format negotiation - Original images (non-transformed) may still be eligible - Use Cloudflare Images variants or edge cache for transformed images ### "High Class A Operations Costs" **Cause:** Frequent cache misses, short TTLs, or frequent revalidation **Solution:** Increase TTL for stable content (24+ hours), enable Tiered Cache to reduce direct Cache Reserve misses, or use stale-while-revalidate ### "Purge Not Working as Expected" **Cause:** Purge by tag only triggers revalidation but doesn't remove from Cache Reserve storage **Solution:** Use purge by URL for immediate removal, or disable Cache Reserve then clear all data for complete removal ### "O2O (Orange-to-Orange) Assets Not Caching" **Cause:** Orange-to-Orange (proxied zone requesting another proxied zone on Cloudflare) bypasses Cache Reserve **Solution:** - **What is O2O**: Zone A (proxied) → Zone B (proxied), both on Cloudflare - **Detection**: Check `cf-cache-status` for `BYPASS` and review request path - **Workaround**: Use R2 or direct origin access instead of O2O proxy chains ### "Cache Reserve must be OFF before clearing data" **Cause:** Attempting to clear Cache Reserve data while it's still enabled **Solution:** Disable Cache Reserve first, wait briefly for propagation (5s), then clear data (can take up to 24 hours) ## Limits | Limit | Value | Notes | |-------|-------|-------| | Minimum TTL | 10 hours (36000 seconds) | Assets with shorter TTL not eligible | | Default retention | 30 days (2592000 seconds) | Configurable | | Maximum file size | Same as R2 limits | No practical limit | | Purge/clear time | Up to 24 hours | Complete propagation time | | Plan requirement | Paid Cache Reserve or Smart Shield | Not available on free plans | | Content-Length header | Required | Must be present for eligibility | | Set-Cookie header | Blocks caching | Must not be present (or use private directive) | | Vary header | Cannot be * | Can use Vary: Accept-Encoding | | Image transformations | Variants not eligible | Original images only | | Range requests | NOT supported | HTTP 206 bypasses Cache Reserve | | Compression | Fetches uncompressed | Serves compressed to visitors | | Worker control | Zone-level only | Cannot control per-request | | O2O requests | Bypassed | Orange-to-Orange not eligible | ## Additional Resources - **Official Docs**: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/ - **API Reference**: https://developers.cloudflare.com/api/resources/cache/subresources/cache_reserve/ - **Cache Rules**: https://developers.cloudflare.com/cache/how-to/cache-rules/ - **Workers Cache API**: https://developers.cloudflare.com/workers/runtime-apis/cache/ - **R2 Documentation**: https://developers.cloudflare.com/r2/ - **Smart Shield**: https://developers.cloudflare.com/smart-shield/ - **Tiered Cache**: https://developers.cloudflare.com/cache/how-to/tiered-cache/ ## Troubleshooting Flowchart Asset not caching in Cache Reserve? ``` 1. Is Cache Reserve enabled for zone? → No: Enable via Dashboard or API → Yes: Continue to step 2 2. Is Tiered Cache enabled? → No: Enable Tiered Cache (required!) → Yes: Continue to step 3 3. Does asset have TTL ≥ 10 hours? → No: Increase via Cache Rules (edge_ttl override) → Yes: Continue to step 4 4. Is Content-Length header present? → No: Fix origin to include Content-Length → Yes: Continue to step 5 5. Is Set-Cookie header present? → Yes: Remove Set-Cookie or scope appropriately → No: Continue to step 6 6. Is Vary header set to *? → Yes: Change to specific value (e.g., Accept-Encoding) → No: Continue to step 7 7. Is this a range request? → Yes: Range requests bypass Cache Reserve (not supported) → No: Continue to step 8 8. Is this an O2O (Orange-to-Orange) request? → Yes: O2O bypasses Cache Reserve → No: Continue to step 9 9. Check Logpush CacheReserveUsed field → Filter logs to see if assets ever hit Cache Reserve → Verify cf-cache-status header (should be HIT after first request) ``` ## See Also - [README](./README.md) - Overview and core concepts - [Configuration](./configuration.md) - Setup and Cache Rules - [API Reference](./api.md) - Purging and monitoring - [Patterns](./patterns.md) - Best practices and optimization ================================================ FILE: skills/.curated/cloudflare-deploy/references/cache-reserve/patterns.md ================================================ # Cache Reserve Patterns ## Best Practices ### 1. Always Enable Tiered Cache ```typescript // Cache Reserve is designed for use WITH Tiered Cache const configuration = { tieredCache: 'enabled', // Required for optimal performance cacheReserve: 'enabled', // Works best with Tiered Cache hierarchy: [ 'Lower-Tier Cache (visitor)', 'Upper-Tier Cache (origin region)', 'Cache Reserve (persistent)', 'Origin' ] }; ``` ### 2. Set Appropriate Cache-Control Headers ```typescript // Origin response headers for Cache Reserve eligibility const originHeaders = { 'Cache-Control': 'public, max-age=86400', // 24hr (minimum 10hr) 'Content-Length': '1024000', // Required 'Cache-Tag': 'images,product-123', // Optional: purging 'ETag': '"abc123"', // Optional: revalidation // Avoid: 'Set-Cookie' and 'Vary: *' prevent caching }; ``` ### 3. Use Cache Rules for Fine-Grained Control ```typescript // Different TTLs for different content types const cacheRules = [ { description: 'Long-term cache for immutable assets', expression: '(http.request.uri.path matches "^/static/.*\\.[a-f0-9]{8}\\.")', action_parameters: { cache_reserve: { eligible: true }, edge_ttl: { mode: 'override_origin', default: 2592000 }, // 30 days cache: true } }, { description: 'Moderate cache for regular images', expression: '(http.request.uri.path matches "\\.(jpg|png|webp)$")', action_parameters: { cache_reserve: { eligible: true }, edge_ttl: { mode: 'override_origin', default: 86400 }, // 24 hours cache: true } }, { description: 'Exclude API from Cache Reserve', expression: '(http.request.uri.path matches "^/api/")', action_parameters: { cache_reserve: { eligible: false }, cache: false } } ]; ``` ### 4. Making Assets Cache Reserve Eligible from Workers **Note**: This modifies response headers to meet eligibility criteria but does NOT directly control Cache Reserve storage (which is zone-level automatic). ```typescript export default { async fetch(request: Request, env: Env): Promise { const response = await fetch(request); if (!response.ok) return response; const headers = new Headers(response.headers); headers.set('Cache-Control', 'public, max-age=36000'); // 10hr minimum headers.delete('Set-Cookie'); // Blocks caching // Ensure Content-Length present if (!headers.has('Content-Length')) { const blob = await response.blob(); headers.set('Content-Length', blob.size.toString()); return new Response(blob, { status: response.status, headers }); } return new Response(response.body, { status: response.status, headers }); } }; ``` ### 5. Hostname Best Practices Use Worker's hostname for efficient caching - avoid overriding hostname unnecessarily. ## Architecture Patterns ### Multi-Tier Caching + Immutable Assets ```typescript // Optimal: L1 (visitor) → L2 (region) → L3 (Cache Reserve) → Origin export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); const isImmutable = /\.[a-f0-9]{8,}\.(js|css|jpg|png|woff2)$/.test(url.pathname); const response = await fetch(request); if (isImmutable) { const headers = new Headers(response.headers); headers.set('Cache-Control', 'public, max-age=31536000, immutable'); return new Response(response.body, { status: response.status, headers }); } return response; } }; ``` ## Cost Optimization ### Cost Calculator ```typescript interface CacheReserveEstimate { avgAssetSizeGB: number; uniqueAssets: number; monthlyReads: number; monthlyWrites: number; originEgressCostPerGB: number; // e.g., AWS: $0.09/GB } function estimateMonthlyCost(input: CacheReserveEstimate) { // Cache Reserve pricing const storageCostPerGBMonth = 0.015; const classAPerMillion = 4.50; // writes const classBPerMillion = 0.36; // reads // Calculate Cache Reserve costs const totalStorageGB = input.avgAssetSizeGB * input.uniqueAssets; const storageCost = totalStorageGB * storageCostPerGBMonth; const writeCost = (input.monthlyWrites / 1_000_000) * classAPerMillion; const readCost = (input.monthlyReads / 1_000_000) * classBPerMillion; const cacheReserveCost = storageCost + writeCost + readCost; // Calculate origin egress cost (what you'd pay without Cache Reserve) const totalTrafficGB = (input.monthlyReads * input.avgAssetSizeGB); const originEgressCost = totalTrafficGB * input.originEgressCostPerGB; // Savings calculation const savings = originEgressCost - cacheReserveCost; const savingsPercent = ((savings / originEgressCost) * 100).toFixed(1); return { cacheReserveCost: `$${cacheReserveCost.toFixed(2)}`, originEgressCost: `$${originEgressCost.toFixed(2)}`, monthlySavings: `$${savings.toFixed(2)}`, savingsPercent: `${savingsPercent}%`, breakdown: { storage: `$${storageCost.toFixed(2)}`, writes: `$${writeCost.toFixed(2)}`, reads: `$${readCost.toFixed(2)}`, } }; } // Example: Media library const mediaLibrary = estimateMonthlyCost({ avgAssetSizeGB: 0.005, // 5MB images uniqueAssets: 10_000, monthlyReads: 5_000_000, monthlyWrites: 50_000, originEgressCostPerGB: 0.09, // AWS S3 }); console.log(mediaLibrary); // { // cacheReserveCost: "$9.98", // originEgressCost: "$25.00", // monthlySavings: "$15.02", // savingsPercent: "60.1%", // breakdown: { storage: "$0.75", writes: "$0.23", reads: "$9.00" } // } ``` ### Optimization Guidelines - **Set appropriate TTLs**: 10hr minimum, 24hr+ optimal for stable content, 30d max cautiously - **Cache high-value stable assets**: Images, media, fonts, archives, documentation - **Exclude frequently changing**: APIs, user-specific content, real-time data - **Compression note**: Cache Reserve fetches uncompressed from origin, serves compressed to visitors - factor in origin egress costs ## See Also - [README](./README.md) - Overview and core concepts - [Configuration](./configuration.md) - Setup and Cache Rules - [API Reference](./api.md) - Purging and monitoring - [Gotchas](./gotchas.md) - Common issues and troubleshooting ================================================ FILE: skills/.curated/cloudflare-deploy/references/containers/README.md ================================================ # Cloudflare Containers Skill Reference **APPLIES TO: Cloudflare Containers ONLY - NOT general Cloudflare Workers** Use when working with Cloudflare Containers: deploying containerized apps on Workers platform, configuring container-enabled Durable Objects, managing container lifecycle, or implementing stateful/stateless container patterns. ## Beta Status ⚠️ Containers is currently in **beta**. API may change without notice. No SLA guarantees. Custom instance types added Jan 2026. ## Core Concepts **Container as Durable Object:** Each container is a Durable Object with persistent identity. Accessed via `getByName(id)` or `getRandom()`. **Image deployment:** Images pre-fetched globally. Deployments use rolling strategy (not instant like Workers). **Lifecycle:** cold start (2-3s) → running → `sleepAfter` timeout → stopped. No autoscaling - manual load balancing via `getRandom()`. **Persistent identity, ephemeral disk:** Container ID persists, but disk resets on stop. Use Durable Object storage for persistence. ## Quick Start ```typescript import { Container } from "@cloudflare/containers"; export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "30m"; } export default { async fetch(request: Request, env: Env) { const container = env.MY_CONTAINER.getByName("instance-1"); await container.startAndWaitForPorts(); return container.fetch(request); } }; ``` ## Reading Order | Task | Files | |------|-------| | Setup new container project | README → configuration.md | | Implement container logic | README → api.md → patterns.md | | Choose routing pattern | patterns.md (routing section) | | Debug issues | gotchas.md | | Production hardening | gotchas.md → patterns.md (lifecycle) | ## Routing Decision Tree **How should requests reach containers?** - **Same user/session → same container:** Use `getByName(sessionId)` for session affinity - **Stateless, spread load:** Use `getRandom()` for load balancing - **Job per container:** Use `getByName(jobId)` + explicit lifecycle management - **Single global instance:** Use `getByName("singleton")` ## When to Use Containers vs Workers **Use Containers when:** - Need stateful, long-lived processes (sessions, WebSockets, games) - Running existing containerized apps (Node.js, Python, custom binaries) - Need filesystem access or specific system dependencies - Per-user/session isolation with dedicated compute **Use Workers when:** - Stateless HTTP handlers - Sub-millisecond cold starts required - Auto-scaling to zero critical - Simple request/response patterns ## In This Reference - **[configuration.md](configuration.md)** - Wrangler config, instance types, Container class properties, environment variables, account limits - **[api.md](api.md)** - Container class API, startup methods, communication (HTTP/TCP/WebSocket), routing helpers, lifecycle hooks, scheduling, state inspection - **[patterns.md](patterns.md)** - Routing patterns (session affinity, load balancing, singleton), WebSocket forwarding, graceful shutdown, Workflow/Queue integration - **[gotchas.md](gotchas.md)** - Critical gotchas (WebSocket, startup methods), common errors with solutions, specific limits, beta caveats ## See Also - [Durable Objects](../durable-objects/) - Containers extend Durable Objects - [Workflows](../workflows/) - Orchestrate container operations - [Queues](../queues/) - Trigger containers from queue messages - [Cloudflare Docs](https://developers.cloudflare.com/containers/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/containers/api.md ================================================ ## Container Class API ```typescript import { Container } from "@cloudflare/containers"; export class MyContainer extends Container { defaultPort = 8080; requiredPorts = [8080]; sleepAfter = "30m"; enableInternet = true; pingEndpoint = "/health"; envVars = {}; entrypoint = []; onStart() { /* container started */ } onStop() { /* container stopping */ } onError(error: Error) { /* container error */ } onActivityExpired(): boolean { /* timeout, return true to stay alive */ } async alarm() { /* scheduled task */ } } ``` ## Routing **getByName(id)** - Named instance for session affinity, per-user state **getRandom()** - Random instance for load balancing stateless services ```typescript const container = env.MY_CONTAINER.getByName("user-123"); const container = env.MY_CONTAINER.getRandom(); ``` ## Startup Methods ### start() - Basic start (8s timeout) ```typescript await container.start(); await container.start({ envVars: { KEY: "value" } }); ``` Returns when **process starts**, NOT when ports ready. Use for fire-and-forget. ### startAndWaitForPorts() - Recommended (20s timeout) ```typescript await container.startAndWaitForPorts(); // Uses requiredPorts await container.startAndWaitForPorts({ ports: [8080, 9090] }); await container.startAndWaitForPorts({ ports: [8080], startOptions: { envVars: { KEY: "value" } } }); ``` Returns when **ports listening**. Use before HTTP/TCP requests. **Port resolution:** explicit ports → requiredPorts → defaultPort → port 33 ### waitForPort() - Wait for specific port ```typescript await container.waitForPort(8080); await container.waitForPort(8080, { timeout: 30000 }); ``` ## Communication ### fetch() - HTTP with WebSocket support ```typescript // ✅ Supports WebSocket upgrades const response = await container.fetch(request); const response = await container.fetch("http://container/api", { method: "POST", body: JSON.stringify({ data: "value" }) }); ``` **Use for:** All HTTP, especially WebSocket. ### containerFetch() - HTTP only (no WebSocket) ```typescript // ❌ No WebSocket support const response = await container.containerFetch(request); ``` **⚠️ Critical:** Use `fetch()` for WebSocket, not `containerFetch()`. ### TCP Connections ```typescript const port = this.ctx.container.getTcpPort(8080); const conn = port.connect(); await conn.opened; if (request.body) await request.body.pipeTo(conn.writable); return new Response(conn.readable); ``` ### switchPort() - Change default port ```typescript this.switchPort(8081); // Subsequent fetch() uses this port ``` ## Lifecycle Hooks ### onStart() Called when container process starts (ports may not be ready). Runs in `blockConcurrencyWhile` - no concurrent requests. ```typescript onStart() { console.log("Container starting"); } ``` ### onStop() Called when SIGTERM received. 15 minutes until SIGKILL. Use for graceful shutdown. ```typescript onStop() { // Save state, close connections, flush logs } ``` ### onError() Called when container crashes or fails to start. ```typescript onError(error: Error) { console.error("Container error:", error); } ``` ### onActivityExpired() Called when `sleepAfter` timeout reached. Return `true` to stay alive, `false` to stop. ```typescript onActivityExpired(): boolean { if (this.hasActiveConnections()) return true; // Keep alive return false; // OK to stop } ``` ## Scheduling ```typescript export class ScheduledContainer extends Container { async fetch(request: Request) { await this.schedule(Date.now() + 60000); // 1 minute await this.schedule("2026-01-28T00:00:00Z"); // ISO string return new Response("Scheduled"); } async alarm() { // Called when schedule fires (SQLite-backed, survives restarts) } } ``` **⚠️ Don't override `alarm()` directly when using `schedule()` helper.** ## State Inspection ### External state check ```typescript const state = await container.getState(); // state.status: "starting" | "running" | "stopping" | "stopped" ``` ### Internal state check ```typescript export class MyContainer extends Container { async fetch(request: Request) { if (this.ctx.container.running) { ... } } } ``` **⚠️ Use `getState()` for external checks, `ctx.container.running` for internal.** ================================================ FILE: skills/.curated/cloudflare-deploy/references/containers/configuration.md ================================================ ## Wrangler Configuration ### Basic Container Config ```jsonc { "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-01-10", "containers": [ { "class_name": "MyContainer", "image": "./Dockerfile", // Path to Dockerfile or directory with Dockerfile "instance_type": "standard-1", // Predefined or custom (see below) "max_instances": 10 } ], "durable_objects": { "bindings": [ { "name": "MY_CONTAINER", "class_name": "MyContainer" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MyContainer"] // Must use new_sqlite_classes } ] } ``` Key config requirements: - `image` - Path to Dockerfile or directory containing Dockerfile - `class_name` - Must match Container class export name - `max_instances` - Max concurrent container instances - Must configure Durable Objects binding AND migrations ### Instance Types #### Predefined Types | Type | vCPU | Memory | Disk | |------|------|--------|------| | lite | 1/16 | 256 MiB | 2 GB | | basic | 1/4 | 1 GiB | 4 GB | | standard-1 | 1/2 | 4 GiB | 8 GB | | standard-2 | 1 | 6 GiB | 12 GB | | standard-3 | 2 | 8 GiB | 16 GB | | standard-4 | 4 | 12 GiB | 20 GB | ```jsonc { "containers": [ { "class_name": "MyContainer", "image": "./Dockerfile", "instance_type": "standard-2" // Use predefined type } ] } ``` #### Custom Types (Jan 2026 Feature) ```jsonc { "containers": [ { "class_name": "MyContainer", "image": "./Dockerfile", "instance_type_custom": { "vcpu": 2, // 1-4 vCPU "memory_mib": 8192, // 512-12288 MiB (up to 12 GiB) "disk_mib": 16384 // 2048-20480 MiB (up to 20 GB) } } ] } ``` **Custom type constraints:** - Minimum 3 GiB memory per vCPU - Maximum 2 GB disk per 1 GiB memory - Max 4 vCPU, 12 GiB memory, 20 GB disk per container ### Account Limits | Resource | Limit | Notes | |----------|-------|-------| | Total memory (all containers) | 400 GiB | Across all running containers | | Total vCPU (all containers) | 100 | Across all running containers | | Total disk (all containers) | 2 TB | Across all running containers | | Image storage per account | 50 GB | Stored container images | ### Container Class Properties ```typescript import { Container } from "@cloudflare/containers"; export class MyContainer extends Container { // Port Configuration defaultPort = 8080; // Default port for fetch() calls requiredPorts = [8080, 9090]; // Ports to wait for in startAndWaitForPorts() // Lifecycle sleepAfter = "30m"; // Inactivity timeout (5m, 30m, 2h, etc.) // Network enableInternet = true; // Allow outbound internet access // Health Check pingEndpoint = "/health"; // Health check endpoint path // Environment envVars = { // Environment variables passed to container NODE_ENV: "production", LOG_LEVEL: "info" }; // Startup entrypoint = ["/bin/start.sh"]; // Override image entrypoint (optional) } ``` **Property details:** - **`defaultPort`**: Port used when calling `container.fetch()` without explicit port. Falls back to port 33 if not set. - **`requiredPorts`**: Array of ports that must be listening before `startAndWaitForPorts()` returns. First port becomes default if `defaultPort` not set. - **`sleepAfter`**: Duration string (e.g., "5m", "30m", "2h"). Container stops after this period of inactivity. Timer resets on each request. - **`enableInternet`**: Boolean. If `true`, container can make outbound HTTP/TCP requests. - **`pingEndpoint`**: Path used for health checks. Should respond with 2xx status. - **`envVars`**: Object of environment variables. Merged with runtime-provided vars (see below). - **`entrypoint`**: Array of strings. Overrides container image's CMD/ENTRYPOINT. ### Runtime Environment Variables Cloudflare automatically provides these environment variables to containers: | Variable | Description | |----------|-------------| | `CLOUDFLARE_APPLICATION_ID` | Worker application ID | | `CLOUDFLARE_COUNTRY_A2` | Two-letter country code of request origin | | `CLOUDFLARE_LOCATION` | Cloudflare data center location | | `CLOUDFLARE_REGION` | Region identifier | | `CLOUDFLARE_DURABLE_OBJECT_ID` | Container's Durable Object ID | Custom `envVars` from Container class are merged with these. Custom vars override runtime vars if names conflict. ### Image Management **Distribution model:** Images pre-fetched to all global locations before deployment. Ensures fast cold starts (2-3s typical). **Rolling deploys:** Unlike Workers (instant), container deployments roll out gradually. Old versions continue running during rollout. **Ephemeral disk:** Container disk is ephemeral and resets on each stop. Use Durable Object storage (`this.ctx.storage`) for persistence. ## wrangler.toml Format ```toml name = "my-worker" main = "src/index.ts" compatibility_date = "2026-01-10" [[containers]] class_name = "MyContainer" image = "./Dockerfile" instance_type = "standard-2" max_instances = 10 [[durable_objects.bindings]] name = "MY_CONTAINER" class_name = "MyContainer" [[migrations]] tag = "v1" new_sqlite_classes = ["MyContainer"] ``` Both `wrangler.jsonc` and `wrangler.toml` are supported. Use `wrangler.jsonc` for comments and better IDE support. ================================================ FILE: skills/.curated/cloudflare-deploy/references/containers/gotchas.md ================================================ ## Critical Gotchas ### ⚠️ WebSocket: fetch() vs containerFetch() **Problem:** WebSocket connections fail silently **Cause:** `containerFetch()` doesn't support WebSocket upgrades **Fix:** Always use `fetch()` for WebSocket ```typescript // ❌ WRONG return container.containerFetch(request); // ✅ CORRECT return container.fetch(request); ``` ### ⚠️ startAndWaitForPorts() vs start() **Problem:** "connection refused" after `start()` **Cause:** `start()` returns when process starts, NOT when ports ready **Fix:** Use `startAndWaitForPorts()` before requests ```typescript // ❌ WRONG await container.start(); return container.fetch(request); // ✅ CORRECT await container.startAndWaitForPorts(); return container.fetch(request); ``` ### ⚠️ Activity Timeout on Long Operations **Problem:** Container stops during long work **Cause:** `sleepAfter` based on request activity, not internal work **Fix:** Renew timeout by touching storage ```typescript const interval = setInterval(() => { this.ctx.storage.put("keepalive", Date.now()); }, 60000); try { await this.doLongWork(data); } finally { clearInterval(interval); } ``` ### ⚠️ blockConcurrencyWhile for Startup **Problem:** Race conditions during initialization **Fix:** Use `blockConcurrencyWhile` for atomic initialization ```typescript await this.ctx.blockConcurrencyWhile(async () => { if (!this.initialized) { await this.startAndWaitForPorts(); this.initialized = true; } }); ``` ### ⚠️ Lifecycle Hooks Block Requests **Problem:** Container unresponsive during `onStart()` **Cause:** Hooks run in `blockConcurrencyWhile` - no concurrent requests **Fix:** Keep hooks fast, avoid long operations ### ⚠️ Don't Override alarm() When Using schedule() **Problem:** Scheduled tasks don't execute **Cause:** `schedule()` uses `alarm()` internally **Fix:** Implement `alarm()` to handle scheduled tasks ## Common Errors ### "Container start timeout" **Cause:** Container took >8s (`start()`) or >20s (`startAndWaitForPorts()`) **Solutions:** - Optimize image (smaller base, fewer layers) - Check `entrypoint` correct - Verify app listens on correct ports - Increase timeout if needed ### "Port not available" **Cause:** Calling `fetch()` before port ready **Solution:** Use `startAndWaitForPorts()` ### "Container memory exceeded" **Cause:** Using more memory than instance type allows **Solutions:** - Use larger instance type (standard-2, standard-3, standard-4) - Optimize app memory usage - Use custom instance type ```jsonc "instance_type_custom": { "vcpu": 2, "memory_mib": 8192 } ``` ### "Max instances reached" **Cause:** All `max_instances` slots in use **Solutions:** - Increase `max_instances` - Implement proper `sleepAfter` - Use `getRandom()` for distribution - Check for instance leaks ### "No container instance available" **Cause:** Account capacity limits reached **Solutions:** - Check account limits - Review instance types across containers - Contact Cloudflare support ## Limits | Resource | Limit | Notes | |----------|-------|-------| | Cold start | 2-3s | Image pre-fetched globally | | Graceful shutdown | 15 min | SIGTERM → SIGKILL | | `start()` timeout | 8s | Process start | | `startAndWaitForPorts()` timeout | 20s | Port ready | | Max vCPU per container | 4 | standard-4 or custom | | Max memory per container | 12 GiB | standard-4 or custom | | Max disk per container | 20 GB | Ephemeral, resets | | Account total memory | 400 GiB | All containers | | Account total vCPU | 100 | All containers | | Account total disk | 2 TB | All containers | | Image storage | 50 GB | Per account | | Disk persistence | None | Use DO storage | ## Best Practices 1. **Use `startAndWaitForPorts()` by default** - Prevents port errors 2. **Set appropriate `sleepAfter`** - Balance resources vs cold starts 3. **Use `fetch()` for WebSocket** - Not `containerFetch()` 4. **Design for restarts** - Ephemeral disk, implement graceful shutdown 5. **Monitor resources** - Stay within account limits 6. **Keep hooks fast** - Run in `blockConcurrencyWhile` 7. **Renew activity for long ops** - Touch storage to prevent timeout ## Beta Caveats ⚠️ Containers in **beta**: - **API may change** without notice - **No SLA** guarantees - **Limited regions** initially - **No autoscaling** - manual via `getRandom()` - **Rolling deploys** only (not instant like Workers) Plan for API changes, test thoroughly before production. ================================================ FILE: skills/.curated/cloudflare-deploy/references/containers/patterns.md ================================================ ## Routing Patterns ### Session Affinity (Stateful) ```typescript export class SessionBackend extends Container { defaultPort = 3000; sleepAfter = "30m"; } export default { async fetch(request: Request, env: Env) { const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID(); const container = env.SESSION_BACKEND.getByName(sessionId); await container.startAndWaitForPorts(); return container.fetch(request); } }; ``` **Use:** User sessions, WebSocket, stateful games, per-user caching. ### Load Balancing (Stateless) ```typescript export default { async fetch(request: Request, env: Env) { const container = env.STATELESS_API.getRandom(); await container.startAndWaitForPorts(); return container.fetch(request); } }; ``` **Use:** Stateless HTTP APIs, CPU-intensive work, read-only queries. ### Singleton Pattern ```typescript export default { async fetch(request: Request, env: Env) { const container = env.GLOBAL_SERVICE.getByName("singleton"); await container.startAndWaitForPorts(); return container.fetch(request); } }; ``` **Use:** Global cache, centralized coordinator, single source of truth. ## WebSocket Forwarding ```typescript export default { async fetch(request: Request, env: Env) { if (request.headers.get("Upgrade") === "websocket") { const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID(); const container = env.WS_BACKEND.getByName(sessionId); await container.startAndWaitForPorts(); // ⚠️ MUST use fetch(), not containerFetch() return container.fetch(request); } return new Response("Not a WebSocket request", { status: 400 }); } }; ``` **⚠️ Critical:** Always use `fetch()` for WebSocket. ## Graceful Shutdown ```typescript export class GracefulContainer extends Container { private connections = new Set(); onStop() { // SIGTERM received, 15 minutes until SIGKILL for (const ws of this.connections) { ws.close(1001, "Server shutting down"); } this.ctx.storage.put("shutdown-time", Date.now()); } onActivityExpired(): boolean { return this.connections.size > 0; // Keep alive if connections } } ``` ## Concurrent Request Handling ```typescript export class SafeContainer extends Container { private initialized = false; async fetch(request: Request) { await this.ctx.blockConcurrencyWhile(async () => { if (!this.initialized) { await this.startAndWaitForPorts(); this.initialized = true; } }); return super.fetch(request); } } ``` **Use:** One-time initialization, preventing concurrent startup. ## Activity Timeout Renewal ```typescript export class LongRunningContainer extends Container { sleepAfter = "5m"; async processLongJob(data: unknown) { const interval = setInterval(() => { this.ctx.storage.put("keepalive", Date.now()); }, 60000); try { await this.doLongWork(data); } finally { clearInterval(interval); } } } ``` **Use:** Long operations exceeding `sleepAfter`. ## Multiple Port Routing ```typescript export class MultiPortContainer extends Container { requiredPorts = [8080, 8081, 9090]; async fetch(request: Request) { const path = new URL(request.url).pathname; if (path.startsWith("/grpc")) this.switchPort(8081); else if (path.startsWith("/metrics")) this.switchPort(9090); return super.fetch(request); } } ``` **Use:** Multi-protocol services (HTTP + gRPC), separate metrics endpoints. ## Workflow Integration ```typescript import { WorkflowEntrypoint } from "cloudflare:workers"; export class ProcessingWorkflow extends WorkflowEntrypoint { async run(event, step) { const container = this.env.PROCESSOR.getByName(event.payload.jobId); await step.do("start", async () => { await container.startAndWaitForPorts(); }); const result = await step.do("process", async () => { return container.fetch("/process", { method: "POST", body: JSON.stringify(event.payload.data) }).then(r => r.json()); }); return result; } } ``` **Use:** Orchestrating multi-step container operations, durable execution. ## Queue Consumer Integration ```typescript export default { async queue(batch, env) { for (const msg of batch.messages) { try { const container = env.PROCESSOR.getByName(msg.body.jobId); await container.startAndWaitForPorts(); const response = await container.fetch("/process", { method: "POST", body: JSON.stringify(msg.body) }); response.ok ? msg.ack() : msg.retry(); } catch (err) { console.error("Queue processing error:", err); msg.retry(); } } } }; ``` **Use:** Asynchronous job processing, batch operations, event-driven execution. ================================================ FILE: skills/.curated/cloudflare-deploy/references/cron-triggers/README.md ================================================ # Cloudflare Cron Triggers Schedule Workers execution using cron expressions. Runs on Cloudflare's global network during underutilized periods. ## Key Features - **UTC-only execution** - All schedules run on UTC time - **5-field cron syntax** - Quartz scheduler extensions (L, W, #) - **Global propagation** - 15min deployment delay - **At-least-once delivery** - Rare duplicate executions possible - **Workflow integration** - Trigger long-running multi-step tasks - **Green Compute** - Optional carbon-aware scheduling during low-carbon periods ## Cron Syntax ``` ┌─────────── minute (0-59) │ ┌───────── hour (0-23) │ │ ┌─────── day of month (1-31) │ │ │ ┌───── month (1-12, JAN-DEC) │ │ │ │ ┌─── day of week (1-7, SUN-SAT, 1=Sunday) * * * * * ``` **Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth) ## Common Schedules ```bash */5 * * * * # Every 5 minutes 0 * * * * # Hourly 0 2 * * * # Daily 2am UTC (off-peak) 0 9 * * MON-FRI # Weekdays 9am UTC 0 0 1 * * # Monthly 1st midnight UTC 0 9 L * * # Last day of month 9am UTC 0 10 * * MON#2 # 2nd Monday 10am UTC */10 9-17 * * MON-FRI # Every 10min, 9am-5pm weekdays ``` ## Quick Start **wrangler.jsonc:** ```jsonc { "name": "my-cron-worker", "triggers": { "crons": ["*/5 * * * *", "0 2 * * *"] } } ``` **Handler:** ```typescript export default { async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ): Promise { console.log("Cron:", controller.cron); console.log("Time:", new Date(controller.scheduledTime)); ctx.waitUntil(asyncTask(env)); // Non-blocking }, }; ``` **Test locally:** ```bash npx wrangler dev curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*" ``` ## Limits - **Free:** 3 triggers/worker, 10ms CPU - **Paid:** Unlimited triggers, 50ms CPU - **Propagation:** 15min global deployment - **Timezone:** UTC only ## Reading Order **New to cron triggers?** Start here: 1. This README - Overview and quick start 2. [configuration.md](./configuration.md) - Set up your first cron trigger 3. [api.md](./api.md) - Understand the handler API 4. [patterns.md](./patterns.md) - Common use cases and examples **Troubleshooting?** Jump to [gotchas.md](./gotchas.md) ## In This Reference - [configuration.md](./configuration.md) - wrangler config, env-specific schedules, Green Compute - [api.md](./api.md) - ScheduledController, noRetry(), waitUntil, testing patterns - [patterns.md](./patterns.md) - Use cases, monitoring, queue integration, Durable Objects - [gotchas.md](./gotchas.md) - Timezone issues, idempotency, security, testing ## See Also - [workflows](../workflows/) - Alternative for long-running scheduled tasks - [workers](../workers/) - Worker runtime documentation ================================================ FILE: skills/.curated/cloudflare-deploy/references/cron-triggers/api.md ================================================ # Cron Triggers API ## Basic Handler ```typescript export default { async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise { console.log("Cron executed:", new Date(controller.scheduledTime)); }, }; ``` **JavaScript:** Same signature without types **Python:** `class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx)` ## ScheduledController ```typescript interface ScheduledController { scheduledTime: number; // Unix ms when scheduled to run cron: string; // Expression that triggered (e.g., "*/5 * * * *") type: string; // Always "scheduled" noRetry(): void; // Prevent automatic retry on failure } ``` **Prevent retry on failure:** ```typescript export default { async scheduled(controller, env, ctx) { try { await riskyOperation(env); } catch (error) { // Don't retry - failure is expected/acceptable controller.noRetry(); console.error("Operation failed, not retrying:", error); } }, }; ``` **When to use noRetry():** - External API failures outside your control (avoid hammering failed services) - Rate limit errors (retry would fail again immediately) - Duplicate execution detected (idempotency check failed) - Non-critical operations where skip is acceptable (analytics, caching) - Validation errors that won't resolve on retry ## Handler Parameters **`controller: ScheduledController`** - Access cron expression and scheduled time **`env: Env`** - All bindings: KV, R2, D1, secrets, service bindings **`ctx: ExecutionContext`** - `ctx.waitUntil(promise)` - Extend execution for async tasks (logging, cleanup, external APIs) - First `waitUntil` failure recorded in Cron Events ## Multiple Schedules ```typescript export default { async scheduled(controller, env, ctx) { switch (controller.cron) { case "*/3 * * * *": ctx.waitUntil(updateRecentData(env)); break; case "0 * * * *": ctx.waitUntil(processHourlyAggregation(env)); break; case "0 2 * * *": ctx.waitUntil(performDailyMaintenance(env)); break; default: console.warn(`Unhandled: ${controller.cron}`); } }, }; ``` ## ctx.waitUntil Usage ```typescript export default { async scheduled(controller, env, ctx) { const data = await fetchCriticalData(); // Critical path // Non-blocking background tasks ctx.waitUntil(Promise.all([ logToAnalytics(data), cleanupOldRecords(env.DB), notifyWebhook(env.WEBHOOK_URL, data), ])); }, }; ``` ## Workflow Integration ```typescript import { WorkflowEntrypoint } from "cloudflare:workers"; export class DataProcessingWorkflow extends WorkflowEntrypoint { async run(event, step) { const data = await step.do("fetch-data", () => fetchLargeDataset()); const processed = await step.do("process-data", () => processDataset(data)); await step.do("store-results", () => storeResults(processed)); } } export default { async scheduled(controller, env, ctx) { const instance = await env.MY_WORKFLOW.create({ params: { scheduledTime: controller.scheduledTime, cron: controller.cron }, }); console.log(`Started workflow: ${instance.id}`); }, }; ``` ## Testing Handler **Local development (/__scheduled endpoint):** ```bash # Start dev server npx wrangler dev # Trigger any cron curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*" # Trigger specific cron with custom time curl "http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000" ``` **Query parameters:** - `cron` - Required. URL-encoded cron expression (use `+` for spaces) - `scheduledTime` - Optional. Unix timestamp in milliseconds (defaults to current time) **Production security:** The `/__scheduled` endpoint is available in production and can be triggered by anyone. Block it or implement authentication - see [gotchas.md](./gotchas.md#security-concerns) **Unit testing (Vitest):** ```typescript // test/scheduled.test.ts import { describe, it, expect } from "vitest"; import { env } from "cloudflare:test"; import worker from "../src/index"; describe("Scheduled Handler", () => { it("processes scheduled event", async () => { const controller = { scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled" as const, noRetry: () => {} }; const ctx = { waitUntil: (p: Promise) => p, passThroughOnException: () => {} }; await worker.scheduled(controller, env, ctx); expect(await env.MY_KV.get("last_run")).toBeDefined(); }); it("handles multiple crons", async () => { const ctx = { waitUntil: () => {}, passThroughOnException: () => {} }; await worker.scheduled({ scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled", noRetry: () => {} }, env, ctx); expect(await env.MY_KV.get("last_type")).toBe("frequent"); }); }); ``` ## Error Handling **Automatic retries:** - Failed cron executions are retried automatically unless `noRetry()` is called - Retry happens after a delay (typically minutes) - Only first `waitUntil()` failure is recorded in Cron Events **Best practices:** ```typescript export default { async scheduled(controller, env, ctx) { try { await criticalOperation(env); } catch (error) { // Log error details console.error("Cron failed:", { cron: controller.cron, scheduledTime: controller.scheduledTime, error: error.message, stack: error.stack, }); // Decide: retry or skip if (error.message.includes("rate limit")) { controller.noRetry(); // Skip retry for rate limits } // Otherwise allow automatic retry throw error; } }, }; ``` ## See Also - [README.md](./README.md) - Overview - [patterns.md](./patterns.md) - Use cases, examples - [gotchas.md](./gotchas.md) - Common errors, testing issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/cron-triggers/configuration.md ================================================ # Cron Triggers Configuration ## wrangler.jsonc ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-cron-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date for new projects "triggers": { "crons": [ "*/5 * * * *", // Every 5 minutes "0 */2 * * *", // Every 2 hours "0 9 * * MON-FRI", // Weekdays at 9am UTC "0 2 1 * *" // Monthly on 1st at 2am UTC ] } } ``` ## Green Compute (Beta) Schedule crons during low-carbon periods for carbon-aware execution: ```jsonc { "name": "eco-cron-worker", "triggers": { "crons": ["0 2 * * *"] }, "placement": { "mode": "smart" // Runs during low-carbon periods } } ``` **Modes:** - `"smart"` - Carbon-aware scheduling (may delay up to 24h for optimal window) - Default (no placement config) - Standard scheduling (no delay) **How it works:** - Cloudflare delays execution until grid carbon intensity is lower - Maximum delay: 24 hours from scheduled time - Ideal for batch jobs with flexible timing requirements **Use cases:** - Nightly data processing and ETL pipelines - Weekly/monthly report generation - Database backups and maintenance - Analytics aggregation - ML model training **Not suitable for:** - Time-sensitive operations (SLA requirements) - User-facing features requiring immediate execution - Real-time monitoring and alerting - Compliance tasks with strict time windows ## Environment-Specific Schedules ```jsonc { "name": "my-cron-worker", "triggers": { "crons": ["0 */6 * * *"] // Prod: every 6 hours }, "env": { "staging": { "triggers": { "crons": ["*/15 * * * *"] // Staging: every 15min } }, "dev": { "triggers": { "crons": ["*/5 * * * *"] // Dev: every 5min } } } } ``` ## Schedule Format **Structure:** `minute hour day-of-month month day-of-week` **Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth) ## Managing Triggers **Remove all:** `"triggers": { "crons": [] }` **Preserve existing:** Omit `"triggers"` field entirely ## Deployment ```bash # Deploy with config crons npx wrangler deploy # Deploy specific environment npx wrangler deploy --env production # View deployments npx wrangler deployments list ``` **⚠️ Changes take up to 15 minutes to propagate globally** ## API Management **Get triggers:** ```bash curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \ -H "Authorization: Bearer {api_token}" ``` **Update triggers:** ```bash curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \ -H "Authorization: Bearer {api_token}" \ -H "Content-Type: application/json" \ -d '{"crons": ["*/5 * * * *", "0 2 * * *"]}' ``` **Delete all:** ```bash curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \ -H "Authorization: Bearer {api_token}" \ -H "Content-Type: application/json" \ -d '{"crons": []}' ``` ## Combining Multiple Workers For complex schedules, use multiple workers: ```jsonc // worker-frequent.jsonc { "name": "data-sync-frequent", "triggers": { "crons": ["*/5 * * * *"] } } // worker-daily.jsonc { "name": "reports-daily", "triggers": { "crons": ["0 2 * * *"] }, "placement": { "mode": "smart" } } // worker-weekly.jsonc { "name": "cleanup-weekly", "triggers": { "crons": ["0 3 * * SUN"] } } ``` **Benefits:** - Separate CPU limits per worker - Independent error isolation - Different Green Compute policies - Easier to maintain and debug ## Validation **Test cron syntax:** - [crontab.guru](https://crontab.guru/) - Interactive validator - Wrangler validates on deploy but won't catch logic errors **Common mistakes:** - `0 0 * * *` runs daily at midnight UTC, not your local timezone - `*/60 * * * *` is invalid (use `0 * * * *` for hourly) - `0 2 31 * *` only runs on months with 31 days ## See Also - [README.md](./README.md) - Overview, quick start - [api.md](./api.md) - Handler implementation - [patterns.md](./patterns.md) - Multi-cron routing examples ================================================ FILE: skills/.curated/cloudflare-deploy/references/cron-triggers/gotchas.md ================================================ # Cron Triggers Gotchas ## Common Errors ### "Timezone Issues" **Problem:** Cron runs at wrong time relative to local timezone **Cause:** All crons execute in UTC, no local timezone support **Solution:** Convert local time to UTC manually **Conversion formula:** `utcHour = (localHour - utcOffset + 24) % 24` **Examples:** - 9am PST (UTC-8) → `(9 - (-8) + 24) % 24 = 17` → `0 17 * * *` - 2am EST (UTC-5) → `(2 - (-5) + 24) % 24 = 7` → `0 7 * * *` - 6pm JST (UTC+9) → `(18 - 9 + 24) % 24 = 33 % 24 = 9` → `0 9 * * *` **Daylight Saving Time:** Adjust manually when DST changes, or schedule at times unaffected by DST (e.g., 2am-4am local time usually safe) ### "Cron Not Executing" **Cause:** Missing `scheduled()` export, invalid syntax, propagation delay (<15min), or plan limits **Solution:** Verify export exists, validate at crontab.guru, wait 15+ min after deploy, check plan limits ### "Duplicate Executions" **Cause:** At-least-once delivery **Solution:** Track execution IDs in KV - see idempotency pattern below ### "Execution Failures" **Cause:** CPU exceeded, unhandled exceptions, network timeouts, binding errors **Solution:** Use try-catch, AbortController timeouts, `ctx.waitUntil()` for long ops, or Workflows for heavy tasks ### "Local Testing Not Working" **Problem:** `/__scheduled` endpoint returns 404 or doesn't trigger handler **Cause:** Missing `scheduled()` export, wrangler not running, or incorrect endpoint format **Solution:** 1. Verify `scheduled()` is exported: ```typescript export default { async scheduled(controller, env, ctx) { console.log("Cron triggered"); }, }; ``` 2. Start dev server: ```bash npx wrangler dev ``` 3. Use correct endpoint format (URL-encode spaces as `+`): ```bash # Correct curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*" # Wrong (will fail) curl "http://localhost:8787/__scheduled?cron=*/5 * * * *" ``` 4. Update Wrangler if outdated: ```bash npm install -g wrangler@latest ``` ### "waitUntil() Tasks Not Completing" **Problem:** Background tasks in `ctx.waitUntil()` fail silently or don't execute **Cause:** Promises rejected without error handling, or handler returns before promise settles **Solution:** Always await or handle errors in waitUntil promises: ```typescript export default { async scheduled(controller, env, ctx) { // BAD: Silent failures ctx.waitUntil(riskyOperation()); // GOOD: Explicit error handling ctx.waitUntil( riskyOperation().catch(err => { console.error("Background task failed:", err); return logError(err, env); }) ); }, }; ``` ### "Idempotency Issues" **Problem:** At-least-once delivery causes duplicate side effects (double charges, duplicate emails) **Cause:** No deduplication mechanism **Solution:** Use KV to track execution IDs: ```typescript export default { async scheduled(controller, env, ctx) { const executionId = `${controller.cron}-${controller.scheduledTime}`; const existing = await env.EXECUTIONS.get(executionId); if (existing) { console.log("Already executed, skipping"); controller.noRetry(); return; } await env.EXECUTIONS.put(executionId, "1", { expirationTtl: 86400 }); // 24h TTL await performIdempotentOperation(env); }, }; ``` ### "Security Concerns" **Problem:** `__scheduled` endpoint exposed in production allows unauthorized cron triggering **Cause:** Testing endpoint available in deployed Workers **Solution:** Block `__scheduled` in production: ```typescript export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Block __scheduled in production if (url.pathname === "/__scheduled" && env.ENVIRONMENT === "production") { return new Response("Not Found", { status: 404 }); } return handleRequest(request, env, ctx); }, async scheduled(controller, env, ctx) { // Your cron logic }, }; ``` **Also:** Use `env.API_KEY` for secrets (never hardcode) **Alternative:** Add middleware to verify request origin: ```typescript export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (url.pathname === "/__scheduled") { // Check Cloudflare headers to verify internal request const cfRay = request.headers.get("cf-ray"); if (!cfRay && env.ENVIRONMENT === "production") { return new Response("Not Found", { status: 404 }); } } return handleRequest(request, env, ctx); }, async scheduled(controller, env, ctx) { // Your cron logic }, }; ``` ## Limits & Quotas | Limit | Free | Paid | Notes | |-------|------|------|-------| | Triggers per Worker | 3 | Unlimited | Maximum cron schedules per Worker | | CPU time | 10ms | 50ms | May need `ctx.waitUntil()` or Workflows | | Execution guarantee | At-least-once | At-least-once | Duplicates possible - use idempotency | | Propagation delay | Up to 15 minutes | Up to 15 minutes | Time for changes to take effect globally | | Min interval | 1 minute | 1 minute | Cannot schedule more frequently | | Cron accuracy | ±1 minute | ±1 minute | Execution may drift slightly | ## Testing Best Practices **Unit tests:** - Mock `ScheduledController`, `ExecutionContext`, and bindings - Test each cron expression separately - Verify `noRetry()` is called when expected - Use Vitest with `@cloudflare/vitest-pool-workers` for realistic env **Integration tests:** - Test via `/__scheduled` endpoint in dev environment - Verify idempotency logic with duplicate `scheduledTime` values - Test error handling and retry behavior **Production:** Start with long intervals (`*/30 * * * *`), monitor Cron Events for 24h, set up alerts before reducing interval ## Resources - [Cron Triggers Docs](https://developers.cloudflare.com/workers/configuration/cron-triggers/) - [Scheduled Handler API](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) - [Cloudflare Workflows](https://developers.cloudflare.com/workflows/) - [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/) - [Crontab Guru](https://crontab.guru/) - Validator - [Vitest Pool Workers](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples) ================================================ FILE: skills/.curated/cloudflare-deploy/references/cron-triggers/patterns.md ================================================ # Cron Triggers Patterns ## API Data Sync ```typescript export default { async scheduled(controller, env, ctx) { const response = await fetch("https://api.example.com/data", {headers: { "Authorization": `Bearer ${env.API_KEY}` }}); if (!response.ok) throw new Error(`API error: ${response.status}`); ctx.waitUntil(env.MY_KV.put("cached_data", JSON.stringify(await response.json()), {expirationTtl: 3600})); }, }; ``` ## Database Cleanup ```typescript export default { async scheduled(controller, env, ctx) { const result = await env.DB.prepare(`DELETE FROM sessions WHERE expires_at < datetime('now')`).run(); console.log(`Deleted ${result.meta.changes} expired sessions`); ctx.waitUntil(env.DB.prepare("VACUUM").run()); }, }; ``` ## Report Generation ```typescript export default { async scheduled(controller, env, ctx) { const startOfWeek = new Date(); startOfWeek.setDate(startOfWeek.getDate() - 7); const { results } = await env.DB.prepare(`SELECT date, revenue, orders FROM daily_stats WHERE date >= ? ORDER BY date`).bind(startOfWeek.toISOString()).all(); const report = {period: "weekly", totalRevenue: results.reduce((sum, d) => sum + d.revenue, 0), totalOrders: results.reduce((sum, d) => sum + d.orders, 0), dailyBreakdown: results}; const reportKey = `reports/weekly-${Date.now()}.json`; await env.REPORTS_BUCKET.put(reportKey, JSON.stringify(report)); ctx.waitUntil(env.SEND_EMAIL.fetch("https://example.com/send", {method: "POST", body: JSON.stringify({to: "team@example.com", subject: "Weekly Report", reportUrl: `https://reports.example.com/${reportKey}`})})); }, }; ``` ## Health Checks ```typescript export default { async scheduled(controller, env, ctx) { const services = [{name: "API", url: "https://api.example.com/health"}, {name: "CDN", url: "https://cdn.example.com/health"}]; const checks = await Promise.all(services.map(async (service) => { const start = Date.now(); try { const response = await fetch(service.url, { signal: AbortSignal.timeout(5000) }); return {name: service.name, status: response.ok ? "up" : "down", responseTime: Date.now() - start}; } catch (error) { return {name: service.name, status: "down", responseTime: Date.now() - start, error: error.message}; } })); ctx.waitUntil(env.STATUS_KV.put("health_status", JSON.stringify(checks))); const failures = checks.filter(c => c.status === "down"); if (failures.length > 0) ctx.waitUntil(fetch(env.ALERT_WEBHOOK, {method: "POST", body: JSON.stringify({text: `${failures.length} service(s) down: ${failures.map(f => f.name).join(", ")}`})})); }, }; ``` ## Batch Processing (Rate-Limited) ```typescript export default { async scheduled(controller, env, ctx) { const queueData = await env.QUEUE_KV.get("pending_items", "json"); if (!queueData || queueData.length === 0) return; const batch = queueData.slice(0, 100); const results = await Promise.allSettled(batch.map(item => fetch("https://api.example.com/process", {method: "POST", headers: {"Authorization": `Bearer ${env.API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify(item)}))); console.log(`Processed ${results.filter(r => r.status === "fulfilled").length}/${batch.length} items`); ctx.waitUntil(env.QUEUE_KV.put("pending_items", JSON.stringify(queueData.slice(100)))); }, }; ``` ## Queue Integration ```typescript export default { async scheduled(controller, env, ctx) { const batch = await env.MY_QUEUE.receive({ batchSize: 100 }); const results = await Promise.allSettled(batch.messages.map(async (msg) => { await processMessage(msg.body, env); await msg.ack(); })); console.log(`Processed ${results.filter(r => r.status === "fulfilled").length}/${batch.messages.length}`); }, }; ``` ## Monitoring & Observability ```typescript export default { async scheduled(controller, env, ctx) { const startTime = Date.now(); const meta = { cron: controller.cron, scheduledTime: controller.scheduledTime }; console.log("[START]", meta); try { const result = await performTask(env); console.log("[SUCCESS]", { ...meta, duration: Date.now() - startTime, count: result.count }); ctx.waitUntil(env.METRICS.put(`cron:${controller.scheduledTime}`, JSON.stringify({ ...meta, status: "success" }), { expirationTtl: 2592000 })); } catch (error) { console.error("[ERROR]", { ...meta, duration: Date.now() - startTime, error: error.message }); ctx.waitUntil(fetch(env.ALERT_WEBHOOK, { method: "POST", body: JSON.stringify({ text: `Cron failed: ${controller.cron}`, error: error.message }) })); throw error; } }, }; ``` **View logs:** `npx wrangler tail` or Dashboard → Workers & Pages → Worker → Logs ## Durable Objects Coordination ```typescript export default { async scheduled(controller, env, ctx) { const stub = env.COORDINATOR.get(env.COORDINATOR.idFromName("cron-lock")); const acquired = await stub.tryAcquireLock(controller.scheduledTime); if (!acquired) { controller.noRetry(); return; } try { await performTask(env); } finally { await stub.releaseLock(); } }, }; ``` ## Python Handler ```python from workers import WorkerEntrypoint class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx): data = await env.MY_KV.get("key") ctx.waitUntil(env.DB.execute("DELETE FROM logs WHERE created_at < datetime('now', '-7 days')")) ``` ## Testing Patterns **Local testing with /__scheduled:** ```bash # Start dev server npx wrangler dev # Test specific cron curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*" # Test with specific time curl "http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000" ``` **Unit tests:** ```typescript // test/scheduled.test.ts import { describe, it, expect, vi } from "vitest"; import { env } from "cloudflare:test"; import worker from "../src/index"; describe("Scheduled Handler", () => { it("executes cron", async () => { const controller = { scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled" as const, noRetry: vi.fn() }; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn() }; await worker.scheduled(controller, env, ctx); expect(await env.MY_KV.get("last_run")).toBeDefined(); }); it("calls noRetry on duplicate", async () => { const controller = { scheduledTime: 1704067200000, cron: "0 2 * * *", type: "scheduled" as const, noRetry: vi.fn() }; await env.EXECUTIONS.put("0 2 * * *-1704067200000", "1"); await worker.scheduled(controller, env, { waitUntil: vi.fn(), passThroughOnException: vi.fn() }); expect(controller.noRetry).toHaveBeenCalled(); }); }); ``` ## See Also - [README.md](./README.md) - Overview - [api.md](./api.md) - Handler implementation - [gotchas.md](./gotchas.md) - Troubleshooting ================================================ FILE: skills/.curated/cloudflare-deploy/references/d1/README.md ================================================ # Cloudflare D1 Database Expert guidance for Cloudflare D1, a serverless SQLite database designed for horizontal scale-out across multiple databases. ## Overview D1 is Cloudflare's managed, serverless database with: - SQLite SQL semantics and compatibility - Built-in disaster recovery via Time Travel (30-day point-in-time recovery) - Horizontal scale-out architecture (10 GB per database) - Worker and HTTP API access - Pricing based on query and storage costs only **Architecture Philosophy**: D1 is optimized for per-user, per-tenant, or per-entity database patterns rather than single large databases. ## Quick Start ```bash # Create database wrangler d1 create # Execute migration wrangler d1 migrations apply --remote # Local development wrangler dev ``` ## Core Query Methods ```typescript // .all() - Returns all rows; .first() - First row or null; .first(col) - Single column value // .run() - INSERT/UPDATE/DELETE; .raw() - Array of arrays (efficient) const { results, success, meta } = await env.DB.prepare('SELECT * FROM users WHERE active = ?').bind(true).all(); const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); ``` ## Batch Operations ```typescript // Multiple queries in single round trip (atomic transaction) const results = await env.DB.batch([ env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1), env.DB.prepare('SELECT * FROM posts WHERE author_id = ?').bind(1), env.DB.prepare('UPDATE users SET last_access = ? WHERE id = ?').bind(Date.now(), 1) ]); ``` ## Sessions API (Paid Plans) ```typescript // Create long-running session for analytics/migrations (up to 15 minutes) const session = env.DB.withSession(); try { await session.prepare('CREATE INDEX idx_heavy ON large_table(column)').run(); await session.prepare('ANALYZE').run(); } finally { session.close(); // Always close to release resources } ``` ## Read Replication (Paid Plans) ```typescript // Read from nearest replica for lower latency (automatic failover) const user = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); // Writes always go to primary await env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run(); ``` ## Platform Limits | Limit | Free Tier | Paid Plans | |-------|-----------|------------| | Database size | 500 MB | 10 GB per database | | Row size | 1 MB max | 1 MB max | | Query timeout | 30 seconds | 30 seconds | | Batch size | 1,000 statements | 10,000 statements | | Time Travel retention | 7 days | 30 days | | Read replicas | Not available | Yes (paid add-on) | **Pricing**: $5/month per database beyond free tier + $0.001 per 1K reads + $1 per 1M writes + $0.75/GB storage/month ## CLI Commands ```bash # Database management wrangler d1 create wrangler d1 list wrangler d1 delete # Migrations wrangler d1 migrations create # Create new migration file wrangler d1 migrations apply --remote # Apply pending migrations wrangler d1 migrations apply --local # Apply locally wrangler d1 migrations list --remote # Show applied migrations # Direct SQL execution wrangler d1 execute --remote --command="SELECT * FROM users" wrangler d1 execute --local --file=./schema.sql # Backups & Import/Export wrangler d1 export --remote --output=./backup.sql # Full export with schema wrangler d1 export --remote --no-schema --output=./data.sql # Data only wrangler d1 time-travel restore --timestamp="2024-01-15T14:30:00Z" # Point-in-time recovery # Development wrangler dev --persist-to=./.wrangler/state ``` ## Reading Order **Start here**: Quick Start above → configuration.md (setup) → api.md (queries) **Common tasks**: - First time setup: configuration.md → Run migrations - Adding queries: api.md → Prepared statements - Pagination/caching: patterns.md - Production optimization: Read Replication + Sessions API (this file) - Debugging: gotchas.md ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc setup, migrations, TypeScript types, ORMs, local dev - [api.md](./api.md) - Query methods (.all/.first/.run/.raw), batch, sessions, read replicas, error handling - [patterns.md](./patterns.md) - Pagination, bulk operations, caching, multi-tenant, sessions, analytics - [gotchas.md](./gotchas.md) - SQL injection, limits by plan tier, performance, common errors ## See Also - [workers](../workers/) - Worker runtime and fetch handler patterns - [hyperdrive](../hyperdrive/) - Connection pooling for external databases ================================================ FILE: skills/.curated/cloudflare-deploy/references/d1/api.md ================================================ # D1 API Reference ## Prepared Statements (Required for Security) ```typescript // ❌ NEVER: Direct string interpolation (SQL injection risk) const result = await env.DB.prepare(`SELECT * FROM users WHERE id = ${userId}`).all(); // ✅ CORRECT: Prepared statements with bind() const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all(); // Multiple parameters const result = await env.DB.prepare('SELECT * FROM users WHERE email = ? AND active = ?').bind(email, true).all(); ``` ## Query Execution Methods ```typescript // .all() - Returns all rows const { results, success, meta } = await env.DB.prepare('SELECT * FROM users WHERE active = ?').bind(true).all(); // results: Array of row objects; success: boolean // meta: { duration: number, rows_read: number, rows_written: number } // .first() - Returns first row or null const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); // .first(columnName) - Returns single column value const email = await env.DB.prepare('SELECT email FROM users WHERE id = ?').bind(userId).first('email'); // Returns string | number | null // .run() - For INSERT/UPDATE/DELETE (no row data returned) const result = await env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run(); // result.meta: { duration, rows_read, rows_written, last_row_id, changes } // .raw() - Returns array of arrays (efficient for large datasets) const rawResults = await env.DB.prepare('SELECT id, name FROM users').raw(); // [[1, 'Alice'], [2, 'Bob']] ``` ## Batch Operations ```typescript // Execute multiple queries in single round trip (atomic transaction) const results = await env.DB.batch([ env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1), env.DB.prepare('SELECT * FROM posts WHERE author_id = ?').bind(1), env.DB.prepare('UPDATE users SET last_access = ? WHERE id = ?').bind(Date.now(), 1) ]); // results is array: [result1, result2, result3] // Batch with same prepared statement, different params const userIds = [1, 2, 3]; const stmt = env.DB.prepare('SELECT * FROM users WHERE id = ?'); const results = await env.DB.batch(userIds.map(id => stmt.bind(id))); ``` ## Transactions (via batch) ```typescript // D1 executes batch() as atomic transaction - all succeed or all fail const results = await env.DB.batch([ env.DB.prepare('INSERT INTO accounts (id, balance) VALUES (?, ?)').bind(1, 100), env.DB.prepare('INSERT INTO accounts (id, balance) VALUES (?, ?)').bind(2, 200), env.DB.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').bind(50, 1), env.DB.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').bind(50, 2) ]); ``` ## Sessions API (Paid Plans) Long-running sessions for operations exceeding 30s timeout (up to 15 min). ```typescript const session = env.DB.withSession({ timeout: 600 }); // 10 min (1-900s) try { await session.prepare('CREATE INDEX idx_large ON big_table(column)').run(); await session.prepare('ANALYZE').run(); } finally { session.close(); // CRITICAL: always close to prevent leaks } ``` **Use cases**: Migrations, ANALYZE, large index creation, bulk transformations ## Read Replication (Paid Plans) Routes queries to nearest replica for lower latency. Writes always go to primary. ```typescript interface Env { DB: D1Database; // Primary (writes) DB_REPLICA: D1Database; // Replica (reads) } // Reads: use replica const user = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); // Writes: use primary await env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run(); // Read-after-write: use primary for consistency (replication lag <100ms-2s) await env.DB.prepare('INSERT INTO posts (title) VALUES (?)').bind(title).run(); const post = await env.DB.prepare('SELECT * FROM posts WHERE title = ?').bind(title).first(); // Primary ``` ## Error Handling ```typescript async function getUser(userId: number, env: Env): Promise { try { const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all(); if (!result.success) return new Response('Database error', { status: 500 }); if (result.results.length === 0) return new Response('User not found', { status: 404 }); return Response.json(result.results[0]); } catch (error) { return new Response('Internal error', { status: 500 }); } } // Constraint violations try { await env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind(email, name).run(); } catch (error) { if (error.message?.includes('UNIQUE constraint failed')) return new Response('Email exists', { status: 409 }); throw error; } ``` ## REST API (HTTP) Access Access D1 from external services (non-Worker contexts) using Cloudflare API. ```typescript // Single query const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/d1/database/${DATABASE_ID}/query`, { method: 'POST', headers: { 'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ sql: 'SELECT * FROM users WHERE id = ?', params: [userId] }) } ); const { result, success, errors } = await response.json(); // result: [{ results: [...], success: true, meta: {...} }] // Batch queries via HTTP const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/d1/database/${DATABASE_ID}/query`, { method: 'POST', headers: { 'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify([ { sql: 'SELECT * FROM users WHERE id = ?', params: [1] }, { sql: 'SELECT * FROM posts WHERE author_id = ?', params: [1] } ]) } ); ``` **Use cases**: Server-side scripts, CI/CD migrations, administrative tools, non-Worker integrations ## Testing & Debugging ```typescript // Vitest with unstable_dev import { unstable_dev } from 'wrangler'; describe('D1', () => { let worker: Awaited>; beforeAll(async () => { worker = await unstable_dev('src/index.ts'); }); afterAll(async () => { await worker.stop(); }); it('queries users', async () => { expect((await worker.fetch('/users')).status).toBe(200); }); }); // Debug query performance const result = await env.DB.prepare('SELECT * FROM users').all(); console.log('Duration:', result.meta.duration, 'ms'); // Query plan analysis const plan = await env.DB.prepare('EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?').bind(email).all(); ``` ```bash # Inspect local database sqlite3 .wrangler/state/v3/d1/.sqlite .tables; .schema users; PRAGMA table_info(users); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/d1/configuration.md ================================================ # D1 Configuration ## wrangler.jsonc Setup ```jsonc { "name": "your-worker-name", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date for new projects "d1_databases": [ { "binding": "DB", // Env variable name "database_name": "your-db-name", // Human-readable name "database_id": "your-database-id", // UUID from dashboard/CLI "migrations_dir": "migrations" // Optional: default is "migrations" }, // Read replica (paid plans only) { "binding": "DB_REPLICA", "database_name": "your-db-name", "database_id": "your-database-id" // Same ID, different binding }, // Multiple databases { "binding": "ANALYTICS_DB", "database_name": "analytics-db", "database_id": "yyy-yyy-yyy" } ] } ``` ## TypeScript Types ```typescript interface Env { DB: D1Database; ANALYTICS_DB?: D1Database; } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const result = await env.DB.prepare('SELECT * FROM users').all(); return Response.json(result.results); } } ``` ## Migrations File structure: `migrations/0001_initial_schema.sql`, `0002_add_posts.sql`, etc. ### Example Migration ```sql -- migrations/0001_initial_schema.sql CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_users_email ON users(email); CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT NOT NULL, content TEXT, published BOOLEAN DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_posts_user_id ON posts(user_id); CREATE INDEX idx_posts_published ON posts(published); ``` ### Running Migrations ```bash # Create new migration file wrangler d1 migrations create add_users_table # Creates: migrations/0001_add_users_table.sql # Apply migrations wrangler d1 migrations apply --local # Apply to local DB wrangler d1 migrations apply --remote # Apply to production DB # List applied migrations wrangler d1 migrations list --remote # Direct SQL execution (bypasses migration tracking) wrangler d1 execute --remote --command="SELECT * FROM users" wrangler d1 execute --local --file=./schema.sql ``` **Migration tracking**: Wrangler creates `d1_migrations` table automatically to track applied migrations ## Indexing Strategy ```sql -- Index frequently queried columns CREATE INDEX idx_users_email ON users(email); -- Composite indexes for multi-column queries CREATE INDEX idx_posts_user_published ON posts(user_id, published); -- Covering indexes (include queried columns) CREATE INDEX idx_users_email_name ON users(email, name); -- Partial indexes for filtered queries CREATE INDEX idx_active_users ON users(email) WHERE active = 1; -- Check if query uses index EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?; ``` ## Drizzle ORM ```typescript // drizzle.config.ts export default { schema: './src/schema.ts', out: './migrations', dialect: 'sqlite', driver: 'd1-http', dbCredentials: { accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, databaseId: process.env.D1_DATABASE_ID!, token: process.env.CLOUDFLARE_API_TOKEN! } } satisfies Config; // schema.ts import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; export const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), email: text('email').notNull().unique(), name: text('name').notNull() }); // worker.ts import { drizzle } from 'drizzle-orm/d1'; import { users } from './schema'; export default { async fetch(request: Request, env: Env) { const db = drizzle(env.DB); return Response.json(await db.select().from(users)); } } ``` ## Import & Export ```bash # Export full database (schema + data) wrangler d1 export --remote --output=./backup.sql # Export data only (no schema) wrangler d1 export --remote --no-schema --output=./data-only.sql # Export with foreign key constraints preserved # (Default: foreign keys are disabled during export for import compatibility) # Import SQL file wrangler d1 execute --remote --file=./backup.sql # Limitations # - BLOB data may not export correctly (use R2 for binary files) # - Very large exports (>1GB) may timeout (split into chunks) # - Import is NOT atomic (use batch() for transactional imports in Workers) ``` ## Plan Tiers | Feature | Free | Paid | |---------|------|------| | Database size | 500 MB | 10 GB | | Batch size | 1,000 statements | 10,000 statements | | Time Travel | 7 days | 30 days | | Read replicas | ❌ | ✅ | | Sessions API | ❌ | ✅ (up to 15 min) | | Pricing | Free | $5/mo + usage | **Usage pricing** (paid plans): $0.001 per 1K reads + $1 per 1M writes + $0.75/GB storage/month ## Local Development ```bash wrangler dev --persist-to=./.wrangler/state # Persist across restarts # Local DB: .wrangler/state/v3/d1/.sqlite sqlite3 .wrangler/state/v3/d1/.sqlite # Inspect # Local dev uses free tier limits by default ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/d1/gotchas.md ================================================ # D1 Gotchas & Troubleshooting ## Common Errors ### "SQL Injection Vulnerability" **Cause:** Using string interpolation instead of prepared statements with bind() **Solution:** ALWAYS use prepared statements: `env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all()` instead of string interpolation which allows attackers to inject malicious SQL ### "no such table" **Cause:** Table doesn't exist because migrations haven't been run, or using wrong database binding **Solution:** Run migrations using `wrangler d1 migrations apply --remote` and verify binding name in wrangler.jsonc matches code ### "UNIQUE constraint failed" **Cause:** Attempting to insert duplicate value in column with UNIQUE constraint **Solution:** Catch error and return 409 Conflict status code ### "Query Timeout (30s exceeded)" **Cause:** Query execution exceeds 30 second timeout limit **Solution:** Break into smaller queries, add indexes to speed up queries, or reduce dataset size ### "N+1 Query Problem" **Cause:** Making multiple individual queries in a loop instead of single optimized query **Solution:** Use JOIN to fetch related data in single query or use `batch()` method for multiple queries ### "Missing Indexes" **Cause:** Queries performing full table scans without indexes **Solution:** Use `EXPLAIN QUERY PLAN` to check if index is used, then create index with `CREATE INDEX idx_users_email ON users(email)` ### "Boolean Type Issues" **Cause:** SQLite uses INTEGER (0/1) not native boolean type **Solution:** Bind 1 or 0 instead of true/false when working with boolean values ### "Date/Time Type Issues" **Cause:** SQLite doesn't have native DATE/TIME types **Solution:** Use TEXT (ISO 8601 format) or INTEGER (unix timestamp) for date/time values ## Plan Tier Limits | Limit | Free Tier | Paid Plans | Notes | |-------|-----------|------------|-------| | Database size | 500 MB | 10 GB | Design for multiple DBs per tenant on paid | | Row size | 1 MB | 1 MB | Store large files in R2, not D1 | | Query timeout | 30s | 30s (900s with sessions) | Use sessions API for migrations | | Batch size | 1,000 statements | 10,000 statements | Split large batches accordingly | | Time Travel | 7 days | 30 days | Point-in-time recovery window | | Read replicas | ❌ Not available | ✅ Available | Paid add-on for lower latency | | Sessions API | ❌ Not available | ✅ Up to 15 min | For migrations and heavy operations | | Concurrent requests | 10,000/min | Higher | Contact support for custom limits | ## Production Gotchas ### "Batch size exceeded" **Cause:** Attempting to send >1,000 statements on free tier or >10,000 on paid **Solution:** Chunk batches: `for (let i = 0; i < stmts.length; i += MAX_BATCH) await env.DB.batch(stmts.slice(i, i + MAX_BATCH))` ### "Session not closed / resource leak" **Cause:** Forgot to call `session.close()` after using sessions API **Solution:** Always use try/finally block: `try { await session.prepare(...) } finally { session.close() }` ### "Replication lag causing stale reads" **Cause:** Reading from replica immediately after write - replication lag can be 100ms-2s **Solution:** Use primary for read-after-write: `await env.DB.prepare(...)` not `env.DB_REPLICA` ### "Migration applied to local but not remote" **Cause:** Forgot `--remote` flag when applying migrations **Solution:** Always run `wrangler d1 migrations apply --remote` for production ### "Foreign key constraint failed" **Cause:** Inserting row with FK to non-existent parent, or deleting parent before children **Solution:** Enable FK enforcement: `PRAGMA foreign_keys = ON;` and use ON DELETE CASCADE in schema ### "BLOB data corrupted on export" **Cause:** D1 export may not handle BLOB correctly **Solution:** Store binary files in R2, only store R2 URLs/keys in D1 ### "Database size approaching limit" **Cause:** Storing too much data in single database **Solution:** Horizontal scale-out: create per-tenant/per-user databases, archive old data, or upgrade to paid plan ### "Local dev vs production behavior differs" **Cause:** Local uses SQLite file, production uses distributed D1 - different performance/limits **Solution:** Always test migrations on remote with `--remote` flag before production rollout ================================================ FILE: skills/.curated/cloudflare-deploy/references/d1/patterns.md ================================================ # D1 Patterns & Best Practices ## Pagination ```typescript async function getUsers({ page, pageSize }: { page: number; pageSize: number }, env: Env) { const offset = (page - 1) * pageSize; const [countResult, dataResult] = await env.DB.batch([ env.DB.prepare('SELECT COUNT(*) as total FROM users'), env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?').bind(pageSize, offset) ]); return { data: dataResult.results, total: countResult.results[0].total, page, pageSize, totalPages: Math.ceil(countResult.results[0].total / pageSize) }; } ``` ## Conditional Queries ```typescript async function searchUsers(filters: { name?: string; email?: string; active?: boolean }, env: Env) { const conditions: string[] = [], params: (string | number | boolean | null)[] = []; if (filters.name) { conditions.push('name LIKE ?'); params.push(`%${filters.name}%`); } if (filters.email) { conditions.push('email = ?'); params.push(filters.email); } if (filters.active !== undefined) { conditions.push('active = ?'); params.push(filters.active ? 1 : 0); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; return await env.DB.prepare(`SELECT * FROM users ${whereClause}`).bind(...params).all(); } ``` ## Bulk Insert ```typescript async function bulkInsertUsers(users: Array<{ name: string; email: string }>, env: Env) { const stmt = env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); const batch = users.map(user => stmt.bind(user.name, user.email)); return await env.DB.batch(batch); } ``` ## Caching with KV ```typescript async function getCachedUser(userId: number, env: { DB: D1Database; CACHE: KVNamespace }) { const cacheKey = `user:${userId}`; const cached = await env.CACHE?.get(cacheKey, 'json'); if (cached) return cached; const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); if (user) await env.CACHE?.put(cacheKey, JSON.stringify(user), { expirationTtl: 300 }); return user; } ``` ## Query Optimization ```typescript // ✅ Use indexes in WHERE clauses const users = await env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email).all(); // ✅ Limit result sets const recentPosts = await env.DB.prepare('SELECT * FROM posts ORDER BY created_at DESC LIMIT 100').all(); // ✅ Use batch() for multiple independent queries const [user, posts, comments] = await env.DB.batch([ env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId), env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(userId), env.DB.prepare('SELECT * FROM comments WHERE user_id = ?').bind(userId) ]); // ❌ Avoid N+1 queries for (const post of posts) { const author = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(post.user_id).first(); // Bad: multiple round trips } // ✅ Use JOINs instead const postsWithAuthors = await env.DB.prepare(` SELECT posts.*, users.name as author_name FROM posts JOIN users ON posts.user_id = users.id `).all(); ``` ## Multi-Tenant SaaS ```typescript // Each tenant gets own database export default { async fetch(request: Request, env: { [key: `TENANT_${string}`]: D1Database }) { const tenantId = request.headers.get('X-Tenant-ID'); const data = await env[`TENANT_${tenantId}`].prepare('SELECT * FROM records').all(); return Response.json(data.results); } } ``` ## Session Storage ```typescript async function createSession(userId: number, token: string, env: Env) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); return await env.DB.prepare('INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, ?)').bind(userId, token, expiresAt).run(); } async function validateSession(token: string, env: Env) { return await env.DB.prepare('SELECT s.*, u.email FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP').bind(token).first(); } ``` ## Analytics/Events ```typescript async function logEvent(event: { type: string; userId?: number; metadata: object }, env: Env) { return await env.DB.prepare('INSERT INTO events (type, user_id, metadata) VALUES (?, ?, ?)').bind(event.type, event.userId || null, JSON.stringify(event.metadata)).run(); } async function getEventStats(startDate: string, endDate: string, env: Env) { return await env.DB.prepare('SELECT type, COUNT(*) as count FROM events WHERE timestamp BETWEEN ? AND ? GROUP BY type ORDER BY count DESC').bind(startDate, endDate).all(); } ``` ## Read Replication Pattern (Paid Plans) ```typescript interface Env { DB: D1Database; DB_REPLICA: D1Database; } export default { async fetch(request: Request, env: Env) { if (request.method === 'GET') { // Reads: use replica for lower latency const users = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE active = 1').all(); return Response.json(users.results); } if (request.method === 'POST') { const { name, email } = await request.json(); const result = await env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email).run(); // Read-after-write: use primary for consistency (replication lag <100ms-2s) const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(result.meta.last_row_id).first(); return Response.json(user, { status: 201 }); } } } ``` **Use replicas for**: Analytics dashboards, search results, public queries (eventual consistency OK) **Use primary for**: Read-after-write, financial transactions, authentication (consistency required) ## Sessions API Pattern (Paid Plans) ```typescript // Migration with long-running session (up to 15 min) async function runMigration(env: Env) { const session = env.DB.withSession({ timeout: 600 }); // 10 min try { await session.prepare('CREATE INDEX idx_users_email ON users(email)').run(); await session.prepare('CREATE INDEX idx_posts_user ON posts(user_id)').run(); await session.prepare('ANALYZE').run(); } finally { session.close(); // Always close to prevent leaks } } // Bulk transformation with batching async function transformLargeDataset(env: Env) { const session = env.DB.withSession({ timeout: 900 }); // 15 min max try { const BATCH_SIZE = 1000; let offset = 0; while (true) { const rows = await session.prepare('SELECT id, data FROM legacy LIMIT ? OFFSET ?').bind(BATCH_SIZE, offset).all(); if (rows.results.length === 0) break; const updates = rows.results.map(row => session.prepare('UPDATE legacy SET new_data = ? WHERE id = ?').bind(transform(row.data), row.id) ); await session.batch(updates); offset += BATCH_SIZE; } } finally { session.close(); } } ``` ## Time Travel & Backups ```bash wrangler d1 time-travel restore --timestamp="2024-01-15T14:30:00Z" # Point-in-time wrangler d1 time-travel info # List restore points (7 days free, 30 days paid) wrangler d1 export --remote --output=./backup.sql # Full export wrangler d1 export --remote --no-schema --output=./data.sql # Data only wrangler d1 execute --remote --file=./backup.sql # Import ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/ddos/README.md ================================================ # Cloudflare DDoS Protection Autonomous, always-on protection against DDoS attacks across L3/4 and L7. ## Protection Types - **HTTP DDoS (L7)**: Protects HTTP/HTTPS traffic, phase `ddos_l7`, zone/account level - **Network DDoS (L3/4)**: UDP/SYN/DNS floods, phase `ddos_l4`, account level only - **Adaptive DDoS**: Learns 7-day baseline, detects deviations, 4 profile types (Origins, User-Agents, Locations, Protocols) ## Plan Availability | Feature | Free | Pro | Business | Enterprise | Enterprise Advanced | |---------|------|-----|----------|------------|---------------------| | HTTP DDoS (L7) | ✓ | ✓ | ✓ | ✓ | ✓ | | Network DDoS (L3/4) | ✓ | ✓ | ✓ | ✓ | ✓ | | Override rules | 1 | 1 | 1 | 1 | 10 | | Custom expressions | ✗ | ✗ | ✗ | ✗ | ✓ | | Log action | ✗ | ✗ | ✗ | ✗ | ✓ | | Adaptive DDoS | ✗ | ✗ | ✗ | ✓ | ✓ | | Alert filters | Basic | Basic | Basic | Advanced | Advanced | ## Actions & Sensitivity - **Actions**: `block`, `managed_challenge`, `challenge`, `log` (Enterprise Advanced only) - **Sensitivity**: `default` (high), `medium`, `low`, `eoff` (essentially off) - **Override**: By category/tag or individual rule ID - **Scope**: Zone-level overrides take precedence over account-level ## Reading Order | File | Purpose | Start Here If... | |------|---------|------------------| | [configuration.md](./configuration.md) | Dashboard setup, rule structure, adaptive profiles | You're setting up DDoS protection for the first time | | [api.md](./api.md) | API endpoints, SDK usage, ruleset ID discovery | You're automating configuration or need programmatic access | | [patterns.md](./patterns.md) | Protection strategies, defense-in-depth, dynamic response | You need implementation patterns or layered security | | [gotchas.md](./gotchas.md) | False positives, tuning, error handling | You're troubleshooting or optimizing existing protection | ## See Also - [waf](../waf/) - Application-layer security rules - [bot-management](../bot-management/) - Bot detection and mitigation ================================================ FILE: skills/.curated/cloudflare-deploy/references/ddos/api.md ================================================ # DDoS API ## Endpoints ### HTTP DDoS (L7) ```typescript // Zone-level PUT /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint GET /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint // Account-level (Enterprise Advanced) PUT /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint GET /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint ``` ### Network DDoS (L3/4) ```typescript // Account-level only PUT /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint GET /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint ``` ## TypeScript SDK **SDK Version**: Requires `cloudflare` >= 3.0.0 for ruleset phase methods. ```typescript import Cloudflare from "cloudflare"; const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN }); // STEP 1: Discover managed ruleset ID (required for overrides) const allRulesets = await client.rulesets.list({ zone_id: zoneId }); const ddosRuleset = allRulesets.result.find( (r) => r.kind === "managed" && r.phase === "ddos_l7" ); if (!ddosRuleset) throw new Error("DDoS managed ruleset not found"); const managedRulesetId = ddosRuleset.id; // STEP 2: Get current HTTP DDoS configuration const entrypointRuleset = await client.zones.rulesets.phases.entrypoint.get("ddos_l7", { zone_id: zoneId, }); // STEP 3: Update HTTP DDoS ruleset with overrides await client.zones.rulesets.phases.entrypoint.update("ddos_l7", { zone_id: zoneId, rules: [ { action: "execute", expression: "true", action_parameters: { id: managedRulesetId, // From discovery step overrides: { sensitivity_level: "medium", action: "managed_challenge", }, }, }, ], }); // Network DDoS (account level, L3/4) const l4Rulesets = await client.rulesets.list({ account_id: accountId }); const l4DdosRuleset = l4Rulesets.result.find( (r) => r.kind === "managed" && r.phase === "ddos_l4" ); const l4Ruleset = await client.accounts.rulesets.phases.entrypoint.get("ddos_l4", { account_id: accountId, }); ``` ## Alert Configuration ```typescript interface DDoSAlertConfig { name: string; enabled: boolean; alert_type: "http_ddos_attack_alert" | "layer_3_4_ddos_attack_alert" | "advanced_http_ddos_attack_alert" | "advanced_layer_3_4_ddos_attack_alert"; filters?: { zones?: string[]; hostnames?: string[]; requests_per_second?: number; packets_per_second?: number; megabits_per_second?: number; ip_prefixes?: string[]; // CIDR ip_addresses?: string[]; protocols?: string[]; }; mechanisms: { email?: Array<{ id: string }>; webhooks?: Array<{ id: string }>; pagerduty?: Array<{ id: string }>; }; } // Create alert await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/alerting/v3/policies`, { method: "POST", headers: { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json", }, body: JSON.stringify(alertConfig), } ); ``` ## Typed Override Examples ```typescript // Override by category interface CategoryOverride { action: "execute"; expression: string; action_parameters: { id: string; overrides: { categories?: Array<{ category: "http-flood" | "http-anomaly" | "udp-flood" | "syn-flood"; sensitivity_level?: "default" | "medium" | "low" | "eoff"; action?: "block" | "managed_challenge" | "challenge" | "log"; }>; }; }; } // Override by rule ID interface RuleOverride { action: "execute"; expression: string; action_parameters: { id: string; overrides: { rules?: Array<{ id: string; action?: "block" | "managed_challenge" | "challenge" | "log"; sensitivity_level?: "default" | "medium" | "low" | "eoff"; }>; }; }; } // Example: Override specific adaptive rule const adaptiveOverride: RuleOverride = { action: "execute", expression: "true", action_parameters: { id: managedRulesetId, overrides: { rules: [ { id: "...adaptive-origins-rule-id...", sensitivity_level: "low" }, ], }, }, }; ``` See [patterns.md](./patterns.md) for complete implementation patterns. ================================================ FILE: skills/.curated/cloudflare-deploy/references/ddos/configuration.md ================================================ # DDoS Configuration ## Dashboard Setup 1. Navigate to Security > DDoS 2. Select HTTP DDoS or Network-layer DDoS 3. Configure sensitivity & action per ruleset/category/rule 4. Apply overrides with optional expressions (Enterprise Advanced) 5. Enable Adaptive DDoS toggle (Enterprise/Enterprise Advanced, requires 7 days traffic history) ## Rule Structure ```typescript interface DDoSOverride { description: string; rules: Array<{ action: "execute"; expression: string; // Custom expression (Enterprise Advanced) or "true" for all action_parameters: { id: string; // Managed ruleset ID (discover via api.md) overrides: { sensitivity_level?: "default" | "medium" | "low" | "eoff"; action?: "block" | "managed_challenge" | "challenge" | "log"; // log = Enterprise Advanced only categories?: Array<{ category: string; // e.g., "http-flood", "udp-flood" sensitivity_level?: string; }>; rules?: Array<{ id: string; action?: string; sensitivity_level?: string; }>; }; }; }>; } ``` ## Expression Availability | Plan | Custom Expressions | Example | |------|-------------------|---------| | Free/Pro/Business | ✗ | Use `"true"` only | | Enterprise | ✗ | Use `"true"` only | | Enterprise Advanced | ✓ | `ip.src in {...}`, `http.request.uri.path matches "..."` | ## Sensitivity Mapping | UI | API | Threshold | |----|-----|-----------| | High | `default` | Most aggressive | | Medium | `medium` | Balanced | | Low | `low` | Less aggressive | | Essentially Off | `eoff` | Minimal mitigation | ## Common Categories - `http-flood`, `http-anomaly` (L7) - `udp-flood`, `syn-flood`, `dns-flood` (L3/4) ## Override Precedence Multiple override layers apply in this order (higher precedence wins): ``` Zone-level > Account-level Individual Rule > Category > Global sensitivity/action ``` **Example**: Zone rule for `/api/*` overrides account-level global settings. ## Adaptive DDoS Profiles **Availability**: Enterprise, Enterprise Advanced **Learning period**: 7 days of traffic history required | Profile Type | Description | Detects | |--------------|-------------|---------| | **Origins** | Traffic patterns per origin server | Anomalous requests to specific origins | | **User-Agents** | Traffic patterns per User-Agent | Malicious/anomalous user agent strings | | **Locations** | Traffic patterns per geo-location | Attacks from specific countries/regions | | **Protocols** | Traffic patterns per protocol (L3/4) | Protocol-specific flood attacks | Configure by targeting specific adaptive rule IDs via API (see api.md#typed-override-examples). ## Alerting Configure via Notifications: - Alert types: `http_ddos_attack_alert`, `layer_3_4_ddos_attack_alert`, `advanced_*` variants - Filters: zones, hostnames, RPS/PPS/Mbps thresholds, IPs, protocols - Mechanisms: email, webhooks, PagerDuty See [api.md](./api.md#alert-configuration) for API examples. ================================================ FILE: skills/.curated/cloudflare-deploy/references/ddos/gotchas.md ================================================ # DDoS Gotchas ## Common Errors ### "False positives blocking legitimate traffic" **Cause**: Sensitivity too high, wrong action, or missing exceptions **Solution**: 1. Lower sensitivity for specific rule/category 2. Use `log` action first to validate (Enterprise Advanced) 3. Add exception with custom expression (e.g., allowlist IPs) 4. Query flagged requests via GraphQL Analytics API to identify patterns ### "Attacks getting through" **Cause**: Sensitivity too low or wrong action **Solution**: Increase to `default` sensitivity and use `block` action: ```typescript const config = { rules: [{ expression: "true", action: "execute", action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "default", action: "block" } }, }], }; ``` ### "Adaptive rules not working" **Cause**: Insufficient traffic history (needs 7 days) **Solution**: Wait for baseline to establish, check dashboard for adaptive rule status ### "Zone override ignored" **Cause**: Account overrides conflict with zone overrides **Solution**: Configure at zone level OR remove zone overrides to use account-level ### "Log action not available" **Cause**: Not on Enterprise Advanced DDoS plan **Solution**: Use `managed_challenge` with low sensitivity for testing ### "Rule limit exceeded" **Cause**: Too many override rules (Free/Pro/Business: 1, Enterprise Advanced: 10) **Solution**: Combine conditions in single expression using `and`/`or` ### "Cannot override rule" **Cause**: Rule is read-only **Solution**: Check API response for read-only indicator, use different rule ### "Cannot disable DDoS protection" **Cause**: DDoS managed rulesets cannot be fully disabled (always-on protection) **Solution**: Set `sensitivity_level: "eoff"` for minimal mitigation ### "Expression not allowed" **Cause**: Custom expressions require Enterprise Advanced plan **Solution**: Use `expression: "true"` for all traffic, or upgrade plan ### "Managed ruleset not found" **Cause**: Zone/account doesn't have DDoS managed ruleset, or incorrect phase **Solution**: Verify ruleset exists via `client.rulesets.list()`, check phase name (`ddos_l7` or `ddos_l4`) ## API Error Codes | Error Code | Message | Cause | Solution | |------------|---------|-------|----------| | 10000 | Authentication error | Invalid/missing API token | Check token has DDoS permissions | | 81000 | Ruleset validation failed | Invalid rule structure | Verify `action_parameters.id` is managed ruleset ID | | 81020 | Expression not allowed | Custom expressions on wrong plan | Use `"true"` or upgrade to Enterprise Advanced | | 81021 | Rule limit exceeded | Too many override rules | Reduce rules or upgrade (Enterprise Advanced: 10) | | 81022 | Invalid sensitivity level | Wrong sensitivity value | Use: `default`, `medium`, `low`, `eoff` | | 81023 | Invalid action | Wrong action for plan | Enterprise Advanced only: `log` action | ## Limits | Resource/Limit | Free/Pro/Business | Enterprise | Enterprise Advanced | |----------------|-------------------|------------|---------------------| | Override rules per zone | 1 | 1 | 10 | | Custom expressions | ✗ | ✗ | ✓ | | Log action | ✗ | ✗ | ✓ | | Adaptive DDoS | ✗ | ✓ | ✓ | | Traffic history required | - | 7 days | 7 days | ## Tuning Strategy 1. Start with `log` action + `medium` sensitivity 2. Monitor for 24-48 hours 3. Identify false positives, add exceptions 4. Gradually increase to `default` sensitivity 5. Change action from `log` → `managed_challenge` → `block` 6. Document all adjustments ## Best Practices - Test during low-traffic periods - Use zone-level for per-site tuning - Reference IP lists for easier management - Set appropriate alert thresholds (avoid noise) - Combine with WAF for layered defense - Avoid over-tuning (keep config simple) See [patterns.md](./patterns.md) for progressive rollout examples. ================================================ FILE: skills/.curated/cloudflare-deploy/references/ddos/patterns.md ================================================ # DDoS Protection Patterns ## Allowlist Trusted IPs ```typescript const config = { description: "Allowlist trusted IPs", rules: [{ expression: "ip.src in { 203.0.113.0/24 192.0.2.1 }", action: "execute", action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "eoff" }, }, }], }; await client.accounts.rulesets.phases.entrypoint.update("ddos_l7", { account_id: accountId, ...config, }); ``` ## Route-specific Sensitivity ```typescript const config = { description: "Route-specific protection", rules: [ { expression: "not http.request.uri.path matches \"^/api/\"", action: "execute", action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "default", action: "block" }, }, }, { expression: "http.request.uri.path matches \"^/api/\"", action: "execute", action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "low", action: "managed_challenge" }, }, }, ], }; ``` ## Progressive Enhancement ```typescript enum ProtectionLevel { MONITORING = "monitoring", LOW = "low", MEDIUM = "medium", HIGH = "high" } const levelConfig = { [ProtectionLevel.MONITORING]: { action: "log", sensitivity: "eoff" }, [ProtectionLevel.LOW]: { action: "managed_challenge", sensitivity: "low" }, [ProtectionLevel.MEDIUM]: { action: "managed_challenge", sensitivity: "medium" }, [ProtectionLevel.HIGH]: { action: "block", sensitivity: "default" }, } as const; async function setProtectionLevel(zoneId: string, level: ProtectionLevel, rulesetId: string, client: Cloudflare) { const settings = levelConfig[level]; return client.zones.rulesets.phases.entrypoint.update("ddos_l7", { zone_id: zoneId, rules: [{ expression: "true", action: "execute", action_parameters: { id: rulesetId, overrides: { action: settings.action, sensitivity_level: settings.sensitivity } }, }], }); } ``` ## Dynamic Response to Attacks ```typescript interface Env { CLOUDFLARE_API_TOKEN: string; ZONE_ID: string; KV: KVNamespace; } export default { async fetch(request: Request, env: Env): Promise { if (request.url.includes("/attack-detected")) { const attackData = await request.json(); await env.KV.put(`attack:${Date.now()}`, JSON.stringify(attackData), { expirationTtl: 86400 }); const recentAttacks = await getRecentAttacks(env.KV); if (recentAttacks.length > 5) { await setProtectionLevel(env.ZONE_ID, ProtectionLevel.HIGH, managedRulesetId, client); return new Response("Protection increased"); } } return new Response("OK"); }, async scheduled(event: ScheduledEvent, env: Env): Promise { const recentAttacks = await getRecentAttacks(env.KV); if (recentAttacks.length === 0) await setProtectionLevel(env.ZONE_ID, ProtectionLevel.MEDIUM, managedRulesetId, client); }, }; ``` ## Multi-rule Tiered Protection (Enterprise Advanced) ```typescript const config = { description: "Multi-tier DDoS protection", rules: [ { expression: "not ip.src in $known_ips and not cf.bot_management.score gt 30", action: "execute", action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "default", action: "block" } }, }, { expression: "cf.bot_management.verified_bot", action: "execute", action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "medium", action: "managed_challenge" } }, }, { expression: "ip.src in $trusted_ips", action: "execute", action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "low" } }, }, ], }; ``` ## Defense in Depth Layered security stack: DDoS + WAF + Rate Limiting + Bot Management. ```typescript // Layer 1: DDoS (volumetric attacks) await client.zones.rulesets.phases.entrypoint.update("ddos_l7", { zone_id: zoneId, rules: [{ expression: "true", action: "execute", action_parameters: { id: ddosRulesetId, overrides: { sensitivity_level: "medium" } } }], }); // Layer 2: WAF (exploit protection) await client.zones.rulesets.phases.entrypoint.update("http_request_firewall_managed", { zone_id: zoneId, rules: [{ expression: "true", action: "execute", action_parameters: { id: wafRulesetId } }], }); // Layer 3: Rate Limiting (abuse prevention) await client.zones.rulesets.phases.entrypoint.update("http_ratelimit", { zone_id: zoneId, rules: [{ expression: "http.request.uri.path eq \"/api/login\"", action: "block", ratelimit: { characteristics: ["ip.src"], period: 60, requests_per_period: 5 } }], }); // Layer 4: Bot Management (automation detection) await client.zones.rulesets.phases.entrypoint.update("http_request_sbfm", { zone_id: zoneId, rules: [{ expression: "cf.bot_management.score lt 30", action: "managed_challenge" }], }); ``` ## Cache Strategy for DDoS Mitigation Exclude query strings from cache key to counter randomized query parameter attacks. ```typescript const cacheRule = { expression: "http.request.uri.path matches \"^/api/\"", action: "set_cache_settings", action_parameters: { cache: true, cache_key: { ignore_query_strings_order: true, custom_key: { query_string: { exclude: { all: true } } } }, }, }; await client.zones.rulesets.phases.entrypoint.update("http_request_cache_settings", { zone_id: zoneId, rules: [cacheRule] }); ``` **Rationale**: Attackers randomize query strings (`?random=123456`) to bypass cache. Excluding query params ensures cache hits absorb attack traffic. See [configuration.md](./configuration.md) for rule structure details. ================================================ FILE: skills/.curated/cloudflare-deploy/references/do-storage/README.md ================================================ # Cloudflare Durable Objects Storage Persistent storage API for Durable Objects with SQLite and KV backends, PITR, and automatic concurrency control. ## Overview DO Storage provides: - SQLite-backed (recommended) or KV-backed - SQL API + synchronous/async KV APIs - Automatic input/output gates (race-free) - 30-day point-in-time recovery (PITR) - Transactions and alarms **Use cases:** Stateful coordination, real-time collaboration, counters, sessions, rate limiters **Billing:** Charged by request, GB-month storage, and rowsRead/rowsWritten for SQL operations ## Quick Start ```typescript export class Counter extends DurableObject { sql: SqlStorage; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.sql = ctx.storage.sql; this.sql.exec('CREATE TABLE IF NOT EXISTS data(key TEXT PRIMARY KEY, value INTEGER)'); } async increment(): Promise { const result = this.sql.exec( 'INSERT INTO data VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = value + 1 RETURNING value', 'counter', 1 ).one(); return result?.value || 1; } } ``` ## Storage Backends | Backend | Create Method | APIs | PITR | |---------|---------------|------|------| | SQLite (recommended) | `new_sqlite_classes` | SQL + sync KV + async KV | ✅ | | KV (legacy) | `new_classes` | async KV only | ❌ | ## Core APIs - **SQL API** (`ctx.storage.sql`): Full SQLite with extensions (FTS5, JSON, math) - **Sync KV** (`ctx.storage.kv`): Synchronous key-value (SQLite only) - **Async KV** (`ctx.storage`): Asynchronous key-value (both backends) - **Transactions** (`transactionSync()`, `transaction()`) - **PITR** (`getBookmarkForTime()`, `onNextSessionRestoreBookmark()`) - **Alarms** (`setAlarm()`, `alarm()` handler) ## Reading Order **New to DO storage:** configuration.md → api.md → patterns.md → gotchas.md **Building features:** patterns.md → api.md → gotchas.md **Debugging issues:** gotchas.md → api.md **Writing tests:** testing.md ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc migrations, SQLite vs KV setup, RPC binding - [api.md](./api.md) - SQL exec/cursors, KV methods, storage options, transactions, alarms, PITR - [patterns.md](./patterns.md) - Schema migrations, caching, rate limiting, batch processing, parent-child coordination - [gotchas.md](./gotchas.md) - Concurrency gates, INTEGER precision, transaction rules, SQL limits - [testing.md](./testing.md) - vitest-pool-workers setup, testing DOs with SQL/alarms/PITR ## See Also - [durable-objects](../durable-objects/) - DO fundamentals and coordination patterns - [workers](../workers/) - Worker runtime for DO stubs - [d1](../d1/) - Shared database alternative to per-DO storage ================================================ FILE: skills/.curated/cloudflare-deploy/references/do-storage/api.md ================================================ # DO Storage API Reference ## SQL API ```typescript const cursor = this.sql.exec('SELECT * FROM users WHERE email = ?', email); for (let row of cursor) {} // Objects: { id, name, email } cursor.toArray(); cursor.one(); // Single row (throws if != 1) for (let row of cursor.raw()) {} // Arrays: [1, "Alice", "..."] // Manual iteration const iter = cursor[Symbol.iterator](); const first = iter.next(); // { value: {...}, done: false } cursor.columnNames; // ["id", "name", "email"] cursor.rowsRead; cursor.rowsWritten; // Billing type User = { id: number; name: string; email: string }; const user = this.sql.exec('...', userId).one(); ``` ## Sync KV API (SQLite only) ```typescript this.ctx.storage.kv.get("counter"); // undefined if missing this.ctx.storage.kv.put("counter", 42); this.ctx.storage.kv.put("user", { name: "Alice", age: 30 }); this.ctx.storage.kv.delete("counter"); // true if existed for (let [key, value] of this.ctx.storage.kv.list()) {} // List options: start, prefix, reverse, limit this.ctx.storage.kv.list({ start: "user:", prefix: "user:", reverse: true, limit: 100 }); ``` ## Async KV API (Both backends) ```typescript await this.ctx.storage.get("key"); // Single await this.ctx.storage.get(["key1", "key2"]); // Multiple (max 128) await this.ctx.storage.put("key", value); // Single await this.ctx.storage.put({ "key1": "v1", "key2": { nested: true } }); // Multiple (max 128) await this.ctx.storage.delete("key"); await this.ctx.storage.delete(["key1", "key2"]); await this.ctx.storage.list({ prefix: "user:", limit: 100 }); // Options: allowConcurrency, noCache, allowUnconfirmed await this.ctx.storage.get("key", { allowConcurrency: true, noCache: true }); await this.ctx.storage.put("key", value, { allowUnconfirmed: true, noCache: true }); ``` ### Storage Options | Option | Methods | Effect | Use Case | |--------|---------|--------|----------| | `allowConcurrency` | get, list | Skip input gate; allow concurrent requests during read | Read-heavy metrics that don't need strict consistency | | `noCache` | get, put, list | Skip in-memory cache; always read from disk | Rarely-accessed data or testing storage directly | | `allowUnconfirmed` | put, delete | Return before write confirms (still protected by output gate) | Non-critical writes where latency matters more than confirmation | ## Transactions ```typescript // Sync (SQL/sync KV only) this.ctx.storage.transactionSync(() => { this.sql.exec('UPDATE accounts SET balance = balance - ? WHERE id = ?', 100, 1); this.sql.exec('UPDATE accounts SET balance = balance + ? WHERE id = ?', 100, 2); return "result"; }); // Async await this.ctx.storage.transaction(async () => { const value = await this.ctx.storage.get("counter"); await this.ctx.storage.put("counter", value + 1); if (value > 100) this.ctx.storage.rollback(); // Explicit rollback }); ``` ## Point-in-Time Recovery ```typescript await this.ctx.storage.getCurrentBookmark(); await this.ctx.storage.getBookmarkForTime(Date.now() - 2 * 24 * 60 * 60 * 1000); await this.ctx.storage.onNextSessionRestoreBookmark(bookmark); this.ctx.abort(); // Restart to apply; bookmarks lexically comparable (earlier < later) ``` ## Alarms ```typescript await this.ctx.storage.setAlarm(Date.now() + 60000); // Timestamp or Date await this.ctx.storage.getAlarm(); await this.ctx.storage.deleteAlarm(); async alarm() { await this.doScheduledWork(); } ``` ## Misc ```typescript await this.ctx.storage.deleteAll(); // Atomic for SQLite; alarm NOT included this.ctx.storage.sql.databaseSize; // Bytes ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/do-storage/configuration.md ================================================ # DO Storage Configuration ## SQLite-backed (Recommended) **wrangler.jsonc:** ```jsonc { "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter", "Session", "RateLimiter"] } ] } ``` **Migration lifecycle:** Migrations run once per deployment. Existing DO instances get new storage backend on next invocation. Renaming/removing classes requires `renamed_classes` or `deleted_classes` entries. ## KV-backed (Legacy) **wrangler.jsonc:** ```jsonc { "migrations": [ { "tag": "v1", "new_classes": ["OldCounter"] } ] } ``` ## TypeScript Setup ```typescript export class MyDurableObject extends DurableObject { sql: SqlStorage; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.sql = ctx.storage.sql; // Initialize schema this.sql.exec(` CREATE TABLE IF NOT EXISTS users( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE ); `); } } // Binding interface Env { MY_DO: DurableObjectNamespace; } export default { async fetch(request: Request, env: Env): Promise { const id = env.MY_DO.idFromName('singleton'); const stub = env.MY_DO.get(id); // Modern RPC: call methods directly (recommended) const result = await stub.someMethod(); return Response.json(result); // Legacy: forward request (still works) // return stub.fetch(request); } } ``` ## CPU Limits ```jsonc { "limits": { "cpu_ms": 300000 // 5 minutes (default 30s) } } ``` ## Location Control ```typescript // Jurisdiction (GDPR/FedRAMP) const euNamespace = env.MY_DO.jurisdiction("eu"); const id = euNamespace.newUniqueId(); const stub = euNamespace.get(id); // Location hint (best effort) const stub = env.MY_DO.get(id, { locationHint: "enam" }); // Hints: wnam, enam, sam, weur, eeur, apac, oc, afr, me ``` ## Initialization ```typescript export class Counter extends DurableObject { value: number; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); // Block concurrent requests during init ctx.blockConcurrencyWhile(async () => { this.value = (await ctx.storage.get("value")) || 0; }); } } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/do-storage/gotchas.md ================================================ # DO Storage Gotchas & Troubleshooting ## Concurrency Model (CRITICAL) Durable Objects use **input/output gates** to prevent race conditions: ### Input Gates Block new requests during storage reads from CURRENT request: ```typescript // SAFE: Input gate active during await async increment() { const val = await this.ctx.storage.get("counter"); // Input gate blocks other requests await this.ctx.storage.put("counter", val + 1); return val; } ``` ### Output Gates Hold response until ALL writes from current request confirm: ```typescript // SAFE: Output gate waits for put() to confirm before returning response async increment() { const val = await this.ctx.storage.get("counter"); this.ctx.storage.put("counter", val + 1); // No await return new Response(String(val)); // Response delayed until write confirms } ``` ### Write Coalescing Multiple writes to same key = atomic (last write wins): ```typescript // SAFE: All three writes coalesce atomically this.ctx.storage.put("key", 1); this.ctx.storage.put("key", 2); this.ctx.storage.put("key", 3); // Final value: 3 ``` ### Breaking Gates (DANGER) **fetch() breaks input/output gates** → allows request interleaving: ```typescript // UNSAFE: fetch() allows another request to interleave async unsafe() { const val = await this.ctx.storage.get("counter"); await fetch("https://api.example.com"); // Gate broken! await this.ctx.storage.put("counter", val + 1); // Race condition possible } ``` **Solution:** Use `blockConcurrencyWhile()` or `transaction()`: ```typescript // SAFE: Block concurrent requests explicitly async safe() { return await this.ctx.blockConcurrencyWhile(async () => { const val = await this.ctx.storage.get("counter"); await fetch("https://api.example.com"); await this.ctx.storage.put("counter", val + 1); return val; }); } ``` ### allowConcurrency Option Opt out of input gate for reads that don't need protection: ```typescript // Allow concurrent reads (no consistency guarantee) const val = await this.ctx.storage.get("metrics", { allowConcurrency: true }); ``` ## Common Errors ### "Race Condition in Concurrent Calls" **Cause:** Multiple concurrent storage operations initiated from same event (e.g., `Promise.all()`) are not protected by input gate **Solution:** Avoid concurrent storage operations within single event; input gate only serializes requests from different events, not operations within same event ### "Direct SQL Transaction Statements" **Cause:** Using `BEGIN TRANSACTION` directly instead of transaction methods **Solution:** Use `this.ctx.storage.transactionSync()` for sync operations or `this.ctx.storage.transaction()` for async operations ### "Async in transactionSync" **Cause:** Using async operations inside `transactionSync()` callback **Solution:** Use async `transaction()` method instead of `transactionSync()` when async operations needed ### "TypeScript Type Mismatch at Runtime" **Cause:** Query doesn't return all fields specified in TypeScript type **Solution:** Ensure SQL query selects all columns that match the TypeScript type definition ### "Silent Data Corruption with Large IDs" **Cause:** JavaScript numbers have 53-bit precision; SQLite INTEGER is 64-bit **Symptom:** IDs > 9007199254740991 (Number.MAX_SAFE_INTEGER) silently truncate/corrupt **Solution:** Store large IDs as TEXT: ```typescript // BAD: Snowflake/Twitter IDs will corrupt this.sql.exec("CREATE TABLE events(id INTEGER PRIMARY KEY)"); this.sql.exec("INSERT INTO events VALUES (?)", 1234567890123456789n); // Corrupts! // GOOD: Store as TEXT this.sql.exec("CREATE TABLE events(id TEXT PRIMARY KEY)"); this.sql.exec("INSERT INTO events VALUES (?)", "1234567890123456789"); ``` ### "Alarm Not Deleted with deleteAll()" **Cause:** `deleteAll()` doesn't delete alarms automatically **Solution:** Call `deleteAlarm()` explicitly before `deleteAll()` to remove alarm ### "Slow Performance" **Cause:** Using async KV API instead of sync API **Solution:** Use sync KV API (`ctx.storage.kv`) for better performance with simple key-value operations ### "High Billing from Storage Operations" **Cause:** Excessive `rowsRead`/`rowsWritten` or unused objects not cleaned up **Solution:** Monitor `rowsRead`/`rowsWritten` metrics and ensure unused objects call `deleteAll()` ### "Durable Object Overloaded" **Cause:** Single DO exceeding ~1K req/sec soft limit **Solution:** Shard across multiple DOs with random IDs or other distribution strategy ## Limits | Limit | Value | Notes | |-------|-------|-------| | Max columns per table | 100 | SQL limitation | | Max string/BLOB per row | 2 MB | SQL limitation | | Max row size | 2 MB | SQL limitation | | Max SQL statement size | 100 KB | SQL limitation | | Max SQL parameters | 100 | SQL limitation | | Max LIKE/GLOB pattern | 50 B | SQL limitation | | SQLite storage per object | 10 GB | SQLite-backed storage | | SQLite key+value size | 2 MB | SQLite-backed storage | | KV storage per object | Unlimited | KV-style storage | | KV key size | 2 KiB | KV-style storage | | KV value size | 128 KiB | KV-style storage | | Request throughput | ~1K req/sec | Soft limit per DO | ================================================ FILE: skills/.curated/cloudflare-deploy/references/do-storage/patterns.md ================================================ # DO Storage Patterns & Best Practices ## Schema Migration ```typescript export class MyDurableObject extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.sql = ctx.storage.sql; // Use SQLite's built-in user_version pragma const ver = this.sql.exec("PRAGMA user_version").one()?.user_version || 0; if (ver === 0) { this.sql.exec(`CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT)`); this.sql.exec("PRAGMA user_version = 1"); } if (ver === 1) { this.sql.exec(`ALTER TABLE users ADD COLUMN email TEXT`); this.sql.exec("PRAGMA user_version = 2"); } } } ``` ## In-Memory Caching ```typescript export class UserCache extends DurableObject { cache = new Map(); async getUser(id: string): Promise { if (this.cache.has(id)) { const cached = this.cache.get(id); if (cached) return cached; } const user = await this.ctx.storage.get(`user:${id}`); if (user) this.cache.set(id, user); return user; } async updateUser(id: string, data: Partial) { const updated = { ...await this.getUser(id), ...data }; this.cache.set(id, updated); await this.ctx.storage.put(`user:${id}`, updated); return updated; } } ``` ## Rate Limiting ```typescript export class RateLimiter extends DurableObject { async checkLimit(key: string, limit: number, window: number): Promise { const now = Date.now(); this.sql.exec('DELETE FROM requests WHERE key = ? AND timestamp < ?', key, now - window); const count = this.sql.exec('SELECT COUNT(*) as count FROM requests WHERE key = ?', key).one().count; if (count >= limit) return false; this.sql.exec('INSERT INTO requests (key, timestamp) VALUES (?, ?)', key, now); return true; } } ``` ## Batch Processing with Alarms ```typescript export class BatchProcessor extends DurableObject { pending: string[] = []; async addItem(item: string) { this.pending.push(item); if (!await this.ctx.storage.getAlarm()) await this.ctx.storage.setAlarm(Date.now() + 5000); } async alarm() { const items = [...this.pending]; this.pending = []; this.sql.exec(`INSERT INTO processed_items (item, timestamp) VALUES ${items.map(() => "(?, ?)").join(", ")}`, ...items.flatMap(item => [item, Date.now()])); } } ``` ## Initialization Pattern ```typescript export class Counter extends DurableObject { value: number; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); ctx.blockConcurrencyWhile(async () => { this.value = (await ctx.storage.get("value")) || 0; }); } async increment() { this.value++; this.ctx.storage.put("value", this.value); // Don't await (output gate protects) return this.value; } } ``` ## Safe Counter / Optimized Write ```typescript // Input gate blocks other requests async getUniqueNumber(): Promise { let val = await this.ctx.storage.get("counter"); await this.ctx.storage.put("counter", val + 1); return val; } // No await on write - output gate delays response until write confirms async increment(): Promise { let val = await this.ctx.storage.get("counter"); this.ctx.storage.put("counter", val + 1); return new Response(String(val)); } ``` ## Parent-Child Coordination Hierarchical DO pattern where parent manages child DOs: ```typescript // Parent DO coordinates children export class Workspace extends DurableObject { async createDocument(name: string): Promise { const docId = crypto.randomUUID(); const childId = this.env.DOCUMENT.idFromName(`${this.ctx.id.toString()}:${docId}`); const childStub = this.env.DOCUMENT.get(childId); await childStub.initialize(name); // Track child in parent storage this.sql.exec('INSERT INTO documents (id, name, created) VALUES (?, ?, ?)', docId, name, Date.now()); return docId; } async listDocuments(): Promise { return this.sql.exec('SELECT id FROM documents').toArray().map(r => r.id); } } // Child DO export class Document extends DurableObject { async initialize(name: string) { this.sql.exec('CREATE TABLE IF NOT EXISTS content(key TEXT PRIMARY KEY, value TEXT)'); this.sql.exec('INSERT INTO content VALUES (?, ?)', 'name', name); } } ``` ## Write Coalescing Pattern Multiple writes to same key coalesce atomically (last write wins): ```typescript async updateMetrics(userId: string, actions: Action[]) { // All writes coalesce - no await needed for (const action of actions) { this.ctx.storage.put(`user:${userId}:lastAction`, action.type); this.ctx.storage.put(`user:${userId}:count`, await this.ctx.storage.get(`user:${userId}:count`) + 1); } // Output gate ensures all writes confirm before response return new Response("OK"); } // Atomic batch with SQL async batchUpdate(items: Item[]) { this.sql.exec('BEGIN'); for (const item of items) { this.sql.exec('INSERT OR REPLACE INTO items VALUES (?, ?)', item.id, item.value); } this.sql.exec('COMMIT'); } ``` ## Cleanup ```typescript async cleanup() { await this.ctx.storage.deleteAlarm(); // Separate from deleteAll await this.ctx.storage.deleteAll(); } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/do-storage/testing.md ================================================ # DO Storage Testing Testing Durable Objects with storage using `vitest-pool-workers`. ## Setup **vitest.config.ts:** ```typescript import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" } } } } }); ``` **package.json:** Add `@cloudflare/vitest-pool-workers` and `vitest` to devDependencies ## Basic Testing ```typescript import { env, runInDurableObject } from "cloudflare:test"; import { describe, it, expect } from "vitest"; describe("Counter DO", () => { it("increments counter", async () => { const id = env.COUNTER.idFromName("test"); const result = await runInDurableObject(env.COUNTER, id, async (instance, state) => { const val1 = await instance.increment(); const val2 = await instance.increment(); return { val1, val2 }; }); expect(result.val1).toBe(1); expect(result.val2).toBe(2); }); }); ``` ## Testing SQL Storage ```typescript it("creates and queries users", async () => { const id = env.USER_MANAGER.idFromName("test"); await runInDurableObject(env.USER_MANAGER, id, async (instance, state) => { await instance.createUser("alice@example.com", "Alice"); const user = await instance.getUser("alice@example.com"); expect(user).toEqual({ email: "alice@example.com", name: "Alice" }); }); }); it("handles schema migrations", async () => { const id = env.USER_MANAGER.idFromName("migration-test"); await runInDurableObject(env.USER_MANAGER, id, async (instance, state) => { const version = state.storage.sql.exec( "SELECT value FROM _meta WHERE key = 'schema_version'" ).one()?.value; expect(version).toBe("1"); }); }); ``` ## Testing Alarms ```typescript import { runDurableObjectAlarm } from "cloudflare:test"; it("processes batch on alarm", async () => { const id = env.BATCH_PROCESSOR.idFromName("test"); // Add items await runInDurableObject(env.BATCH_PROCESSOR, id, async (instance) => { await instance.addItem("item1"); await instance.addItem("item2"); }); // Trigger alarm await runDurableObjectAlarm(env.BATCH_PROCESSOR, id); // Verify processed await runInDurableObject(env.BATCH_PROCESSOR, id, async (instance, state) => { const count = state.storage.sql.exec( "SELECT COUNT(*) as count FROM processed_items" ).one().count; expect(count).toBe(2); }); }); ``` ## Testing Concurrency ```typescript it("handles concurrent increments safely", async () => { const id = env.COUNTER.idFromName("concurrent-test"); // Parallel increments const results = await Promise.all([ runInDurableObject(env.COUNTER, id, (i) => i.increment()), runInDurableObject(env.COUNTER, id, (i) => i.increment()), runInDurableObject(env.COUNTER, id, (i) => i.increment()) ]); // All should get unique values expect(new Set(results).size).toBe(3); expect(Math.max(...results)).toBe(3); }); ``` ## Test Isolation ```typescript // Per-test unique IDs let testId: string; beforeEach(() => { testId = crypto.randomUUID(); }); it("isolated test", async () => { const id = env.MY_DO.idFromName(testId); // Uses unique DO instance }); // Cleanup pattern it("with cleanup", async () => { const id = env.MY_DO.idFromName("cleanup-test"); try { await runInDurableObject(env.MY_DO, id, async (instance) => {}); } finally { await runInDurableObject(env.MY_DO, id, async (instance, state) => { await state.storage.deleteAll(); }); } }); ``` ## Testing PITR ```typescript it("restores from bookmark", async () => { const id = env.MY_DO.idFromName("pitr-test"); // Create checkpoint const bookmark = await runInDurableObject(env.MY_DO, id, async (instance, state) => { await state.storage.put("value", 1); return await state.storage.getCurrentBookmark(); }); // Modify and restore await runInDurableObject(env.MY_DO, id, async (instance, state) => { await state.storage.put("value", 2); await state.storage.onNextSessionRestoreBookmark(bookmark); state.abort(); }); // Verify restored await runInDurableObject(env.MY_DO, id, async (instance, state) => { const value = await state.storage.get("value"); expect(value).toBe(1); }); }); ``` ## Testing Transactions ```typescript it("rolls back on error", async () => { const id = env.BANK.idFromName("transaction-test"); await runInDurableObject(env.BANK, id, async (instance, state) => { await state.storage.put("balance", 100); await expect( state.storage.transaction(async () => { await state.storage.put("balance", 50); throw new Error("Cancel"); }) ).rejects.toThrow("Cancel"); const balance = await state.storage.get("balance"); expect(balance).toBe(100); // Rolled back }); }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/durable-objects/README.md ================================================ # Cloudflare Durable Objects Expert guidance for building stateful applications with Cloudflare Durable Objects. ## Reading Order 1. **First time?** Read this overview + Quick Start 2. **Setting up?** See [Configuration](./configuration.md) 3. **Building features?** Use decision trees below → [Patterns](./patterns.md) 4. **Debugging issues?** Check [Gotchas](./gotchas.md) 5. **Deep dive?** [API](./api.md) and [DO Storage](../do-storage/README.md) ## Overview Durable Objects combine compute with storage in globally-unique, strongly-consistent packages: - **Globally unique instances**: Each DO has unique ID for multi-client coordination - **Co-located storage**: Fast, strongly-consistent storage with compute - **Automatic placement**: Objects spawn near first request location - **Stateful serverless**: In-memory state + persistent storage - **Single-threaded**: Serial request processing (no race conditions) ## Rules of Durable Objects Critical rules preventing most production issues: 1. **One alarm per DO** - Schedule multiple events via queue pattern 2. **~1K req/s per DO max** - Shard for higher throughput 3. **Constructor runs every wake** - Keep initialization light; use lazy loading 4. **Hibernation clears memory** - In-memory state lost; persist critical data 5. **Use `ctx.waitUntil()` for cleanup** - Ensures completion after response sent 6. **No setTimeout for persistence** - Use `setAlarm()` for reliable scheduling ## Core Concepts ### Class Structure All DOs extend `DurableObject` base class with constructor receiving `DurableObjectState` (storage, WebSockets, alarms) and `Env` (bindings). ### Lifecycle States ``` [Not Created] → [Active] ⇄ [Hibernated] → [Evicted] ↓ [Destroyed] ``` - **Not Created**: DO ID exists but instance never spawned - **Active**: Processing requests, in-memory state valid, billed per GB-hour - **Hibernated**: WebSocket connections open but zero compute, zero cost - **Evicted**: Removed from memory; next request triggers cold start - **Destroyed**: Data deleted via migration or manual deletion ### Accessing from Workers Workers use bindings to get stubs, then call RPC methods directly (recommended) or use fetch handler (legacy). **RPC vs fetch() decision:** ``` ├─ New project + compat ≥2024-04-03 → RPC (type-safe, simpler) ├─ Need HTTP semantics (headers, status) → fetch() ├─ Proxying requests to DO → fetch() └─ Legacy compatibility → fetch() ``` See [Patterns: RPC vs fetch()](./patterns.md) for examples. ### ID Generation - `idFromName()`: Deterministic, named coordination (rate limiting, locks) - `newUniqueId()`: Random IDs for sharding high-throughput workloads - `idFromString()`: Derive from existing IDs - Jurisdiction option: Data locality compliance ### Storage Options **Which storage API?** ``` ├─ Structured data, relations, transactions → SQLite (recommended) ├─ Simple KV on SQLite DO → ctx.storage.kv (sync KV) └─ Legacy KV-only DO → ctx.storage (async KV) ``` - **SQLite** (recommended): Structured data, transactions, 10GB/DO - **Synchronous KV API**: Simple key-value on SQLite objects - **Asynchronous KV API**: Legacy/advanced use cases See [DO Storage](../do-storage/README.md) for deep dive. ### Special Features - **Alarms**: Schedule future execution per-DO (1 per DO - use queue pattern for multiple) - **WebSocket Hibernation**: Zero-cost idle connections (memory cleared on hibernation) - **Point-in-Time Recovery**: Restore to any point in 30 days (SQLite only) ## Quick Start ```typescript import { DurableObject } from "cloudflare:workers"; export class Counter extends DurableObject { async increment(): Promise { const result = this.ctx.storage.sql.exec( `INSERT INTO counters (id, value) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET value = value + 1 RETURNING value` ).one(); return result.value; } } // Worker access export default { async fetch(request: Request, env: Env): Promise { const id = env.COUNTER.idFromName("global"); const stub = env.COUNTER.get(id); const count = await stub.increment(); return new Response(`Count: ${count}`); } }; ``` ## Decision Trees ### What do you need? ``` ├─ Coordinate requests (rate limit, lock, session) │ → idFromName(identifier) → [Patterns: Rate Limiting/Locks](./patterns.md) │ ├─ High throughput (>1K req/s) │ → Sharding with newUniqueId() or hash → [Patterns: Sharding](./patterns.md) │ ├─ Real-time updates (WebSocket, chat, collab) │ → WebSocket hibernation + room pattern → [Patterns: Real-time](./patterns.md) │ ├─ Background work (cleanup, notifications, scheduled tasks) │ → Alarms + queue pattern (1 alarm/DO) → [Patterns: Multiple Events](./patterns.md) │ └─ User sessions with expiration → Session pattern + alarm cleanup → [Patterns: Session Management](./patterns.md) ``` ### Which access pattern? ``` ├─ New project + typed methods → RPC (compat ≥2024-04-03) ├─ Need HTTP semantics → fetch() ├─ Proxying to DO → fetch() └─ Legacy compat → fetch() ``` See [Patterns: RPC vs fetch()](./patterns.md) for examples. ### Which storage? ``` ├─ Structured data, SQL queries, transactions → SQLite (recommended) ├─ Simple KV on SQLite DO → ctx.storage.kv (sync API) └─ Legacy KV-only DO → ctx.storage (async API) ``` See [DO Storage](../do-storage/README.md) for complete guide. ## Essential Commands ```bash npx wrangler dev # Local dev with DOs npx wrangler dev --remote # Test against prod DOs npx wrangler deploy # Deploy + auto-apply migrations ``` ## Resources **Docs**: https://developers.cloudflare.com/durable-objects/ **API Reference**: https://developers.cloudflare.com/durable-objects/api/ **Examples**: https://developers.cloudflare.com/durable-objects/examples/ ## In This Reference - **[Configuration](./configuration.md)** - wrangler.jsonc setup, migrations, bindings, environments - **[API](./api.md)** - Class structure, ctx methods, alarms, WebSocket hibernation - **[Patterns](./patterns.md)** - Sharding, rate limiting, locks, real-time, sessions - **[Gotchas](./gotchas.md)** - Limits, hibernation caveats, common errors ## See Also - **[DO Storage](../do-storage/README.md)** - SQLite, KV, transactions (detailed storage guide) - **[Workers](../workers/README.md)** - Core Workers runtime features - **[WebSockets](../websockets/README.md)** - WebSocket APIs and patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/durable-objects/api.md ================================================ # Durable Objects API ## Class Structure ```typescript import { DurableObject } from "cloudflare:workers"; export class MyDO extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); // Runs on EVERY wake - keep light! } // RPC methods (called directly from worker) async myMethod(arg: string): Promise { return arg; } // fetch handler (legacy/HTTP semantics) async fetch(req: Request): Promise { /* ... */ } // Lifecycle handlers async alarm() { /* alarm fired */ } async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) { /* ... */ } async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { /* ... */ } async webSocketError(ws: WebSocket, error: unknown) { /* ... */ } } ``` ## DurableObjectState Context Methods ### Concurrency Control ```typescript // Complete work after response sent (e.g., cleanup, logging) this.ctx.waitUntil(promise: Promise): void // Critical section - blocks all other requests until complete await this.ctx.blockConcurrencyWhile(async () => { // No other requests processed during this block // Use for initialization or critical operations }) ``` **When to use:** - `waitUntil()`: Background cleanup, logging, non-critical work after response - `blockConcurrencyWhile()`: First-time init, schema migration, critical state setup ### Lifecycle ```typescript this.ctx.id // DurableObjectId of this instance this.ctx.abort() // Force eviction (use after PITR restore to reload state) ``` ### Storage Access ```typescript this.ctx.storage.sql // SQLite API (recommended) this.ctx.storage.kv // Sync KV API (SQLite DOs only) this.ctx.storage // Async KV API (legacy/KV-only DOs) ``` See **[DO Storage](../do-storage/README.md)** for complete storage API reference. ### WebSocket Management ```typescript this.ctx.acceptWebSocket(ws: WebSocket, tags?: string[]) // Enable hibernation this.ctx.getWebSockets(tag?: string): WebSocket[] // Get by tag or all this.ctx.getTags(ws: WebSocket): string[] // Get tags for connection ``` ### Alarms ```typescript await this.ctx.storage.setAlarm(timestamp: number | Date) // Schedule (overwrites existing) await this.ctx.storage.getAlarm(): number | null // Get next alarm time await this.ctx.storage.deleteAlarm(): void // Cancel alarm ``` **Limit:** 1 alarm per DO. Use queue pattern for multiple events (see [Patterns](./patterns.md)). ## Storage APIs For detailed storage documentation including SQLite queries, KV operations, transactions, and Point-in-Time Recovery, see **[DO Storage](../do-storage/README.md)**. Quick reference: ```typescript // SQLite (recommended) this.ctx.storage.sql.exec("SELECT * FROM users WHERE id = ?", userId).one() // Sync KV (SQLite DOs only) this.ctx.storage.kv.get("key") // Async KV (legacy) await this.ctx.storage.get("key") ``` ## Alarms Schedule future work that survives eviction: ```typescript // Set alarm (overwrites any existing alarm) await this.ctx.storage.setAlarm(Date.now() + 3600000) // 1 hour from now await this.ctx.storage.setAlarm(new Date("2026-02-01")) // Absolute time // Check next alarm const nextRun = await this.ctx.storage.getAlarm() // null if none // Cancel alarm await this.ctx.storage.deleteAlarm() // Handler called when alarm fires async alarm() { // Runs once alarm triggers // DO wakes from hibernation if needed // Use for cleanup, notifications, scheduled tasks } ``` **Limitations:** - 1 alarm per DO maximum - Overwrites previous alarm when set - Use queue pattern for multiple scheduled events (see [Patterns](./patterns.md)) **Reliability:** - Alarms survive DO eviction/restart - Cloudflare retries failed alarms automatically - Not guaranteed exactly-once (handle idempotently) ## WebSocket Hibernation Hibernation allows DOs with open WebSocket connections to consume zero compute/memory until message arrives. ```typescript async fetch(req: Request): Promise { const [client, server] = Object.values(new WebSocketPair()); this.ctx.acceptWebSocket(server, ["room:123"]); // Tags for filtering server.serializeAttachment({ userId: "abc" }); // Persisted metadata return new Response(null, { status: 101, webSocket: client }); } // Called when message arrives (DO wakes from hibernation) async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) { const data = ws.deserializeAttachment(); // Retrieve metadata for (const c of this.ctx.getWebSockets("room:123")) c.send(msg); } // Called on close (optional handler) async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { // Cleanup logic, remove from lists, etc. } // Called on error (optional handler) async webSocketError(ws: WebSocket, error: unknown) { console.error("WebSocket error:", error); // Handle error, close connection, etc. } ``` **Key concepts:** - **Auto-hibernation:** DO hibernates when no active requests/alarms - **Zero cost:** Hibernated DOs incur no charges while preserving connections - **Memory cleared:** All in-memory state lost on hibernation - **Attachment persistence:** Use `serializeAttachment()` for per-connection metadata that survives hibernation - **Tags for filtering:** Group connections by room/channel/user for targeted broadcasts **Handler lifecycle:** - `webSocketMessage`: DO wakes, processes message, may hibernate after - `webSocketClose`: Called when client closes (optional - implement for cleanup) - `webSocketError`: Called on connection error (optional - implement for error handling) **Metadata persistence:** ```typescript // Store connection metadata (survives hibernation) ws.serializeAttachment({ userId: "abc", room: "lobby" }) // Retrieve after hibernation const { userId, room } = ws.deserializeAttachment() ``` ## See Also - **[DO Storage](../do-storage/README.md)** - Complete storage API reference - **[Patterns](./patterns.md)** - Real-world usage patterns - **[Gotchas](./gotchas.md)** - Hibernation caveats and limits ================================================ FILE: skills/.curated/cloudflare-deploy/references/durable-objects/configuration.md ================================================ # Durable Objects Configuration ## Basic Setup ```jsonc { "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use latest; ≥2024-04-03 for RPC "durable_objects": { "bindings": [ { "name": "MY_DO", // Env binding name "class_name": "MyDO" // Class exported from this worker }, { "name": "EXTERNAL", // Access DO from another worker "class_name": "ExternalDO", "script_name": "other-worker" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MyDO"] } // Prefer SQLite ] } ``` ## Binding Options ```jsonc { "name": "BINDING_NAME", "class_name": "ClassName", "script_name": "other-worker", // Optional: external DO "environment": "production" // Optional: isolate by env } ``` ## Jurisdiction (Data Locality) Specify jurisdiction at ID creation for data residency compliance: ```typescript // EU data residency const id = env.MY_DO.idFromName("user:123", { jurisdiction: "eu" }) // Available jurisdictions const jurisdictions = ["eu", "fedramp"] // More may be added // All operations on this DO stay within jurisdiction const stub = env.MY_DO.get(id) await stub.someMethod() // Data stays in EU ``` **Key points:** - Set at ID creation time, immutable afterward - DO instance physically located within jurisdiction - Storage and compute guaranteed within boundary - Use for GDPR, FedRAMP, other compliance requirements - No cross-jurisdiction access (requests fail if DO in different jurisdiction) ## Migrations ```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! ] } ``` **Migration rules:** - Tags must be unique and sequential (v1, v2, v3...) - No rollback supported (test with `--dry-run` first) - Auto-applied on deploy - `new_sqlite_classes` recommended over `new_classes` (SQLite vs KV) - `deleted_classes` immediately destroys ALL data (irreversible) ## Environment Isolation Separate DO namespaces per environment (staging/production have distinct object instances): ```jsonc { "durable_objects": { "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }, "env": { "production": { "durable_objects": { "bindings": [ { "name": "MY_DO", "class_name": "MyDO", "environment": "production" } ] } } } } ``` Deploy: `npx wrangler deploy --env production` ## Limits & Settings ```jsonc { "limits": { "cpu_ms": 300000 // Max CPU time: 30s default, 300s max } } ``` See [Gotchas](./gotchas.md) for complete limits table. ## Types ```typescript import { DurableObject } from "cloudflare:workers"; interface Env { MY_DO: DurableObjectNamespace; } export class MyDO extends DurableObject {} type DurableObjectNamespace = { newUniqueId(options?: { jurisdiction?: string }): DurableObjectId; idFromName(name: string): DurableObjectId; idFromString(id: string): DurableObjectId; get(id: DurableObjectId): DurableObjectStub; }; ``` ## Commands ```bash # Development npx wrangler dev # Local dev npx wrangler dev --remote # Test against production DOs # Deployment npx wrangler deploy # Deploy + auto-apply migrations npx wrangler deploy --dry-run # Validate migrations without deploying npx wrangler deploy --env production # Management npx wrangler durable-objects list # List namespaces npx wrangler durable-objects info # Inspect specific DO npx wrangler durable-objects delete # Delete DO (destroys data) ``` ## See Also - **[API](./api.md)** - DurableObjectState and lifecycle handlers - **[Patterns](./patterns.md)** - Multi-environment patterns - **[Gotchas](./gotchas.md)** - Migration caveats, limits ================================================ FILE: skills/.curated/cloudflare-deploy/references/durable-objects/gotchas.md ================================================ # Durable Objects Gotchas ## Common Errors ### "Hibernation Cleared My In-Memory State" **Problem:** Variables lost after hibernation **Cause:** DO auto-hibernates when idle; in-memory state not persisted **Solution:** Use `ctx.storage` for critical data, `ws.serializeAttachment()` for per-connection metadata ```typescript // ❌ Wrong - lost on hibernation private userCount = 0; async webSocketMessage(ws: WebSocket, msg: string) { this.userCount++; // Lost! } // ✅ Right - persisted async webSocketMessage(ws: WebSocket, msg: string) { const count = this.ctx.storage.kv.get("userCount") || 0; this.ctx.storage.kv.put("userCount", count + 1); } ``` ### "setTimeout Didn't Fire After Restart" **Problem:** Scheduled work lost on eviction **Cause:** `setTimeout` in-memory only; eviction clears timers **Solution:** Use `ctx.storage.setAlarm()` for reliable scheduling ```typescript // ❌ Wrong - lost on eviction setTimeout(() => this.cleanup(), 3600000); // ✅ Right - survives eviction await this.ctx.storage.setAlarm(Date.now() + 3600000); async alarm() { await this.cleanup(); } ``` ### "Constructor Runs on Every Wake" **Problem:** Expensive init logic slows all requests **Cause:** Constructor runs on every wake (first request after eviction OR after hibernation) **Solution:** Lazy initialization or cache in storage **Critical understanding:** Constructor runs in two scenarios: 1. **Cold start** - DO evicted from memory, first request creates new instance 2. **Wake from hibernation** - DO with WebSockets hibernated, message/alarm wakes it ```typescript // ❌ Wrong - expensive on every wake constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.heavyData = this.loadExpensiveData(); // Slow! } // ✅ Right - lazy load private heavyData?: HeavyData; private getHeavyData() { if (!this.heavyData) this.heavyData = this.loadExpensiveData(); return this.heavyData; } ``` ### "Durable Object Overloaded (503 errors)" **Problem:** 503 errors under load **Cause:** Single DO exceeding ~1K req/s throughput limit **Solution:** Shard across multiple DOs (see [Patterns: Sharding](./patterns.md)) ### "Storage Quota Exceeded (Write failures)" **Problem:** Write operations failing **Cause:** DO storage exceeding 10GB limit or account quota **Solution:** Cleanup with alarms, use `deleteAll()` for old data, upgrade plan ### "CPU Time Exceeded (Terminated)" **Problem:** Request terminated mid-execution **Cause:** Processing exceeding 30s CPU time default limit **Solution:** Increase `limits.cpu_ms` in wrangler.jsonc (max 300s) or chunk work ### "WebSockets Disconnect on Eviction" **Problem:** Connections drop unexpectedly **Cause:** DO evicted from memory without hibernation API **Solution:** Use WebSocket hibernation handlers + client reconnection logic ### "Migration Failed (Deploy error)" **Cause:** Non-unique tags, non-sequential tags, or invalid class names in migration **Solution:** Check tag uniqueness/sequential ordering and verify class names are correct ### "RPC Method Not Found" **Cause:** compatibility_date < 2024-04-03 preventing RPC usage **Solution:** Update compatibility_date to >= 2024-04-03 or use fetch() instead of RPC ### "Only One Alarm Allowed" **Cause:** Need multiple scheduled tasks but only one alarm supported per DO **Solution:** Use event queue pattern to schedule multiple tasks with single alarm ### "Race Condition Despite Single-Threading" **Problem:** Concurrent requests see inconsistent state **Cause:** Async operations allow request interleaving (await = yield point) **Solution:** Use `blockConcurrencyWhile()` for critical sections or atomic storage ops ```typescript // ❌ Wrong - race condition async incrementCounter() { const count = await this.ctx.storage.get("count") || 0; // ⚠️ Another request could execute here during await await this.ctx.storage.put("count", count + 1); } // ✅ Right - atomic operation async incrementCounter() { return this.ctx.storage.sql.exec( "INSERT INTO counters (id, value) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET value = value + 1 RETURNING value" ).one().value; } // ✅ Right - explicit locking async criticalOperation() { await this.ctx.blockConcurrencyWhile(async () => { const count = await this.ctx.storage.get("count") || 0; await this.ctx.storage.put("count", count + 1); }); } ``` ### "Migration Rollback Not Supported" **Cause:** Attempting to rollback a migration after deployment **Solution:** Test with `--dry-run` before deploying; migrations cannot be rolled back ### "deleted_classes Destroys Data" **Problem:** Migration deleted all data **Cause:** `deleted_classes` migration immediately destroys all DO instances and data **Solution:** Test with `--dry-run`; use `transferred_classes` to preserve data during moves ### "Cold Starts Are Slow" **Problem:** First request after eviction takes longer **Cause:** DO constructor + initial storage access on cold start **Solution:** Expected behavior; optimize constructor, use connection pooling in clients, consider warming strategy for critical DOs ```typescript // Warming strategy (periodically ping critical DOs) export default { async scheduled(event: ScheduledEvent, env: Env) { const criticalIds = ["auth", "sessions", "locks"]; await Promise.all(criticalIds.map(name => { const id = env.MY_DO.idFromName(name); const stub = env.MY_DO.get(id); return stub.ping(); // Keep warm })); } }; ``` ## Limits | Limit | Free | Paid | Notes | |-------|------|------|-------| | SQLite storage per DO | 10 GB | 10 GB | Per Durable Object instance | | SQLite total storage | 5 GB | Unlimited | Account-wide quota | | Key+value size | 2 MB | 2 MB | Single KV pair (SQLite/async) | | CPU time default | 30s | 30s | Per request; configurable | | CPU time max | 300s | 300s | Set via `limits.cpu_ms` | | DO classes | 100 | 500 | Distinct DO class definitions | | SQL columns | 100 | 100 | Per table | | SQL statement size | 100 KB | 100 KB | Max SQL query size | | WebSocket message size | 32 MiB | 32 MiB | Per message | | Request throughput | ~1K req/s | ~1K req/s | Per DO (soft limit - shard for more) | | Alarms per DO | 1 | 1 | Use queue pattern for multiple events | | Total DOs | Unlimited | Unlimited | Create as many instances as needed | | WebSockets | Unlimited | Unlimited | Within 128MB memory limit per DO | | Memory per DO | 128 MB | 128 MB | In-memory state + WebSocket buffers | ## Hibernation Caveats 1. **Memory cleared** - All in-memory variables lost; reconstruct from storage or `deserializeAttachment()` 2. **Constructor reruns** - Runs on wake; avoid expensive operations, use lazy initialization 3. **No guarantees** - DO may evict instead of hibernate; design for both 4. **Attachment limit** - `serializeAttachment()` data must be JSON-serializable, keep small 5. **Alarm wakes DO** - Alarm prevents hibernation until handler completes 6. **WebSocket state not automatic** - Must explicitly persist with `serializeAttachment()` or storage ## See Also - **[Patterns](./patterns.md)** - Workarounds for common limitations - **[API](./api.md)** - Storage limits and quotas - **[Configuration](./configuration.md)** - Setting CPU limits ================================================ FILE: skills/.curated/cloudflare-deploy/references/durable-objects/patterns.md ================================================ # Durable Objects Patterns ## When to Use Which Pattern | Need | Pattern | ID Strategy | |------|---------|-------------| | Rate limit per user/IP | Rate Limiting | `idFromName(identifier)` | | Mutual exclusion | Distributed Lock | `idFromName(resource)` | | >1K req/s throughput | Sharding | `newUniqueId()` or hash | | Real-time updates | WebSocket Collab | `idFromName(room)` | | User sessions | Session Management | `idFromName(sessionId)` | | Background cleanup | Alarm-based | Any | ## RPC vs fetch() **RPC** (compat ≥2024-04-03): Type-safe, simpler, default for new projects **fetch()**: Legacy compat, HTTP semantics, proxying ```typescript const count = await stub.increment(); // RPC const count = await (await stub.fetch(req)).json(); // fetch() ``` ## Sharding (High Throughput) Single DO ~1K req/s max. Shard for higher throughput: ```typescript export default { async fetch(req: Request, env: Env): Promise { const userId = new URL(req.url).searchParams.get("user"); const hash = hashCode(userId) % 100; // 100 shards const id = env.COUNTER.idFromName(`shard:${hash}`); return env.COUNTER.get(id).fetch(req); } }; function hashCode(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) hash = ((hash << 5) - hash) + str.charCodeAt(i); return Math.abs(hash); } ``` **Decisions:** - **Shard count**: 10-1000 typical (start with 100, measure, adjust) - **Shard key**: User ID, IP, session - must distribute evenly (use hash) - **Aggregation**: Coordinator DO or external system (D1, R2) ## Rate Limiting ```typescript async checkLimit(key: string, limit: number, windowMs: number): Promise { const req = this.ctx.storage.sql.exec("SELECT COUNT(*) as count FROM requests WHERE key = ? AND timestamp > ?", key, Date.now() - windowMs).one(); if (req.count >= limit) return false; this.ctx.storage.sql.exec("INSERT INTO requests (key, timestamp) VALUES (?, ?)", key, Date.now()); return true; } ``` ## Distributed Lock ```typescript private held = false; async acquire(timeoutMs = 5000): Promise { if (this.held) return false; this.held = true; await this.ctx.storage.setAlarm(Date.now() + timeoutMs); return true; } async release() { this.held = false; await this.ctx.storage.deleteAlarm(); } async alarm() { this.held = false; } // Auto-release on timeout ``` ## Hibernation-Aware Pattern Preserve state across hibernation: ```typescript async fetch(req: Request): Promise { const [client, server] = Object.values(new WebSocketPair()); const userId = new URL(req.url).searchParams.get("user"); server.serializeAttachment({ userId }); // Survives hibernation this.ctx.acceptWebSocket(server, ["room:lobby"]); server.send(JSON.stringify({ type: "init", state: this.ctx.storage.kv.get("state") })); return new Response(null, { status: 101, webSocket: client }); } async webSocketMessage(ws: WebSocket, msg: string) { const { userId } = ws.deserializeAttachment(); // Retrieve after wake const state = this.ctx.storage.kv.get("state") || {}; state[userId] = JSON.parse(msg); this.ctx.storage.kv.put("state", state); for (const c of this.ctx.getWebSockets("room:lobby")) c.send(msg); } ``` ## Real-time Collaboration Broadcast updates to all connected clients: ```typescript async webSocketMessage(ws: WebSocket, msg: string) { const data = JSON.parse(msg); this.ctx.storage.kv.put("doc", data.content); // Persist for (const c of this.ctx.getWebSockets()) if (c !== ws) c.send(msg); // Broadcast } ``` ### WebSocket Reconnection **Client-side** (exponential backoff): ```typescript class ResilientWS { private delay = 1000; connect(url: string) { const ws = new WebSocket(url); ws.onclose = () => setTimeout(() => { this.connect(url); this.delay = Math.min(this.delay * 2, 30000); }, this.delay); } } ``` **Server-side** (cleanup on close): ```typescript async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { const { userId } = ws.deserializeAttachment(); this.ctx.storage.sql.exec("UPDATE users SET online = false WHERE id = ?", userId); for (const c of this.ctx.getWebSockets()) c.send(JSON.stringify({ type: "user_left", userId })); } ``` ## Session Management ```typescript async createSession(userId: string, data: object): Promise { const id = crypto.randomUUID(), exp = Date.now() + 86400000; this.ctx.storage.sql.exec("INSERT INTO sessions VALUES (?, ?, ?, ?)", id, userId, JSON.stringify(data), exp); await this.ctx.storage.setAlarm(exp); return id; } async getSession(id: string): Promise { const row = this.ctx.storage.sql.exec("SELECT data FROM sessions WHERE id = ? AND expires_at > ?", id, Date.now()).one(); return row ? JSON.parse(row.data) : null; } async alarm() { this.ctx.storage.sql.exec("DELETE FROM sessions WHERE expires_at <= ?", Date.now()); } ``` ## Multiple Events (Single Alarm) Queue pattern to schedule multiple events: ```typescript async scheduleEvent(id: string, runAt: number) { await this.ctx.storage.put(`event:${id}`, { id, runAt }); const curr = await this.ctx.storage.getAlarm(); if (!curr || runAt < curr) await this.ctx.storage.setAlarm(runAt); } async alarm() { const events = await this.ctx.storage.list({ prefix: "event:" }), now = Date.now(); let next = null; for (const [key, ev] of events) { if (ev.runAt <= now) { await this.processEvent(ev); await this.ctx.storage.delete(key); } else if (!next || ev.runAt < next) next = ev.runAt; } if (next) await this.ctx.storage.setAlarm(next); } ``` ## Graceful Cleanup Use `ctx.waitUntil()` to complete work after response: ```typescript async myMethod() { const response = { success: true }; this.ctx.waitUntil(this.ctx.storage.sql.exec("DELETE FROM old_data WHERE timestamp < ?", cutoff)); return response; } ``` ## Best Practices - **Design**: Use `idFromName()` for coordination, `newUniqueId()` for sharding, minimize constructor work - **Storage**: Prefer SQLite, batch with transactions, set alarms for cleanup, use PITR before risky ops - **Performance**: ~1K req/s per DO max - shard for more, cache in memory, use alarms for deferred work - **Reliability**: Handle 503 with retry+backoff, design for cold starts, test migrations with `--dry-run` - **Security**: Validate inputs in Workers, rate limit DO creation, use jurisdiction for compliance ## See Also - **[API](./api.md)** - ctx methods, WebSocket handlers - **[Gotchas](./gotchas.md)** - Hibernation caveats, common errors - **[DO Storage](../do-storage/README.md)** - Storage patterns and transactions ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-routing/README.md ================================================ # Cloudflare Email Routing Skill Reference ## Overview Cloudflare Email Routing enables custom email addresses for your domain that route to verified destination addresses. It's free, privacy-focused (no storage/access), and includes Email Workers for programmatic email processing. **Available to all Cloudflare customers using Cloudflare as authoritative nameserver.** ## Quick Start ```typescript // Basic email handler export default { async email(message, env, ctx) { // CRITICAL: Must consume stream before response const parser = new PostalMime.default(); const email = await parser.parse(await message.raw.arrayBuffer()); // Process email console.log(`From: ${message.from}, Subject: ${email.subject}`); // Forward or reject await message.forward("verified@destination.com"); } } satisfies ExportedHandler; ``` ## Reading Order **Start here based on your goal:** 1. **New to Email Routing?** → [configuration.md](configuration.md) → [patterns.md](patterns.md) 2. **Adding Workers?** → [api.md](api.md) § Worker Runtime API → [patterns.md](patterns.md) 3. **Sending emails?** → [api.md](api.md) § SendEmail Binding 4. **Managing via API?** → [api.md](api.md) § REST API Operations 5. **Debugging issues?** → [gotchas.md](gotchas.md) ## Decision Tree ``` Need to receive emails? ├─ Simple forwarding only? → Dashboard rules (configuration.md) ├─ Complex logic/filtering? → Email Workers (api.md + patterns.md) └─ Parse attachments/body? → postal-mime library (patterns.md § Parse Email) Need to send emails? ├─ From Worker? → SendEmail binding (api.md § SendEmail) └─ From external app? → Use external SMTP/API service Having issues? ├─ Email not arriving? → gotchas.md § Mail Authentication ├─ Worker crashing? → gotchas.md § Stream Consumption └─ Forward failing? → gotchas.md § Destination Verification ``` ## Key Concepts **Routing Rules**: Pattern-based forwarding configured via Dashboard/API. Simple but limited. **Email Workers**: Custom TypeScript handlers with full email access. Handles complex logic, parsing, storage, rejection. **SendEmail Binding**: Outbound email API for Workers. Transactional email only (no marketing/bulk). **ForwardableEmailMessage**: Runtime interface for incoming emails. Provides headers, raw stream, forward/reject methods. ## In This Reference - **[configuration.md](configuration.md)** - Setup, deployment, wrangler config - **[api.md](api.md)** - REST API + Worker runtime API + types - **[patterns.md](patterns.md)** - Common patterns with working examples - **[gotchas.md](gotchas.md)** - Critical pitfalls, troubleshooting, limits ## Architecture ``` Internet → MX Records → Cloudflare Email Routing ├─ Routing Rules (dashboard) └─ Email Worker (your code) ├─ Forward to destination ├─ Reject with reason ├─ Store in R2/KV/D1 └─ Send outbound (SendEmail) ``` ## See Also - [Cloudflare Docs: Email Routing](https://developers.cloudflare.com/email-routing/) - [Cloudflare Docs: Email Workers](https://developers.cloudflare.com/email-routing/email-workers/) - [postal-mime npm package](https://www.npmjs.com/package/postal-mime) ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-routing/api.md ================================================ # Email Routing API Reference ## Worker Runtime API ### Email Handler Interface ```typescript interface ExportedHandler { email?(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): void | Promise; } ``` ### ForwardableEmailMessage Main interface for incoming emails: ```typescript interface ForwardableEmailMessage { readonly from: string; // Envelope sender (e.g., "sender@example.com") readonly to: string; // Envelope recipient (e.g., "you@yourdomain.com") readonly headers: Headers; // Web API Headers object readonly raw: ReadableStream; // Raw MIME message stream setReject(reason: string): void; forward(rcptTo: string, headers?: Headers): Promise; } ``` **Key Properties:** | Property | Type | Description | |----------|------|-------------| | `from` | `string` | Envelope sender (MAIL FROM), not header From | | `to` | `string` | Envelope recipient (RCPT TO), not header To | | `headers` | `Headers` | Email headers (Subject, From, To, etc.) | | `raw` | `ReadableStream` | Raw MIME message (consume once only) | **Methods:** - `setReject(reason)`: Reject email with bounce message - `forward(rcptTo, headers?)`: Forward to verified destination, optionally add headers ### Headers Object Standard Web API Headers interface: ```typescript // Access headers const subject = message.headers.get("subject"); const from = message.headers.get("from"); const messageId = message.headers.get("message-id"); // Check spam score const spamScore = parseFloat(message.headers.get("x-cf-spamh-score") || "0"); if (spamScore > 5) { message.setReject("Spam detected"); } ``` ### Common Headers `subject`, `from`, `to`, `x-cf-spamh-score` (spam score), `message-id` (deduplication), `dkim-signature` (auth) ### Envelope vs Header Addresses **Critical distinction:** ```typescript // Envelope addresses (routing, auth checks) message.from // "bounce@sender.com" (actual sender) message.to // "you@yourdomain.com" (your address) // Header addresses (display, user-facing) message.headers.get("from") // "Alice " message.headers.get("to") // "Bob " ``` **Use envelope addresses for:** - Authentication/SPF checks - Routing decisions - Bounce handling **Use header addresses for:** - Display to users - Reply-To logic - User-facing filtering ## SendEmail Binding Outbound email API for transactional messages. ### Configuration ```jsonc // wrangler.jsonc { "send_email": [ { "name": "EMAIL" } ] } ``` ### TypeScript Types ```typescript interface Env { EMAIL: SendEmail; } interface SendEmail { send(message: EmailMessage): Promise; } interface EmailMessage { from: string | { name?: string; email: string }; to: string | { name?: string; email: string } | Array; subject: string; text?: string; html?: string; headers?: Headers; reply_to?: string | { name?: string; email: string }; } ``` ### Send Email Example ```typescript interface Env { EMAIL: SendEmail; } export default { async fetch(request, env, ctx): Promise { await env.EMAIL.send({ from: { name: "Acme Corp", email: "noreply@yourdomain.com" }, to: [ { name: "Alice", email: "alice@example.com" }, "bob@example.com" ], subject: "Your order #12345 has shipped", text: "Track your package at: https://track.example.com/12345", html: "

Track your package at: View tracking

", reply_to: { name: "Support", email: "support@yourdomain.com" } }); return new Response("Email sent"); } } satisfies ExportedHandler; ``` ### SendEmail Constraints - **From address**: Must be on verified domain (your domain with Email Routing enabled) - **Volume limits**: Transactional only, no bulk/marketing email - **Rate limits**: 100 emails/minute on Free plan, higher on Paid - **No attachments**: Use links to hosted files instead - **No DKIM control**: Cloudflare signs automatically ## REST API Operations Base URL: `https://api.cloudflare.com/client/v4` ### Authentication ```bash curl -H "Authorization: Bearer $API_TOKEN" https://api.cloudflare.com/client/v4/... ``` ### Key Endpoints | Operation | Method | Endpoint | |-----------|--------|----------| | Enable routing | POST | `/zones/{zone_id}/email/routing/enable` | | Disable routing | POST | `/zones/{zone_id}/email/routing/disable` | | List rules | GET | `/zones/{zone_id}/email/routing/rules` | | Create rule | POST | `/zones/{zone_id}/email/routing/rules` | | Verify destination | POST | `/zones/{zone_id}/email/routing/addresses` | | List destinations | GET | `/zones/{zone_id}/email/routing/addresses` | ### Create Routing Rule Example ```bash curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/rules" \ -H "Authorization: Bearer $API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "name": "Forward sales", "matchers": [{"type": "literal", "field": "to", "value": "sales@yourdomain.com"}], "actions": [{"type": "forward", "value": ["alice@company.com"]}], "priority": 0 }' ``` Matcher types: `literal` (exact match), `all` (catch-all). ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-routing/configuration.md ================================================ # Email Routing Configuration ## Wrangler Configuration ### Basic Email Worker ```jsonc // wrangler.jsonc { "name": "email-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", "send_email": [{ "name": "EMAIL" }] } ``` ```typescript // src/index.ts export default { async email(message, env, ctx) { await message.forward("destination@example.com"); } } satisfies ExportedHandler; ``` ### With Storage Bindings ```jsonc { "name": "email-processor", "send_email": [{ "name": "EMAIL" }], "kv_namespaces": [{ "binding": "KV", "id": "abc123" }], "r2_buckets": [{ "binding": "R2", "bucket_name": "emails" }], "d1_databases": [{ "binding": "DB", "database_id": "def456" }] } ``` ```typescript interface Env { EMAIL: SendEmail; KV: KVNamespace; R2: R2Bucket; DB: D1Database; } ``` ## Local Development ```bash npx wrangler dev # Test with curl curl -X POST 'http://localhost:8787/__email' \ --header 'content-type: message/rfc822' \ --data 'From: test@example.com To: you@yourdomain.com Subject: Test Body' ``` ## Deployment ```bash npx wrangler deploy ``` **Connect to Email Routing:** Dashboard: Email > Email Routing > [domain] > Settings > Email Workers > Select worker API: ```bash curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/settings" \ -H "Authorization: Bearer $API_TOKEN" \ -d '{"enabled": true, "worker": "email-worker"}' ``` ## DNS (Auto-Created) ```dns yourdomain.com. IN MX 1 isaac.mx.cloudflare.net. yourdomain.com. IN MX 2 linda.mx.cloudflare.net. yourdomain.com. IN MX 3 amir.mx.cloudflare.net. yourdomain.com. IN TXT "v=spf1 include:_spf.mx.cloudflare.net ~all" ``` ## Secrets & Variables ```bash # Secrets (encrypted) npx wrangler secret put API_KEY # Variables (plain) # wrangler.jsonc { "vars": { "THRESHOLD": "5.0" } } ``` ```typescript interface Env { API_KEY: string; THRESHOLD: string; } ``` ## TypeScript Setup ```bash npm install --save-dev @cloudflare/workers-types ``` ```json // tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "ES2022", "lib": ["ES2022"], "types": ["@cloudflare/workers-types"], "moduleResolution": "bundler", "strict": true } } ``` ```typescript import type { ForwardableEmailMessage } from "@cloudflare/workers-types"; export default { async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise { await message.forward("dest@example.com"); } } satisfies ExportedHandler; ``` ## Dependencies ```bash npm install postal-mime ``` ```typescript import PostalMime from 'postal-mime'; export default { async email(message, env, ctx) { const parser = new PostalMime(); const email = await parser.parse(await message.raw.arrayBuffer()); console.log(email.subject); await message.forward("inbox@corp.com"); } } satisfies ExportedHandler; ``` ## Multi-Environment ```bash # wrangler.dev.jsonc { "name": "worker-dev", "vars": { "ENV": "dev" } } # wrangler.prod.jsonc { "name": "worker-prod", "vars": { "ENV": "prod" } } npx wrangler deploy --config wrangler.dev.jsonc npx wrangler deploy --config wrangler.prod.jsonc ``` ## CI/CD (GitHub Actions) ```yaml # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - run: npm ci - run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-routing/gotchas.md ================================================ # Gotchas & Troubleshooting ## Critical Pitfalls ### Stream Consumption (MOST COMMON) **Problem:** "stream already consumed" or worker hangs **Cause:** `message.raw` is `ReadableStream` - consume once only **Solution:** ```typescript // ❌ WRONG const email1 = await parser.parse(await message.raw.arrayBuffer()); const email2 = await parser.parse(await message.raw.arrayBuffer()); // FAILS // ✅ CORRECT const raw = await message.raw.arrayBuffer(); const email = await parser.parse(raw); ``` Consume `message.raw` immediately before any async operations. ### Destination Verification **Problem:** Emails not forwarding **Cause:** Destination unverified **Solution:** Add destination, check inbox for verification email, click link. Verify status: `GET /zones/{id}/email/routing/addresses` ### Mail Authentication **Problem:** Legitimate emails rejected **Cause:** Missing SPF/DKIM/DMARC on sender domain **Solution:** Configure sender DNS: ```dns example.com. IN TXT "v=spf1 include:_spf.example.com ~all" selector._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=..." _dmarc.example.com. IN TXT "v=DMARC1; p=quarantine" ``` ### Envelope vs Header **Problem:** Filtering on wrong address **Solution:** ```typescript // Routing/auth: envelope if (message.from === "trusted@example.com") { } // Display: headers const display = message.headers.get("from"); ``` ### SendEmail Limits | Issue | Limit | Solution | |-------|-------|----------| | From domain | Must own | Use Email Routing domain | | Volume | ~100/min Free | Upgrade or throttle | | Attachments | Not supported | Link to R2 | | Type | Transactional | No bulk | ## Common Errors ### CPU Time Exceeded **Cause:** Heavy parsing, large emails **Solution:** ```typescript const size = parseInt(message.headers.get("content-length") || "0") / 1024 / 1024; if (size > 20) { message.setReject("Too large"); return; } ctx.waitUntil(expensiveWork()); await message.forward("dest@example.com"); ``` ### Rule Not Triggering **Causes:** Priority conflict, matcher error, catch-all override **Solution:** Check priority (lower=first), verify exact match, confirm destination verified ### Undefined Property **Cause:** Missing header **Solution:** ```typescript // ❌ WRONG const subj = message.headers.get("subject").toLowerCase(); // ✅ CORRECT const subj = message.headers.get("subject")?.toLowerCase() || ""; ``` ## Limits | Resource | Free | Paid | |----------|------|------| | Email size | 25 MB | 25 MB | | Rules | 200 | 200 | | Destinations | 200 | 200 | | CPU time | 10ms | 50ms | | SendEmail | ~100/min | Higher | ## Debugging ### Local ```bash npx wrangler dev curl -X POST 'http://localhost:8787/__email' \ --header 'content-type: message/rfc822' \ --data 'From: test@example.com To: you@yourdomain.com Subject: Test Body' ``` ### Production ```bash npx wrangler tail ``` ### Pattern ```typescript export default { async email(message, env, ctx) { try { console.log("From:", message.from); await process(message, env); } catch (err) { console.error(err); message.setReject(err.message); } } } satisfies ExportedHandler; ``` ## Auth Troubleshooting ### Check Status ```typescript const auth = message.headers.get("authentication-results") || ""; console.log({ spf: auth.includes("spf=pass"), dkim: auth.includes("dkim=pass"), dmarc: auth.includes("dmarc=pass") }); if (!auth.includes("pass")) { message.setReject("Failed auth"); return; } ``` ### SPF Issues **Causes:** Forwarding breaks SPF, too many lookups (>10), missing includes **Solution:** ```dns ; ✅ Good example.com. IN TXT "v=spf1 include:_spf.google.com ~all" ; ❌ Bad - too many example.com. IN TXT "v=spf1 include:a.com include:b.com ... ~all" ``` ### DMARC Alignment **Cause:** From domain must match SPF/DKIM domain ## Best Practices 1. Consume `message.raw` immediately 2. Verify destinations 3. Handle missing headers (`?.`) 4. Use envelope for routing 5. Check spam scores 6. Test locally first 7. Use `ctx.waitUntil` for background work 8. Size-check early ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-routing/patterns.md ================================================ # Common Patterns ## 1. Allowlist/Blocklist ```typescript // Allowlist const allowed = ["user@example.com", "trusted@corp.com"]; if (!allowed.includes(message.from)) { message.setReject("Not allowed"); return; } await message.forward("inbox@corp.com"); ``` ## 2. Parse Email Body ```typescript import PostalMime from 'postal-mime'; export default { async email(message, env, ctx) { // CRITICAL: Consume stream immediately const raw = await message.raw.arrayBuffer(); const parser = new PostalMime(); const email = await parser.parse(raw); console.log({ subject: email.subject, text: email.text, html: email.html, from: email.from.address, attachments: email.attachments.length }); await message.forward("inbox@corp.com"); } } satisfies ExportedHandler; ``` ## 3. Spam Filter ```typescript const score = parseFloat(message.headers.get("x-cf-spamh-score") || "0"); if (score > 5) { message.setReject("Spam detected"); return; } await message.forward("inbox@corp.com"); ``` ## 4. Archive to R2 ```typescript interface Env { R2: R2Bucket; } export default { async email(message, env, ctx) { const raw = await message.raw.arrayBuffer(); const key = `${new Date().toISOString()}-${message.from}.eml`; await env.R2.put(key, raw, { httpMetadata: { contentType: "message/rfc822" } }); await message.forward("inbox@corp.com"); } } satisfies ExportedHandler; ``` ## 5. Store Metadata in KV ```typescript import PostalMime from 'postal-mime'; interface Env { KV: KVNamespace; } export default { async email(message, env, ctx) { const raw = await message.raw.arrayBuffer(); const parser = new PostalMime(); const email = await parser.parse(raw); const metadata = { from: email.from.address, subject: email.subject, timestamp: new Date().toISOString(), size: raw.byteLength }; await env.KV.put(`email:${Date.now()}`, JSON.stringify(metadata)); await message.forward("inbox@corp.com"); } } satisfies ExportedHandler; ``` ## 6. Subject-Based Routing ```typescript export default { async email(message, env, ctx) { const subject = message.headers.get("subject")?.toLowerCase() || ""; if (subject.includes("[urgent]")) { await message.forward("oncall@corp.com"); } else if (subject.includes("[billing]")) { await message.forward("billing@corp.com"); } else if (subject.includes("[support]")) { await message.forward("support@corp.com"); } else { await message.forward("general@corp.com"); } } } satisfies ExportedHandler; ``` ## 7. Auto-Reply ```typescript interface Env { EMAIL: SendEmail; REPLIED: KVNamespace; } export default { async email(message, env, ctx) { const msgId = message.headers.get("message-id"); if (msgId && await env.REPLIED.get(msgId)) { await message.forward("archive@corp.com"); return; } ctx.waitUntil((async () => { await env.EMAIL.send({ from: "noreply@yourdomain.com", to: message.from, subject: "Re: " + (message.headers.get("subject") || ""), text: "Thank you. We'll respond within 24h." }); if (msgId) await env.REPLIED.put(msgId, "1", { expirationTtl: 604800 }); })()); await message.forward("support@corp.com"); } } satisfies ExportedHandler; ``` ## 8. Extract Attachments ```typescript import PostalMime from 'postal-mime'; interface Env { ATTACHMENTS: R2Bucket; } export default { async email(message, env, ctx) { const parser = new PostalMime(); const email = await parser.parse(await message.raw.arrayBuffer()); for (const att of email.attachments) { const key = `${Date.now()}-${att.filename}`; await env.ATTACHMENTS.put(key, att.content, { httpMetadata: { contentType: att.mimeType } }); } await message.forward("inbox@corp.com"); } } satisfies ExportedHandler; ``` ## 9. Log to D1 ```typescript import PostalMime from 'postal-mime'; interface Env { DB: D1Database; } export default { async email(message, env, ctx) { const parser = new PostalMime(); const email = await parser.parse(await message.raw.arrayBuffer()); ctx.waitUntil( env.DB.prepare("INSERT INTO log (ts, from_addr, subj) VALUES (?, ?, ?)") .bind(new Date().toISOString(), email.from.address, email.subject || "") .run() ); await message.forward("inbox@corp.com"); } } satisfies ExportedHandler; ``` ## 10. Multi-Tenant ```typescript interface Env { TENANTS: KVNamespace; } export default { async email(message, env, ctx) { const subdomain = message.to.split("@")[1].split(".")[0]; const config = await env.TENANTS.get(subdomain, "json") as { forward: string } | null; if (!config) { message.setReject("Unknown tenant"); return; } await message.forward(config.forward); } } satisfies ExportedHandler; ``` ## Summary | Pattern | Use Case | Storage | |---------|----------|---------| | Allowlist | Security | None | | Parse | Body/attachments | None | | Spam Filter | Reduce spam | None | | R2 Archive | Email storage | R2 | | KV Meta | Analytics | KV | | Subject Route | Dept routing | None | | Auto-Reply | Support | KV | | Attachments | Doc mgmt | R2 | | D1 Log | Audit trail | D1 | | Multi-Tenant | SaaS | KV | ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-workers/README.md ================================================ # Cloudflare Email Workers Process incoming emails programmatically using Cloudflare Workers runtime. ## Overview Email Workers enable custom email processing logic at the edge. Build spam filters, auto-responders, ticket systems, notification handlers, and more using the same Workers runtime you use for HTTP requests. **Key capabilities**: - Process inbound emails with full message access - Forward to verified destinations - Send replies with proper threading - Parse MIME content and attachments - Integrate with KV, R2, D1, and external APIs ## Quick Start ### Minimal ES Modules Handler ```typescript export default { async email(message, env, ctx) { // Reject spam if (message.from.includes('spam.com')) { message.setReject('Blocked'); return; } // Forward to inbox await message.forward('inbox@example.com'); } }; ``` ### Core Operations | Operation | Method | Use Case | |-----------|--------|----------| | Forward | `message.forward(to, headers?)` | Route to verified destination | | Reject | `message.setReject(reason)` | Block with SMTP error | | Reply | `message.reply(emailMessage)` | Auto-respond with threading | | Parse | postal-mime library | Extract subject, body, attachments | ## Reading Order For comprehensive understanding, read files in this order: 1. **README.md** (this file) - Overview and quick start 2. **configuration.md** - Setup, deployment, bindings 3. **api.md** - Complete API reference 4. **patterns.md** - Real-world implementation examples 5. **gotchas.md** - Critical pitfalls and debugging ## In This Reference | File | Description | Key Topics | |------|-------------|------------| | [api.md](./api.md) | Complete API reference | ForwardableEmailMessage, SendEmail bindings, reply() method, postal-mime/mimetext APIs | | [configuration.md](./configuration.md) | Setup and configuration | wrangler.jsonc, bindings, deployment, dependencies | | [patterns.md](./patterns.md) | Real-world examples | Allowlists from KV, auto-reply with threading, attachment extraction, webhook notifications | | [gotchas.md](./gotchas.md) | Pitfalls and debugging | Stream consumption, ctx.waitUntil errors, security, limits | ## Architecture ``` Incoming Email → Email Routing → Email Worker ↓ Process + Decide ↓ ┌───────────────┼───────────────┐ ↓ ↓ ↓ Forward Reply Reject ``` **Event flow**: 1. Email arrives at your domain 2. Email Routing matches route (e.g., `support@example.com`) 3. Bound Email Worker receives `ForwardableEmailMessage` 4. Worker processes and takes action (forward/reply/reject) 5. Email delivered or rejected based on worker logic ## Key Concepts ### Envelope vs Headers - **Envelope addresses** (`message.from`, `message.to`): SMTP transport addresses (trusted) - **Header addresses** (parsed from body): Display addresses (can be spoofed) Use envelope addresses for security decisions. ### Single-Use Streams `message.raw` is a ReadableStream that can only be read once. Buffer to ArrayBuffer for multiple uses. ```typescript // Buffer first const buffer = await new Response(message.raw).arrayBuffer(); const email = await PostalMime.parse(buffer); ``` See [gotchas.md](./gotchas.md#readablestream-can-only-be-consumed-once) for details. ### Verified Destinations `forward()` only works with addresses verified in the Cloudflare Email Routing dashboard. Add destinations before deployment. ## Use Cases - **Spam filtering**: Block based on sender, content, or reputation - **Auto-responders**: Send acknowledgment replies with threading - **Ticket creation**: Parse emails and create support tickets - **Email archival**: Store in KV, R2, or D1 - **Notification routing**: Forward to Slack, Discord, or webhooks - **Attachment processing**: Extract files to R2 storage - **Multi-tenant routing**: Route based on recipient subdomain - **Size filtering**: Reject oversized attachments ## Limits | Limit | Value | |-------|-------| | Max message size | 25 MiB | | Max routing rules | 200 | | Max destinations | 200 | | CPU time (free tier) | 10ms | | CPU time (paid tier) | 50ms | See [gotchas.md](./gotchas.md#limits-reference) for complete limits table. ## Prerequisites Before deploying Email Workers: 1. **Enable Email Routing** in Cloudflare dashboard for your domain 2. **Verify destination addresses** for forwarding 3. **Configure DMARC/SPF** for sending domains (required for replies) 4. **Set up wrangler.jsonc** with SendEmail binding See [configuration.md](./configuration.md) for detailed setup. ## Service Worker Syntax (Deprecated) Modern projects should use ES modules format shown above. Service Worker syntax (`addEventListener('email', ...)`) is deprecated but still supported. ## See Also - [Email Routing Documentation](https://developers.cloudflare.com/email-routing/) - [Workers Platform](https://developers.cloudflare.com/workers/) - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) - [postal-mime on npm](https://www.npmjs.com/package/postal-mime) - [mimetext on npm](https://www.npmjs.com/package/mimetext) ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-workers/api.md ================================================ # Email Workers API Reference Complete API reference for Cloudflare Email Workers runtime. ## ForwardableEmailMessage Interface The main interface passed to email handlers. ```typescript interface ForwardableEmailMessage { readonly from: string; // Envelope MAIL FROM (SMTP sender) readonly to: string; // Envelope RCPT TO (SMTP recipient) readonly headers: Headers; // Web-standard Headers object readonly raw: ReadableStream; // Raw MIME message (single-use stream) readonly rawSize: number; // Total message size in bytes setReject(reason: string): void; forward(rcptTo: string, headers?: Headers): Promise; reply(message: EmailMessage): Promise; } ``` ### Properties | Property | Type | Description | |----------|------|-------------| | `from` | string | Envelope sender (SMTP MAIL FROM) - use for security | | `to` | string | Envelope recipient (SMTP RCPT TO) | | `headers` | Headers | Message headers (Subject, Message-ID, etc.) | | `raw` | ReadableStream | Raw MIME message (**single-use**, buffer first) | | `rawSize` | number | Message size in bytes | ### Methods #### setReject(reason: string): void Reject with permanent SMTP 5xx error. Email not delivered, sender may receive bounce. ```typescript if (blockList.includes(message.from)) { message.setReject('Sender blocked'); } ``` #### forward(rcptTo: string, headers?: Headers): Promise Forward to verified destination. Only `X-*` custom headers allowed. ```typescript await message.forward('inbox@example.com'); // With custom headers const h = new Headers(); h.set('X-Processed-By', 'worker'); await message.forward('inbox@example.com', h); ``` #### reply(message: EmailMessage): Promise Send a reply to the original sender (March 2025 feature). ```typescript import { EmailMessage } from 'cloudflare:email'; import { createMimeMessage } from 'mimetext'; const msg = createMimeMessage(); msg.setSender({ name: 'Support', addr: 'support@example.com' }); msg.setRecipient(message.from); msg.setSubject(`Re: ${message.headers.get('Subject')}`); msg.setHeader('In-Reply-To', message.headers.get('Message-ID')); msg.setHeader('References', message.headers.get('References') || ''); msg.addMessage({ contentType: 'text/plain', data: 'Thank you for your message.' }); await message.reply(new EmailMessage( 'support@example.com', message.from, msg.asRaw() )); ``` **Requirements**: - Incoming email needs valid DMARC - Reply once per event, recipient = `message.from` - Sender domain = receiving domain, with DMARC/SPF/DKIM - Max 100 `References` entries - Threading: `In-Reply-To` (original Message-ID), `References`, new `Message-ID` ## EmailMessage Constructor ```typescript import { EmailMessage } from 'cloudflare:email'; new EmailMessage(from: string, to: string, raw: ReadableStream | string) ``` Used for sending emails (replies or via SendEmail binding). Domain must be verified. ## SendEmail Interface ```typescript interface SendEmail { send(message: EmailMessage): Promise; } // Usage await env.EMAIL.send(new EmailMessage(from, to, mimeContent)); ``` ## SendEmail Binding Types ```jsonc { "send_email": [ { "name": "EMAIL" }, // Type 1: Any verified address { "name": "LOGS", "destination_address": "logs@example.com" }, // Type 2: Single dest { "name": "TEAM", "allowed_destination_addresses": ["a@ex.com", "b@ex.com"] }, // Type 3: Dest allowlist { "name": "NOREPLY", "allowed_sender_addresses": ["noreply@ex.com"] } // Type 4: Sender allowlist ] } ``` ## postal-mime Parsed Output postal-mime v2.7.3 parses incoming emails into structured data. ```typescript interface ParsedEmail { headers: Array<{ key: string; value: string }>; from: { name: string; address: string } | null; to: Array<{ name: string; address: string }> | { name: string; address: string } | null; cc: Array<{ name: string; address: string }> | null; bcc: Array<{ name: string; address: string }> | null; subject: string; messageId: string | null; inReplyTo: string | null; references: string | null; date: string | null; html: string | null; text: string | null; attachments: Array<{ filename: string; mimeType: string; disposition: string | null; related: boolean; contentId: string | null; content: Uint8Array; }>; } ``` ### Usage ```typescript import PostalMime from 'postal-mime'; const buffer = await new Response(message.raw).arrayBuffer(); const email = await PostalMime.parse(buffer); console.log(email.subject); console.log(email.from?.address); console.log(email.text); console.log(email.attachments.length); ``` ## mimetext API Quick Reference mimetext v3.0.27 composes outgoing emails. ```typescript import { createMimeMessage } from 'mimetext'; const msg = createMimeMessage(); // Sender msg.setSender({ name: 'John Doe', addr: 'john@example.com' }); // Recipients msg.setRecipient('alice@example.com'); msg.setRecipients(['bob@example.com', 'carol@example.com']); msg.setCc('manager@example.com'); msg.setBcc(['audit@example.com']); // Headers msg.setSubject('Meeting Notes'); msg.setHeader('In-Reply-To', ''); msg.setHeader('References', ' '); msg.setHeader('Message-ID', `<${crypto.randomUUID()}@example.com>`); // Content msg.addMessage({ contentType: 'text/plain', data: 'Plain text content' }); msg.addMessage({ contentType: 'text/html', data: '

HTML content

' }); // Attachments msg.addAttachment({ filename: 'report.pdf', contentType: 'application/pdf', data: pdfBuffer // Uint8Array or base64 string }); // Generate raw MIME const raw = msg.asRaw(); // Returns string ``` ## TypeScript Types ```typescript import { ForwardableEmailMessage, EmailMessage } from 'cloudflare:email'; interface Env { EMAIL: SendEmail; EMAIL_ARCHIVE: KVNamespace; ALLOWED_SENDERS: KVNamespace; } export default { async email( message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext ): Promise { // Fully typed } }; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-workers/configuration.md ================================================ # Email Workers Configuration ## wrangler.jsonc ```jsonc { "name": "email-worker", "main": "src/index.ts", "compatibility_date": "2025-01-27", "send_email": [ { "name": "EMAIL" }, // Unrestricted { "name": "EMAIL_LOGS", "destination_address": "logs@example.com" }, // Single dest { "name": "EMAIL_TEAM", "allowed_destination_addresses": ["a@ex.com", "b@ex.com"] }, { "name": "EMAIL_NOREPLY", "allowed_sender_addresses": ["noreply@ex.com"] } ], "kv_namespaces": [{ "binding": "ARCHIVE", "id": "xxx" }], "r2_buckets": [{ "binding": "ATTACHMENTS", "bucket_name": "email-attachments" }], "vars": { "WEBHOOK_URL": "https://hooks.example.com" } } ``` ## TypeScript Types ```typescript interface Env { EMAIL: SendEmail; ARCHIVE: KVNamespace; ATTACHMENTS: R2Bucket; WEBHOOK_URL: string; } export default { async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) {} }; ``` ## Dependencies ```bash npm install postal-mime mimetext npm install -D @cloudflare/workers-types wrangler typescript ``` Use postal-mime v2.x, mimetext v3.x. ## tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "module": "ES2022", "lib": ["ES2022"], "types": ["@cloudflare/workers-types"], "moduleResolution": "bundler", "strict": true } } ``` ## Local Development ```bash npx wrangler dev # Test receiving curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \ --url-query 'from=sender@example.com' --url-query 'to=recipient@example.com' \ --header 'Content-Type: text/plain' --data-raw 'Subject: Test\n\nHello' ``` Sent emails write to local `.eml` files. ## Deployment Checklist - [ ] Enable Email Routing in dashboard - [ ] Verify destination addresses - [ ] Configure DMARC/SPF/DKIM for sending - [ ] Create KV/R2 resources if needed - [ ] Update wrangler.jsonc with production IDs ```bash npx wrangler deploy npx wrangler deployments list ``` ## Dashboard Setup 1. **Email Routing:** Domain → Email → Enable Email Routing 2. **Verify addresses:** Email → Destination addresses → Add & verify 3. **Bind Worker:** Email → Email Workers → Create route → Select pattern & Worker 4. **DMARC:** Add TXT `_dmarc.domain.com`: `v=DMARC1; p=quarantine;` ## Secrets ```bash npx wrangler secret put API_KEY # Access: env.API_KEY ``` ## Monitoring ```bash npx wrangler tail npx wrangler tail --status error npx wrangler tail --format json ``` ## Troubleshooting | Error | Fix | |-------|-----| | "Binding not found" | Check `send_email` name matches code | | "Invalid destination" | Verify in Email Routing dashboard | | Type errors | Install `@cloudflare/workers-types` | ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-workers/gotchas.md ================================================ # Email Workers Gotchas ## Critical Issues ### ReadableStream Single-Use ```typescript // ❌ WRONG: Stream consumed twice const email = await PostalMime.parse(await new Response(message.raw).arrayBuffer()); const rawText = await new Response(message.raw).text(); // EMPTY! // ✅ CORRECT: Buffer first const buffer = await new Response(message.raw).arrayBuffer(); const email = await PostalMime.parse(buffer); const rawText = new TextDecoder().decode(buffer); ``` ### ctx.waitUntil() Errors Silent ```typescript // ❌ Errors dropped silently ctx.waitUntil(fetch(webhookUrl, { method: 'POST', body: data })); // ✅ Catch and log ctx.waitUntil( fetch(webhookUrl, { method: 'POST', body: data }) .catch(err => env.ERROR_LOG.put(`error:${Date.now()}`, err.message)) ); ``` ## Security ### Envelope vs Header From (Spoofing) ```typescript const envelopeFrom = message.from; // SMTP MAIL FROM (trusted) const headerFrom = (await PostalMime.parse(buffer)).from?.address; // (untrusted) // Use envelope for security decisions ``` ### Input Validation ```typescript if (message.rawSize > 5_000_000) { message.setReject('Too large'); return; } if ((message.headers.get('Subject') || '').length > 1000) { message.setReject('Invalid subject'); return; } ``` ### DMARC for Replies Replies fail silently without DMARC. Verify: `dig TXT _dmarc.example.com` ## Parsing ### Address Parsing ```typescript const email = await PostalMime.parse(buffer); const fromAddress = email.from?.address || 'unknown'; const toAddresses = Array.isArray(email.to) ? email.to.map(t => t.address) : [email.to?.address]; ``` ### Character Encoding Let postal-mime handle decoding - `email.subject`, `email.text`, `email.html` are UTF-8. ## API Behavior ### setReject() vs throw ```typescript // setReject() for SMTP rejection if (blockList.includes(message.from)) { message.setReject('Blocked'); return; } // throw for worker errors if (!env.KV) throw new Error('KV not configured'); ``` ### forward() Only X-* Headers ```typescript headers.set('X-Processed-By', 'worker'); // ✅ Works headers.set('Subject', 'Modified'); // ❌ Dropped ``` ### Reply Requires Verified Domain ```typescript // Use same domain as receiving address const receivingDomain = message.to.split('@')[1]; await message.reply(new EmailMessage(`noreply@${receivingDomain}`, message.from, rawMime)); ``` ## Performance ### CPU Limit ```typescript // Skip parsing large emails if (message.rawSize > 5_000_000) { await message.forward('inbox@example.com'); return; } ``` Monitor: `npx wrangler tail` ## Limits | Limit | Value | |-------|-------| | Max message size | 25 MiB | | Max rules/zone | 200 | | CPU time (free/paid) | 10ms / 50ms | | Reply References | 100 | ## Common Errors | Error | Fix | |-------|-----| | "Address not verified" | Add in Email Routing dashboard | | "Exceeded CPU time" | Use `ctx.waitUntil()` or upgrade | | "Stream is locked" | Buffer `message.raw` first | | Silent reply failure | Check DMARC records | ================================================ FILE: skills/.curated/cloudflare-deploy/references/email-workers/patterns.md ================================================ # Email Workers Patterns ## Parse Email ```typescript import PostalMime from 'postal-mime'; export default { async email(message, env, ctx) { const buffer = await new Response(message.raw).arrayBuffer(); const email = await PostalMime.parse(buffer); console.log(email.from, email.subject, email.text, email.attachments.length); await message.forward('inbox@example.com'); } }; ``` ## Filtering ```typescript // Allowlist from KV const allowList = await env.ALLOWED_SENDERS.get('list', 'json') || []; if (!allowList.includes(message.from)) { message.setReject('Not allowed'); return; } // Size check (avoid parsing large emails) if (message.rawSize > 5_000_000) { await message.forward('inbox@example.com'); // Forward without parsing return; } ``` ## Auto-Reply with Threading ```typescript import { EmailMessage } from 'cloudflare:email'; import { createMimeMessage } from 'mimetext'; const msg = createMimeMessage(); msg.setSender({ addr: 'support@example.com' }); msg.setRecipient(message.from); msg.setSubject(`Re: ${message.headers.get('Subject')}`); msg.setHeader('In-Reply-To', message.headers.get('Message-ID') || ''); msg.addMessage({ contentType: 'text/plain', data: 'Thank you. We will respond.' }); await message.reply(new EmailMessage('support@example.com', message.from, msg.asRaw())); ``` ## Rate-Limited Auto-Reply ```typescript const rateKey = `rate:${message.from}`; if (!await env.RATE_LIMIT.get(rateKey)) { // Send reply... ctx.waitUntil(env.RATE_LIMIT.put(rateKey, '1', { expirationTtl: 3600 })); } ``` ## Subject-Based Routing ```typescript const subject = (message.headers.get('Subject') || '').toLowerCase(); if (subject.includes('billing')) await message.forward('billing@example.com'); else if (subject.includes('support')) await message.forward('support@example.com'); else await message.forward('general@example.com'); ``` ## Multi-Tenant Routing ```typescript // support+tenant123@example.com → tenant123 const tenantId = message.to.split('@')[0].match(/\+(.+)$/)?.[1] || 'default'; const config = await env.TENANT_CONFIG.get(tenantId, 'json'); config?.forwardTo ? await message.forward(config.forwardTo) : message.setReject('Unknown'); ``` ## Archive & Extract Attachments ```typescript // Archive to KV ctx.waitUntil(env.ARCHIVE.put(`email:${Date.now()}`, JSON.stringify({ from: message.from, subject: email.subject }))); // Attachments to R2 for (const att of email.attachments) { ctx.waitUntil(env.R2.put(`${Date.now()}-${att.filename}`, att.content)); } ``` ## Webhook Integration ```typescript ctx.waitUntil( fetch(env.WEBHOOK_URL, { method: 'POST', body: JSON.stringify({ from: message.from, subject: message.headers.get('Subject') }) }).catch(err => console.error(err)) ); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/hyperdrive/README.md ================================================ # Hyperdrive Accelerates database queries from Workers via connection pooling, edge setup, query caching. ## Key Features - **Connection Pooling**: Persistent connections eliminate TCP/TLS/auth handshakes (~7 round-trips) - **Edge Setup**: Connection negotiation at edge, pooling near origin - **Query Caching**: Auto-cache non-mutating queries (default 60s TTL) - **Support**: PostgreSQL, MySQL + compatibles (CockroachDB, Timescale, PlanetScale, Neon, Supabase) ## Architecture ``` Worker → Edge (setup) → Pool (near DB) → Origin ↓ cached reads Cache ``` ## Quick Start ```bash # Create config npx wrangler hyperdrive create my-db \ --connection-string="postgres://user:pass@host:5432/db" # wrangler.jsonc { "compatibility_flags": ["nodejs_compat"], "hyperdrive": [{"binding": "HYPERDRIVE", "id": ""}] } ``` ```typescript import { Client } from "pg"; export default { async fetch(req: Request, env: Env): Promise { const client = new Client({ connectionString: env.HYPERDRIVE.connectionString, }); await client.connect(); const result = await client.query("SELECT * FROM users WHERE id = $1", [123]); await client.end(); return Response.json(result.rows); }, }; ``` ## When to Use ✅ Global access to single-region DBs, high read ratios, popular queries, connection-heavy loads ❌ Write-heavy, real-time data (<1s), single-region apps close to DB **💡 Pair with Smart Placement** for Workers making multiple queries - executes near DB to minimize latency. ## Driver Choice | Driver | Use When | Notes | |--------|----------|-------| | **pg** (recommended) | General use, TypeScript, ecosystem compatibility | Stable, widely used, works with most ORMs | | **postgres.js** | Advanced features, template literals, streaming | Lighter than pg, `prepare: true` is default | | **mysql2** | MySQL/MariaDB/PlanetScale | MySQL only, less mature support | ## Reading Order | New to Hyperdrive | Implementing | Troubleshooting | |-------------------|--------------|-----------------| | 1. README (this) | 1. [configuration.md](./configuration.md) | 1. [gotchas.md](./gotchas.md) | | 2. [configuration.md](./configuration.md) | 2. [api.md](./api.md) | 2. [patterns.md](./patterns.md) | | 3. [api.md](./api.md) | 3. [patterns.md](./patterns.md) | 3. [api.md](./api.md) | ## In This Reference - [configuration.md](./configuration.md) - Setup, wrangler config, Smart Placement - [api.md](./api.md) - Binding APIs, query patterns, driver usage - [patterns.md](./patterns.md) - Use cases, ORMs, multi-query optimization - [gotchas.md](./gotchas.md) - Limits, troubleshooting, connection management ## See Also - [smart-placement](../smart-placement/) - Optimize multi-query Workers near databases - [d1](../d1/) - Serverless SQLite alternative for edge-native apps - [workers](../workers/) - Worker runtime with database bindings ================================================ FILE: skills/.curated/cloudflare-deploy/references/hyperdrive/api.md ================================================ # API Reference See [README.md](./README.md) for overview, [configuration.md](./configuration.md) for setup. ## Binding Interface ```typescript interface Hyperdrive { connectionString: string; // PostgreSQL // MySQL properties: host: string; port: number; user: string; password: string; database: string; } interface Env { HYPERDRIVE: Hyperdrive; } ``` **Generate types:** `npx wrangler types` (auto-creates worker-configuration.d.ts from wrangler.jsonc) ## PostgreSQL (node-postgres) - RECOMMENDED ```typescript import { Client } from "pg"; // pg@^8.17.2 export default { async fetch(req: Request, env: Env): Promise { const client = new Client({connectionString: env.HYPERDRIVE.connectionString}); try { await client.connect(); const result = await client.query("SELECT * FROM users WHERE id = $1", [123]); return Response.json(result.rows); } finally { await client.end(); } }, }; ``` **⚠️ Workers connection limit: 6 per Worker invocation** - use connection pooling wisely. ## PostgreSQL (postgres.js) ```typescript import postgres from "postgres"; // postgres@^3.4.8 const sql = postgres(env.HYPERDRIVE.connectionString, { max: 5, // Limit per Worker (Workers max: 6) prepare: true, // Enabled by default, required for caching fetch_types: false, // Reduce latency if not using arrays }); const users = await sql`SELECT * FROM users WHERE active = ${true} LIMIT 10`; ``` **⚠️ `prepare: true` is enabled by default and required for Hyperdrive caching.** Setting to `false` disables prepared statements + cache. ## MySQL (mysql2) ```typescript import { createConnection } from "mysql2/promise"; // mysql2@^3.16.2 const conn = await createConnection({ host: env.HYPERDRIVE.host, user: env.HYPERDRIVE.user, password: env.HYPERDRIVE.password, database: env.HYPERDRIVE.database, port: env.HYPERDRIVE.port, disableEval: true, // ⚠️ REQUIRED for Workers }); const [results] = await conn.query("SELECT * FROM users WHERE active = ? LIMIT ?", [true, 10]); ctx.waitUntil(conn.end()); ``` **⚠️ MySQL support is less mature than PostgreSQL** - expect fewer optimizations and potential edge cases. ## Query Caching **Cacheable:** ```sql SELECT * FROM posts WHERE published = true; SELECT COUNT(*) FROM users; ``` **NOT cacheable:** ```sql -- Writes INSERT/UPDATE/DELETE -- Volatile functions SELECT NOW(); SELECT random(); SELECT LASTVAL(); -- PostgreSQL SELECT UUID(); -- MySQL ``` **Cache config:** - Default: `max_age=60s`, `swr=15s` - Max `max_age`: 3600s - Disable: `--caching-disabled=true` **Multiple configs pattern:** ```typescript // Reads: cached const sqlCached = postgres(env.HYPERDRIVE_CACHED.connectionString); const posts = await sqlCached`SELECT * FROM posts ORDER BY views DESC LIMIT 10`; // Writes/time-sensitive: no cache const sqlNoCache = postgres(env.HYPERDRIVE_NO_CACHE.connectionString); const orders = await sqlNoCache`SELECT * FROM orders WHERE created_at > NOW() - INTERVAL 5 MINUTE`; ``` ## ORMs **Drizzle:** ```typescript import { drizzle } from "drizzle-orm/postgres-js"; // drizzle-orm@^0.45.1 import postgres from "postgres"; const client = postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true}); const db = drizzle(client); const users = await db.select().from(users).where(eq(users.active, true)).limit(10); ``` **Kysely:** ```typescript import { Kysely, PostgresDialect } from "kysely"; // kysely@^0.27+ import postgres from "postgres"; const db = new Kysely({ dialect: new PostgresDialect({ postgres: postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true}), }), }); const users = await db.selectFrom("users").selectAll().where("active", "=", true).execute(); ``` See [patterns.md](./patterns.md) for use cases, [gotchas.md](./gotchas.md) for limits. ================================================ FILE: skills/.curated/cloudflare-deploy/references/hyperdrive/configuration.md ================================================ # Configuration See [README.md](./README.md) for overview. ## Create Config **PostgreSQL:** ```bash # Basic npx wrangler hyperdrive create my-db \ --connection-string="postgres://user:pass@host:5432/db" # Custom cache npx wrangler hyperdrive create my-db \ --connection-string="postgres://..." \ --max-age=120 --swr=30 # No cache npx wrangler hyperdrive create my-db \ --connection-string="postgres://..." \ --caching-disabled=true ``` **MySQL:** ```bash npx wrangler hyperdrive create my-db \ --connection-string="mysql://user:pass@host:3306/db" ``` ## wrangler.jsonc ```jsonc { "compatibility_date": "2025-01-01", // Use latest for new projects "compatibility_flags": ["nodejs_compat"], "hyperdrive": [ { "binding": "HYPERDRIVE", "id": "", "localConnectionString": "postgres://user:pass@localhost:5432/dev" } ] } ``` **Generate TypeScript types:** Run `npx wrangler types` to auto-generate `worker-configuration.d.ts` from your wrangler.jsonc. **Multiple configs:** ```jsonc { "hyperdrive": [ {"binding": "HYPERDRIVE_CACHED", "id": ""}, {"binding": "HYPERDRIVE_NO_CACHE", "id": ""} ] } ``` ## Management ```bash npx wrangler hyperdrive list npx wrangler hyperdrive get npx wrangler hyperdrive update --max-age=180 npx wrangler hyperdrive delete ``` ## Config Options Hyperdrive create/update CLI flags: | Option | Default | Notes | |--------|---------|-------| | `--caching-disabled` | `false` | Disable caching | | `--max-age` | `60` | Cache TTL (max 3600s) | | `--swr` | `15` | Stale-while-revalidate | | `--origin-connection-limit` | 20/100 | Free/paid | | `--access-client-id` | - | Tunnel auth | | `--access-client-secret` | - | Tunnel auth | | `--sslmode` | `require` | PostgreSQL only | ## Smart Placement Integration For Workers making **multiple queries** per request, enable Smart Placement to execute near your database: ```jsonc { "compatibility_date": "2025-01-01", "compatibility_flags": ["nodejs_compat"], "placement": { "mode": "smart" }, "hyperdrive": [ { "binding": "HYPERDRIVE", "id": "" } ] } ``` **Benefits:** Multi-query Workers run closer to DB, reducing round-trip latency. See [patterns.md](./patterns.md) for examples. ## Private DB via Tunnel ``` Worker → Hyperdrive → Access → Tunnel → Private Network → DB ``` **Setup:** ```bash # 1. Create tunnel cloudflared tunnel create my-db-tunnel # 2. Configure hostname in Zero Trust dashboard # Domain: db-tunnel.example.com # Service: TCP -> localhost:5432 # 3. Create service token (Zero Trust > Service Auth) # Save Client ID/Secret # 4. Create Access app (db-tunnel.example.com) # Policy: Service Auth token from step 3 # 5. Create Hyperdrive npx wrangler hyperdrive create my-private-db \ --host=db-tunnel.example.com \ --user=dbuser --password=dbpass --database=prod \ --access-client-id= --access-client-secret= ``` **⚠️ Don't specify `--port` with Tunnel** - port configured in tunnel service settings. ## Local Dev **Option 1: Local (RECOMMENDED):** ```bash # Env var (takes precedence) export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://user:pass@localhost:5432/dev" npx wrangler dev # wrangler.jsonc {"hyperdrive": [{"binding": "HYPERDRIVE", "localConnectionString": "postgres://..."}]} ``` **Remote DB locally:** ```bash # PostgreSQL export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://user:pass@remote:5432/db?sslmode=require" # MySQL export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="mysql://user:pass@remote:3306/db?sslMode=REQUIRED" ``` **Option 2: Remote execution:** ```bash npx wrangler dev --remote # Uses deployed config, affects production ``` See [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md). ================================================ FILE: skills/.curated/cloudflare-deploy/references/hyperdrive/gotchas.md ================================================ # Gotchas See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md). ## Common Errors ### "Too many open connections" / "Connection limit exceeded" **Cause:** Workers have a hard limit of **6 concurrent connections per invocation** **Solution:** Set `max: 5` in driver config, reuse connections, ensure proper cleanup with `client.end()` or `ctx.waitUntil(conn.end())` ### "Failed to acquire a connection (Pool exhausted)" **Cause:** All connections in pool are in use, often due to long-running transactions **Solution:** Reduce transaction duration, avoid queries >60s, don't hold connections during external calls, or upgrade to paid plan for more connections ### "connection_refused" **Cause:** Database refusing connections due to firewall, connection limits, or service down **Solution:** Check firewall allows Cloudflare IPs, verify DB listening on port, confirm service running, and validate credentials ### "Query timeout (deadline exceeded)" **Cause:** Query execution exceeding 60s timeout limit **Solution:** Optimize with indexes, reduce dataset with LIMIT, break into smaller queries, or use async processing ### "password authentication failed" **Cause:** Invalid credentials in Hyperdrive configuration **Solution:** Check username and password in Hyperdrive config match database credentials ### "SSL/TLS connection error" **Cause:** SSL/TLS configuration mismatch between Hyperdrive and database **Solution:** Add `sslmode=require` (Postgres) or `sslMode=REQUIRED` (MySQL), upload CA cert if self-signed, verify DB has SSL enabled, and check cert expiry ### "Queries not being cached" **Cause:** Query is mutating (INSERT/UPDATE/DELETE), contains volatile functions (NOW(), RANDOM()), or caching disabled **Solution:** Verify query is non-mutating SELECT, avoid volatile functions, confirm caching enabled, use `wrangler dev --remote` to test, and set `prepare=true` for postgres.js ### "Slow multi-query Workers despite Hyperdrive" **Cause:** Worker executing at edge, each query round-trips to DB region **Solution:** Enable Smart Placement (`"placement": {"mode": "smart"}` in wrangler.jsonc) to execute Worker near DB. See [patterns.md](./patterns.md) Multi-Query pattern. ### "Local database connection failed" **Cause:** `localConnectionString` incorrect or database not running **Solution:** Verify `localConnectionString` correct, check DB running, confirm env var name matches binding, and test with psql/mysql client ### "Environment variable not working" **Cause:** Environment variable format incorrect or not exported **Solution:** Use format `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_`, ensure binding matches wrangler.jsonc, export variable in shell, and restart wrangler dev ## Limits | Limit | Free | Paid | Notes | |-------|------|------|-------| | Max configs | 10 | 25 | Hyperdrive configurations per account | | Worker connections | 6 | 6 | Max concurrent connections per Worker invocation | | Username/DB name | 63 bytes | 63 bytes | Maximum length | | Connection timeout | 15s | 15s | Time to establish connection | | Idle timeout | 10 min | 10 min | Connection idle timeout | | Max origin connections | ~20 | ~100 | Connections to origin database | | Query duration max | 60s | 60s | Queries >60s terminated | | Cached response max | 50 MB | 50 MB | Responses >50MB returned but not cached | ## Resources - [Docs](https://developers.cloudflare.com/hyperdrive/) - [Getting Started](https://developers.cloudflare.com/hyperdrive/get-started/) - [Wrangler Reference](https://developers.cloudflare.com/hyperdrive/reference/wrangler-commands/) - [Supported DBs](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/) - [Discord #hyperdrive](https://discord.cloudflare.com) - [Limit Increase Form](https://forms.gle/ukpeZVLWLnKeixDu7) ================================================ FILE: skills/.curated/cloudflare-deploy/references/hyperdrive/patterns.md ================================================ # Patterns See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md). ## High-Traffic Read-Heavy ```typescript const sql = postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true}); // Cacheable: popular content const posts = await sql`SELECT * FROM posts WHERE published = true ORDER BY views DESC LIMIT 20`; // Cacheable: user profiles const [user] = await sql`SELECT id, username, bio FROM users WHERE id = ${userId}`; ``` **Benefits:** Trending/profiles cached (60s), connection pooling handles spikes. ## Mixed Read/Write ```typescript interface Env { HYPERDRIVE_CACHED: Hyperdrive; // max_age=120 HYPERDRIVE_REALTIME: Hyperdrive; // caching disabled } // Reads: cached if (req.method === "GET") { const sql = postgres(env.HYPERDRIVE_CACHED.connectionString, {prepare: true}); const products = await sql`SELECT * FROM products WHERE category = ${cat}`; } // Writes: no cache (immediate consistency) if (req.method === "POST") { const sql = postgres(env.HYPERDRIVE_REALTIME.connectionString, {prepare: true}); await sql`INSERT INTO orders ${sql(data)}`; } ``` ## Analytics Dashboard ```typescript const client = new Client({connectionString: env.HYPERDRIVE.connectionString}); await client.connect(); // Aggregate queries cached (use fixed timestamps for caching) const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); const dailyStats = await client.query(` SELECT DATE(created_at) as date, COUNT(*) as orders, SUM(amount) as revenue FROM orders WHERE created_at >= $1 GROUP BY DATE(created_at) ORDER BY date DESC `, [thirtyDaysAgo]); const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); const topProducts = await client.query(` SELECT p.name, COUNT(oi.id) as count, SUM(oi.quantity * oi.price) as revenue FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.created_at >= $1 GROUP BY p.id, p.name ORDER BY revenue DESC LIMIT 10 `, [sevenDaysAgo]); ``` **Benefits:** Expensive aggregations cached (avoid NOW() for cacheability), dashboard instant, reduced DB load. ## Multi-Tenant ```typescript const tenantId = req.headers.get("X-Tenant-ID"); const sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true}); // Tenant-scoped queries cached separately const docs = await sql` SELECT * FROM documents WHERE tenant_id = ${tenantId} AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 50 `; ``` **Benefits:** Per-tenant caching, shared connection pool, protects DB from multi-tenant load. ## Geographically Distributed ```typescript // Worker runs at edge nearest user // Connection setup at edge (fast), pooling near DB (efficient) const sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true}); const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`; return Response.json({ user, serverRegion: req.cf?.colo, // Edge location }); ``` **Benefits:** Edge setup + DB pooling = global → single-region DB without replication. ## Multi-Query + Smart Placement For Workers making **multiple queries** per request, enable Smart Placement to execute near DB: ```jsonc // wrangler.jsonc { "placement": {"mode": "smart"}, "hyperdrive": [{"binding": "HYPERDRIVE", "id": ""}] } ``` ```typescript const sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true}); // Multiple queries benefit from Smart Placement const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`; const orders = await sql`SELECT * FROM orders WHERE user_id = ${userId} ORDER BY created_at DESC LIMIT 10`; const stats = await sql`SELECT COUNT(*) as total, SUM(amount) as spent FROM orders WHERE user_id = ${userId}`; return Response.json({user, orders, stats}); ``` **Benefits:** Worker executes near DB → reduces latency for each query. Without Smart Placement, each query round-trips from edge. ## Connection Pooling Operates in **transaction mode**: connection acquired per transaction, `RESET` on return. **SET statements:** ```typescript // ✅ Within transaction await client.query("BEGIN"); await client.query("SET work_mem = '256MB'"); await client.query("SELECT * FROM large_table"); // Uses SET await client.query("COMMIT"); // RESET after // ✅ Single statement await client.query("SET work_mem = '256MB'; SELECT * FROM large_table"); // ❌ Across queries (may get different connection) await client.query("SET work_mem = '256MB'"); await client.query("SELECT * FROM large_table"); // SET not applied ``` **Best practices:** ```typescript // ❌ Long transactions block pooling await client.query("BEGIN"); await processThousands(); // Connection held entire time await client.query("COMMIT"); // ✅ Short transactions await client.query("BEGIN"); await client.query("UPDATE users SET status = $1 WHERE id = $2", [status, id]); await client.query("COMMIT"); // ✅ SET LOCAL within transaction await client.query("BEGIN"); await client.query("SET LOCAL work_mem = '256MB'"); await client.query("SELECT * FROM large_table"); await client.query("COMMIT"); ``` ## Performance Tips **Enable prepared statements (required for caching):** ```typescript const sql = postgres(connectionString, {prepare: true}); // Default, enables caching ``` **Optimize connection settings:** ```typescript const sql = postgres(connectionString, { max: 5, // Stay under Workers' 6 connection limit fetch_types: false, // Reduce latency if not using arrays idle_timeout: 60, // Match Worker lifetime }); ``` **Write cache-friendly queries:** ```typescript // ✅ Cacheable (deterministic) await sql`SELECT * FROM products WHERE category = 'electronics' LIMIT 10`; // ❌ Not cacheable (volatile NOW()) await sql`SELECT * FROM logs WHERE created_at > NOW()`; // ✅ Cacheable (parameterized timestamp) const ts = Date.now(); await sql`SELECT * FROM logs WHERE created_at > ${ts}`; ``` See [gotchas.md](./gotchas.md) for limits, troubleshooting. ================================================ FILE: skills/.curated/cloudflare-deploy/references/images/README.md ================================================ # Cloudflare Images Skill Reference **Cloudflare Images** is an end-to-end image management solution providing storage, transformation, optimization, and delivery at scale via Cloudflare's global network. ## Quick Decision Tree **Need to:** - **Transform in Worker?** → [api.md](api.md#workers-binding-api-2026-primary-method) (Workers Binding API) - **Upload from Worker?** → [api.md](api.md#upload-from-worker) (REST API) - **Upload from client?** → [patterns.md](patterns.md#upload-from-client-direct-creator-upload) (Direct Creator Upload) - **Set up variants?** → [configuration.md](configuration.md#variants-configuration) - **Serve responsive images?** → [patterns.md](patterns.md#responsive-images) - **Add watermarks?** → [patterns.md](patterns.md#watermarking) - **Fix errors?** → [gotchas.md](gotchas.md#common-errors) ## Reading Order **For building image upload/transform feature:** 1. [configuration.md](configuration.md) - Setup Workers binding 2. [api.md](api.md#workers-binding-api-2026-primary-method) - Learn transform API 3. [patterns.md](patterns.md#upload-from-client-direct-creator-upload) - Direct upload pattern 4. [gotchas.md](gotchas.md) - Check limits and errors **For URL-based transforms:** 1. [configuration.md](configuration.md#variants-configuration) - Create variants 2. [api.md](api.md#url-transform-api) - URL syntax 3. [patterns.md](patterns.md#responsive-images) - Responsive patterns **For troubleshooting:** 1. [gotchas.md](gotchas.md#common-errors) - Error messages 2. [gotchas.md](gotchas.md#limits) - Size/format limits ## Core Methods | Method | Use Case | Location | |--------|----------|----------| | `env.IMAGES.input().transform()` | Transform in Worker | [api.md:11](api.md) | | REST API `/images/v1` | Upload images | [api.md:57](api.md) | | Direct Creator Upload | Client-side upload | [api.md:127](api.md) | | URL transforms | Static image delivery | [api.md:112](api.md) | ## In This Reference - **[api.md](api.md)** - Complete API: Workers binding, REST endpoints, URL transforms - **[configuration.md](configuration.md)** - Setup: wrangler.toml, variants, auth, signed URLs - **[patterns.md](patterns.md)** - Patterns: responsive images, watermarks, format negotiation, caching - **[gotchas.md](gotchas.md)** - Troubleshooting: limits, errors, best practices ## Key Features - **Automatic Optimization** - AVIF/WebP format negotiation - **On-the-fly Transforms** - Resize, crop, blur, sharpen via URL or API - **Workers Binding** - Transform images in Workers (2026 primary method) - **Direct Upload** - Secure client-side uploads without backend proxy - **Global Delivery** - Cached at 300+ Cloudflare data centers - **Watermarking** - Overlay images programmatically ## See Also - [Official Docs](https://developers.cloudflare.com/images/) - [Workers Examples](https://developers.cloudflare.com/images/tutorials/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/images/api.md ================================================ # API Reference ## Workers Binding API ```toml # wrangler.toml [images] binding = "IMAGES" ``` ### Transform Images ```typescript const imageResponse = await env.IMAGES .input(fileBuffer) .transform({ width: 800, height: 600, fit: "cover", quality: 85, format: "avif" }) .output(); return imageResponse.response(); ``` ### Transform Options ```typescript interface TransformOptions { width?: number; height?: number; fit?: "scale-down" | "contain" | "cover" | "crop" | "pad"; quality?: number; // 1-100 format?: "avif" | "webp" | "jpeg" | "png"; dpr?: number; // 1-3 gravity?: "auto" | "left" | "right" | "top" | "bottom" | "face" | string; sharpen?: number; // 0-10 blur?: number; // 1-250 rotate?: 90 | 180 | 270; background?: string; // CSS color for pad metadata?: "none" | "copyright" | "keep"; brightness?: number; contrast?: number; gamma?: number; // 0-2 } ``` ### Draw/Watermark ```typescript await env.IMAGES.input(baseImage) .draw(env.IMAGES.input(watermark).transform({ width: 100 }), { top: 10, left: 10, opacity: 0.8 }) .output(); ``` ## REST API ### Upload Image ```bash curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \ -H "Authorization: Bearer {token}" -F file=@image.jpg -F metadata='{"key":"value"}' ``` ### Other Operations ```bash GET /accounts/{account_id}/images/v1/{image_id} # Get details DELETE /accounts/{account_id}/images/v1/{image_id} # Delete GET /accounts/{account_id}/images/v1?page=1 # List ``` ## URL Transform API ``` https://imagedelivery.net/{hash}/{id}/width=800,height=600,fit=cover,format=avif ``` **Params:** `w=`, `h=`, `fit=`, `q=`, `f=`, `dpr=`, `gravity=`, `sharpen=`, `blur=`, `rotate=`, `background=`, `metadata=` ## Direct Creator Upload ```typescript // 1. Get upload URL (backend) const { result } = await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v2/direct_upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ requireSignedURLs: false }) } ).then(r => r.json()); // 2. Client uploads to result.uploadURL const formData = new FormData(); formData.append('file', file); await fetch(result.uploadURL, { method: 'POST', body: formData }); ``` ## Error Codes | Code | Message | Solution | |------|---------|----------| | 5400 | Invalid format | Use JPEG, PNG, GIF, WebP | | 5401 | Too large | Max 100MB | | 5403 | Invalid transform | Check params | | 9413 | Rate limit | Implement backoff | ================================================ FILE: skills/.curated/cloudflare-deploy/references/images/configuration.md ================================================ # Configuration ## Wrangler Integration ### Workers Binding Setup Add to `wrangler.toml`: ```toml name = "my-image-worker" main = "src/index.ts" compatibility_date = "2024-01-01" [images] binding = "IMAGES" ``` Access in Worker: ```typescript interface Env { IMAGES: ImageBinding; } export default { async fetch(request: Request, env: Env): Promise { return await env.IMAGES .input(imageBuffer) .transform({ width: 800 }) .output() .response(); } }; ``` ### Upload via Script Wrangler doesn't have built-in Images commands, use REST API: ```typescript // scripts/upload-image.ts import fs from 'fs'; import FormData from 'form-data'; async function uploadImage(filePath: string) { const accountId = process.env.CLOUDFLARE_ACCOUNT_ID!; const apiToken = process.env.CLOUDFLARE_API_TOKEN!; const formData = new FormData(); formData.append('file', fs.createReadStream(filePath)); const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, }, body: formData, } ); const result = await response.json(); console.log('Uploaded:', result); } uploadImage('./photo.jpg'); ``` ### Environment Variables Store account hash for URL construction: ```toml [vars] IMAGES_ACCOUNT_HASH = "your-account-hash" ACCOUNT_ID = "your-account-id" ``` Access in Worker: ```typescript const imageUrl = `https://imagedelivery.net/${env.IMAGES_ACCOUNT_HASH}/${imageId}/public`; ``` ## Variants Configuration Variants are named presets for transformations. ### Create Variant (Dashboard) 1. Navigate to Images → Variants 2. Click "Create Variant" 3. Set name (e.g., `thumbnail`) 4. Configure: `width=200,height=200,fit=cover` ### Create Variant (API) ```bash curl -X POST \ https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1/variants \ -H "Authorization: Bearer {api_token}" \ -H "Content-Type: application/json" \ -d '{ "id": "thumbnail", "options": { "width": 200, "height": 200, "fit": "cover" }, "neverRequireSignedURLs": true }' ``` ### Use Variant ``` https://imagedelivery.net/{account_hash}/{image_id}/thumbnail ``` ### Common Variant Presets ```json { "thumbnail": { "width": 200, "height": 200, "fit": "cover" }, "avatar": { "width": 128, "height": 128, "fit": "cover", "gravity": "face" }, "hero": { "width": 1920, "height": 1080, "fit": "cover", "quality": 90 }, "mobile": { "width": 640, "fit": "scale-down", "quality": 80, "format": "avif" } } ``` ## Authentication ### API Token (Recommended) Generate at: Dashboard → My Profile → API Tokens Required permissions: - Account → Cloudflare Images → Edit ```bash curl -H "Authorization: Bearer {api_token}" \ https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 ``` ### API Key (Legacy) ```bash curl -H "X-Auth-Email: {email}" \ -H "X-Auth-Key: {api_key}" \ https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 ``` ## Signed URLs For private images, enable signed URLs: ```bash # Upload with signed URLs required curl -X POST \ https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \ -H "Authorization: Bearer {api_token}" \ -F file=@private.jpg \ -F requireSignedURLs=true ``` Generate signed URL: ```typescript import { createHmac } from 'crypto'; function signUrl(imageId: string, variant: string, expiry: number, key: string): string { const path = `/${imageId}/${variant}`; const toSign = `${path}${expiry}`; const signature = createHmac('sha256', key) .update(toSign) .digest('hex'); return `https://imagedelivery.net/{hash}${path}?exp=${expiry}&sig=${signature}`; } // Sign URL valid for 1 hour const signedUrl = signUrl('image-id', 'public', Date.now() + 3600, env.SIGNING_KEY); ``` ## Local Development ```bash npx wrangler dev --remote ``` Must use `--remote` for Images binding access. ================================================ FILE: skills/.curated/cloudflare-deploy/references/images/gotchas.md ================================================ # Gotchas & Best Practices ## Fit Modes | Mode | Best For | Behavior | |------|----------|----------| | `cover` | Hero images, thumbnails | Fills space, crops excess | | `contain` | Product images, artwork | Preserves full image, may add padding | | `scale-down` | User uploads | Never enlarges | | `crop` | Precise crops | Uses gravity | | `pad` | Fixed aspect ratio | Adds background | ## Format Selection ```typescript format: 'auto' // Recommended - negotiates best format ``` **Support:** AVIF (Chrome 85+, Firefox 93+, Safari 16.4+), WebP (Chrome 23+, Firefox 65+, Safari 14+) ## Quality Settings | Use Case | Quality | |----------|---------| | Thumbnails | 75-80 | | Standard | 85 (default) | | High-quality | 90-95 | ## Common Errors ### 5403: "Image transformation failed" - Verify `width`/`height` ≤ 12000 - Check `quality` 1-100, `dpr` 1-3 - Don't combine incompatible options ### 9413: "Rate limit exceeded" Implement caching and exponential backoff: ```typescript for (let i = 0; i < 3; i++) { try { return await env.IMAGES.input(buffer).transform({...}).output(); } catch { await new Promise(r => setTimeout(r, 2 ** i * 1000)); } } ``` ### 5401: "Image too large" Pre-process images before upload (max 100MB, 12000×12000px) ### 5400: "Invalid image format" Supported: JPEG, PNG, GIF, WebP, AVIF, SVG ### 401/403: "Unauthorized" Verify API token has `Cloudflare Images → Edit` permission ## Limits | Resource | Limit | |----------|-------| | Max input size | 100MB | | Max dimensions | 12000×12000px | | Quality range | 1-100 | | DPR range | 1-3 | | API rate limit | ~1200 req/min | ## AVIF Gotchas - **Slower encoding**: First request may have higher latency - **Browser detection**: ```typescript const format = /image\/avif/.test(request.headers.get('Accept') || '') ? 'avif' : 'webp'; ``` ## Anti-Patterns ```typescript // ❌ No caching - transforms every request return env.IMAGES.input(buffer).transform({...}).output().response(); // ❌ cover without both dimensions transform({ width: 800, fit: 'cover' }) // ✅ Always set both for cover transform({ width: 800, height: 600, fit: 'cover' }) // ❌ Exposes API token to client // ✅ Use Direct Creator Upload (patterns.md) ``` ## Debugging ```typescript // Check response headers console.log('Content-Type:', response.headers.get('Content-Type')); // Test with curl // curl -I "https://imagedelivery.net/{hash}/{id}/width=800,format=avif" // Monitor logs // npx wrangler tail ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/images/patterns.md ================================================ # Common Patterns ## URL Transform Options ``` width= height= fit=scale-down|contain|cover|crop|pad quality=85 format=auto|webp|avif|jpeg|png dpr=2 gravity=auto|face|left|right|top|bottom sharpen=2 blur=10 rotate=90|180|270 background=white metadata=none|copyright|keep ``` ## Responsive Images (srcset) ```html ``` ## Format Negotiation ```typescript async fetch(request: Request, env: Env): Promise { const accept = request.headers.get('Accept') || ''; const format = /image\/avif/.test(accept) ? 'avif' : /image\/webp/.test(accept) ? 'webp' : 'jpeg'; return env.IMAGES.input(buffer).transform({ format, quality: 85 }).output().response(); } ``` ## Direct Creator Upload ```typescript // Backend: Generate upload URL const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/images/v2/direct_upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.API_TOKEN}` }, body: JSON.stringify({ requireSignedURLs: false, metadata: { userId } }) } ); // Frontend: Upload to returned uploadURL const formData = new FormData(); formData.append('file', file); await fetch(result.uploadURL, { method: 'POST', body: formData }); // Use: https://imagedelivery.net/{hash}/${result.id}/public ``` ## Transform & Store to R2 ```typescript async fetch(request: Request, env: Env): Promise { const file = (await request.formData()).get('image') as File; const transformed = await env.IMAGES .input(await file.arrayBuffer()) .transform({ width: 800, format: 'avif', quality: 80 }) .output(); await env.R2.put(`images/${Date.now()}.avif`, transformed.response().body); return Response.json({ success: true }); } ``` ## Watermarking ```typescript const watermark = await env.ASSETS.fetch(new URL('/watermark.png', request.url)); const result = await env.IMAGES .input(await image.arrayBuffer()) .draw(env.IMAGES.input(watermark.body).transform({ width: 100 }), { bottom: 20, right: 20, opacity: 0.7 }) .transform({ format: 'avif' }) .output(); return result.response(); ``` ## Device-Based Transforms ```typescript const ua = request.headers.get('User-Agent') || ''; const isMobile = /Mobile|Android|iPhone/i.test(ua); return env.IMAGES.input(buffer) .transform({ width: isMobile ? 400 : 1200, quality: isMobile ? 75 : 85, format: 'avif' }) .output().response(); ``` ## Caching Strategy ```typescript async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const cache = caches.default; let response = await cache.match(request); if (!response) { response = await env.IMAGES.input(buffer).transform({ width: 800, format: 'avif' }).output().response(); response = new Response(response.body, { headers: { ...response.headers, 'Cache-Control': 'public, max-age=86400' } }); ctx.waitUntil(cache.put(request, response.clone())); } return response; } ``` ## Batch Processing ```typescript const results = await Promise.all(images.map(buffer => env.IMAGES.input(buffer).transform({ width: 800, fit: 'cover', format: 'avif' }).output() )); ``` ## Error Handling ```typescript try { return (await env.IMAGES.input(buffer).transform({ width: 800 }).output()).response(); } catch (error) { console.error('Transform failed:', error); return new Response('Image processing failed', { status: 500 }); } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/kv/README.md ================================================ # Cloudflare Workers KV Globally-distributed, eventually-consistent key-value store optimized for high read volume and low latency. ## Overview KV provides: - Eventual consistency (60s global propagation) - Read-optimized performance - 25 MiB value limit per key - Auto-replication to Cloudflare edge - Metadata support (1024 bytes) **Use cases:** Config storage, user sessions, feature flags, caching, A/B testing ## When to Use KV | Need | Recommendation | |------|----------------| | Strong consistency | → [Durable Objects](../durable-objects/) | | SQL queries | → [D1](../d1/) | | Object storage (files) | → [R2](../r2/) | | High read, low write volume | → KV ✅ | | Sub-10ms global reads | → KV ✅ | **Quick comparison:** | Feature | KV | D1 | Durable Objects | |---------|----|----|-----------------| | Consistency | Eventual | Strong | Strong | | Read latency | <10ms | ~50ms | <1ms | | Write limit | 1/s per key | Unlimited | Unlimited | | Use case | Config, cache | Relational data | Coordination | ## Quick Start ```bash wrangler kv namespace create MY_NAMESPACE # Add binding to wrangler.jsonc ``` ```typescript // Write await env.MY_KV.put("key", "value", { expirationTtl: 300 }); // Read const value = await env.MY_KV.get("key"); const json = await env.MY_KV.get("config", "json"); ``` ## Core Operations | Method | Purpose | Returns | |--------|---------|---------| | `get(key, type?)` | Single read | `string \| null` | | `get(keys, type?)` | Bulk read (≤100) | `Map` | | `put(key, value, options?)` | Write | `Promise` | | `delete(key)` | Delete | `Promise` | | `list(options?)` | List keys | `{ keys, list_complete, cursor? }` | | `getWithMetadata(key)` | Get + metadata | `{ value, metadata }` | ## Consistency Model - **Write visibility:** Immediate in same location, ≤60s globally - **Read path:** Eventually consistent - **Write rate:** 1 write/second per key (429 on exceed) ## Reading Order | Task | Files to Read | |------|---------------| | Quick start | README → configuration.md | | Implement feature | README → api.md → patterns.md | | Debug issues | gotchas.md → api.md | | Batch operations | api.md (bulk section) → patterns.md | | Performance tuning | gotchas.md (performance) → patterns.md (caching) | ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc setup, namespace creation, TypeScript types - [api.md](./api.md) - KV methods, bulk operations, cacheTtl, content types - [patterns.md](./patterns.md) - Caching, sessions, rate limiting, A/B testing - [gotchas.md](./gotchas.md) - Eventual consistency, concurrent writes, value limits ## See Also - [workers](../workers/) - Worker runtime for KV access - [d1](../d1/) - Use D1 for strong consistency needs - [durable-objects](../durable-objects/) - Strongly consistent alternative ================================================ FILE: skills/.curated/cloudflare-deploy/references/kv/api.md ================================================ # KV API Reference ## Read Operations ```typescript // Single key (string) const value = await env.MY_KV.get("user:123"); // JSON type (auto-parsed) const config = await env.MY_KV.get("config", "json"); // ArrayBuffer for binary const buffer = await env.MY_KV.get("image", "arrayBuffer"); // Stream for large values const stream = await env.MY_KV.get("large-file", "stream"); // With cache TTL (min 60s) const value = await env.MY_KV.get("key", { type: "text", cacheTtl: 300 }); // Bulk get (max 100 keys, counts as 1 operation) const keys = ["user:1", "user:2", "user:3", "missing:key"]; const results = await env.MY_KV.get(keys); // Returns Map console.log(results.get("user:1")); // "John" (if exists) console.log(results.get("missing:key")); // null // Process results with null handling for (const [key, value] of results) { if (value !== null) { // Handle found keys console.log(`${key}: ${value}`); } } // TypeScript with generics (type-safe JSON parsing) interface UserProfile { name: string; email: string; } const profile = await env.USERS.get("user:123", "json"); // profile is typed as UserProfile | null if (profile) { console.log(profile.name); // Type-safe access } // Bulk get with type const configs = await env.MY_KV.get(["config:app", "config:feature"], "json"); // Map ``` ## Write Operations ```typescript // Basic put await env.MY_KV.put("key", "value"); await env.MY_KV.put("config", JSON.stringify({ theme: "dark" })); // With expiration (UNIX timestamp) await env.MY_KV.put("session", token, { expiration: Math.floor(Date.now() / 1000) + 3600 }); // With TTL (seconds from now, min 60) await env.MY_KV.put("cache", data, { expirationTtl: 300 }); // With metadata (max 1024 bytes) await env.MY_KV.put("user:profile", userData, { metadata: { version: 2, lastUpdated: Date.now() } }); // Combined await env.MY_KV.put("temp", value, { expirationTtl: 3600, metadata: { temporary: true } }); ``` ## Get with Metadata ```typescript // Single key const result = await env.MY_KV.getWithMetadata("user:profile"); // { value: string | null, metadata: any | null } if (result.value && result.metadata) { const { version, lastUpdated } = result.metadata; } // Multiple keys (bulk) const keys = ["key1", "key2", "key3"]; const results = await env.MY_KV.getWithMetadata(keys); // Returns Map for (const [key, result] of results) { if (result.value) { console.log(`${key}: ${result.value}`); console.log(`Metadata: ${JSON.stringify(result.metadata)}`); // cacheStatus field indicates cache hit/miss (when available) } } // With type const result = await env.MY_KV.getWithMetadata("user:123", "json"); // result: { value: UserData | null, metadata: any | null, cacheStatus?: string } ``` ## Delete Operations ```typescript await env.MY_KV.delete("key"); // Always succeeds (even if key missing) ``` ## List Operations ```typescript // List all const keys = await env.MY_KV.list(); // { keys: [...], list_complete: boolean, cursor?: string } // With prefix const userKeys = await env.MY_KV.list({ prefix: "user:" }); // Pagination let cursor: string | undefined; let allKeys = []; do { const result = await env.MY_KV.list({ cursor, limit: 1000 }); allKeys.push(...result.keys); cursor = result.cursor; } while (!result.list_complete); ``` ## Performance Considerations ### Type Selection | Type | Use Case | Performance | |------|----------|-------------| | `stream` | Large values (>1MB) | Fastest - no buffering | | `arrayBuffer` | Binary data | Fast - single allocation | | `text` | String values | Medium | | `json` | Objects (parse overhead) | Slowest - parsing cost | ### Parallel Reads ```typescript // Efficient parallel reads with Promise.all() const [user, settings, cache] = await Promise.all([ env.USERS.get("user:123", "json"), env.SETTINGS.get("config:app", "json"), env.CACHE.get("data:latest") ]); ``` ## Error Handling - **Missing keys:** Return `null` (not an error) - **Rate limit (429):** Retry with exponential backoff (see gotchas.md) - **Response too large (413):** Values >25MB fail with 413 error See [gotchas.md](./gotchas.md) for detailed error patterns and solutions. ================================================ FILE: skills/.curated/cloudflare-deploy/references/kv/configuration.md ================================================ # KV Configuration ## Create Namespace ```bash wrangler kv namespace create MY_NAMESPACE # Output: { binding = "MY_NAMESPACE", id = "abc123..." } wrangler kv namespace create MY_NAMESPACE --preview # For local dev ``` ## Workers Binding **wrangler.jsonc:** ```jsonc { "kv_namespaces": [ { "binding": "MY_KV", "id": "abc123xyz789" }, // Optional: Different namespace for preview/development { "binding": "MY_KV", "preview_id": "preview-abc123" } ] } ``` ## TypeScript Types **env.d.ts:** ```typescript interface Env { MY_KV: KVNamespace; SESSIONS: KVNamespace; CACHE: KVNamespace; } ``` **worker.ts:** ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { // env.MY_KV is now typed as KVNamespace const value = await env.MY_KV.get("key"); return new Response(value || "Not found"); } } satisfies ExportedHandler; ``` **Type-safe JSON operations:** ```typescript interface UserProfile { name: string; email: string; role: "admin" | "user"; } const profile = await env.USERS.get("user:123", "json"); // profile: UserProfile | null (type-safe!) if (profile) { console.log(profile.name); // TypeScript knows this is a string } ``` ## CLI Operations ```bash # Put wrangler kv key put --binding=MY_KV "key" "value" wrangler kv key put --binding=MY_KV "key" --path=./file.json --ttl=3600 # Get wrangler kv key get --binding=MY_KV "key" # Delete wrangler kv key delete --binding=MY_KV "key" # List wrangler kv key list --binding=MY_KV --prefix="user:" # Bulk operations (max 10,000 keys per file) wrangler kv bulk put data.json --binding=MY_KV wrangler kv bulk get keys.json --binding=MY_KV wrangler kv bulk delete keys.json --binding=MY_KV --force ``` ## Local Development ```bash wrangler dev # Local KV (isolated) wrangler dev --remote # Remote KV (production) # Or in wrangler.jsonc: # "kv_namespaces": [{ "binding": "MY_KV", "id": "...", "remote": true }] ``` ## REST API ### Single Operations ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiEmail: process.env.CLOUDFLARE_EMAIL, apiKey: process.env.CLOUDFLARE_API_KEY }); // Single key operations await client.kv.namespaces.values.update(namespaceId, 'key', { account_id: accountId, value: 'value', expiration_ttl: 3600 }); ``` ### Bulk Operations ```typescript // Bulk update (up to 10,000 keys, max 100MB total) await client.kv.namespaces.bulkUpdate(namespaceId, { account_id: accountId, body: [ { key: "key1", value: "value1", expiration_ttl: 3600 }, { key: "key2", value: "value2", metadata: { version: 1 } }, { key: "key3", value: "value3" } ] }); // Bulk get (up to 100 keys) const results = await client.kv.namespaces.bulkGet(namespaceId, { account_id: accountId, keys: ["key1", "key2", "key3"] }); // Bulk delete (up to 10,000 keys) await client.kv.namespaces.bulkDelete(namespaceId, { account_id: accountId, keys: ["key1", "key2", "key3"] }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/kv/gotchas.md ================================================ # KV Gotchas & Troubleshooting ## Common Errors ### "Stale Read After Write" **Cause:** Eventual consistency means writes may not be immediately visible in other regions **Solution:** Don't read immediately after write; return confirmation without reading or use the local value you just wrote. Writes visible immediately in same location, ≤60s globally ```typescript // ❌ BAD: Read immediately after write await env.KV.put("key", "value"); const value = await env.KV.get("key"); // May be null in other regions! // ✅ GOOD: Use the value you just wrote const newValue = "value"; await env.KV.put("key", newValue); return new Response(newValue); // Don't re-read ``` ### "429 Rate Limit on Concurrent Writes" **Cause:** Multiple concurrent writes to same key exceeding 1 write/second limit **Solution:** Use sequential writes, unique keys for concurrent operations, or implement retry with exponential backoff ```typescript async function putWithRetry( kv: KVNamespace, key: string, value: string, maxAttempts = 5 ): Promise { let delay = 1000; for (let i = 0; i < maxAttempts; i++) { try { await kv.put(key, value); return; } catch (err) { if (err instanceof Error && err.message.includes("429")) { if (i === maxAttempts - 1) throw err; await new Promise(r => setTimeout(r, delay)); delay *= 2; // Exponential backoff } else { throw err; } } } } ``` ### "Inefficient Multiple Gets" **Cause:** Making multiple individual get() calls instead of bulk operation **Solution:** Use bulk get with array of keys: `env.USERS.get(["user:1", "user:2", "user:3"])` to reduce to 1 operation ### "Null Reference Error" **Cause:** Attempting to use value without checking for null when key doesn't exist **Solution:** Always handle null returns - KV returns `null` for missing keys, not undefined ```typescript // ❌ BAD: Assumes value exists const config = await env.KV.get("config", "json"); return config.theme; // TypeError if null! // ✅ GOOD: Null checks const config = await env.KV.get("config", "json"); return config?.theme ?? "default"; // ✅ GOOD: Early return const config = await env.KV.get("config", "json"); if (!config) return new Response("Not found", { status: 404 }); return new Response(config.theme); ``` ### "Negative Lookup Caching" **Cause:** Keys that don't exist are cached as "not found" for up to 60s **Solution:** Creating a key after checking won't be visible until cache expires ```typescript // Check → create pattern has race condition const exists = await env.KV.get("key"); // null, cached as "not found" if (!exists) { await env.KV.put("key", "value"); // Next get() may still return null for ~60s due to negative cache } // Alternative: Always assume key may not exist, use defaults const value = await env.KV.get("key") ?? "default-value"; ``` ## Performance Tips | Scenario | Recommendation | Why | |----------|----------------|-----| | Large values (>1MB) | Use `stream` type | Avoids buffering entire value in memory | | Many small keys | Coalesce into one JSON object | Reduces operations, improves cache hit rate | | High write volume | Spread across different keys | Avoid 1 write/second per-key limit | | Cold reads | Increase `cacheTtl` parameter | Reduces latency for frequently-read data | | Bulk operations | Use array form of get() | Single operation, better performance | ## Cost Examples **Free tier:** - 100K reads/day = 3M/month ✅ - 1K writes/day = 30K/month ✅ - 1GB storage ✅ **Example paid workload:** - 10M reads/month = $5.00 - 100K writes/month = $0.50 - 1GB storage = $0.50 - **Total: ~$6/month** ## Limits | Limit | Value | Notes | |-------|-------|-------| | Key size | 512 bytes | Maximum key length | | Value size | 25 MiB | Maximum value; 413 error if exceeded | | Metadata size | 1024 bytes | Maximum metadata per key | | cacheTtl minimum | 60s | Minimum cache TTL | | Write rate per key | 1 write/second | All plans; 429 error if exceeded | | Propagation time | ≤60s | Global propagation time | | Bulk get max | 100 keys | Maximum keys per bulk operation | | Operations per Worker | 1,000 | Per request (bulk counts as 1) | | Reads pricing | $0.50 per 10M | Per million reads | | Writes pricing | $5.00 per 1M | Per million writes | | Deletes pricing | $5.00 per 1M | Per million deletes | | Storage pricing | $0.50 per GB-month | Per GB per month | ================================================ FILE: skills/.curated/cloudflare-deploy/references/kv/patterns.md ================================================ # KV Patterns & Best Practices ## Multi-Tier Caching ```typescript // Memory → KV → Origin (3-tier cache) const memoryCache = new Map(); async function getCached(env: Env, key: string): Promise { const now = Date.now(); // L1: Memory cache (fastest) const cached = memoryCache.get(key); if (cached && cached.expires > now) { return cached.data; } // L2: KV cache (fast) const kvValue = await env.CACHE.get(key, "json"); if (kvValue) { memoryCache.set(key, { data: kvValue, expires: now + 60000 }); // 1min in memory return kvValue; } // L3: Origin (slow) const origin = await fetch(`https://api.example.com/${key}`).then(r => r.json()); // Backfill caches await env.CACHE.put(key, JSON.stringify(origin), { expirationTtl: 300 }); // 5min in KV memoryCache.set(key, { data: origin, expires: now + 60000 }); return origin; } ``` ## API Response Caching ```typescript async function getCachedData(env: Env, key: string, fetcher: () => Promise): Promise { const cached = await env.MY_KV.get(key, "json"); if (cached) return cached; const data = await fetcher(); await env.MY_KV.put(key, JSON.stringify(data), { expirationTtl: 300 }); return data; } const apiData = await getCachedData( env, "cache:users", () => fetch("https://api.example.com/users").then(r => r.json()) ); ``` ## Session Management ```typescript interface Session { userId: string; expiresAt: number; } async function createSession(env: Env, userId: string): Promise { const sessionId = crypto.randomUUID(); const expiresAt = Date.now() + (24 * 60 * 60 * 1000); await env.SESSIONS.put( `session:${sessionId}`, JSON.stringify({ userId, expiresAt }), { expirationTtl: 86400, metadata: { createdAt: Date.now() } } ); return sessionId; } async function getSession(env: Env, sessionId: string): Promise { const data = await env.SESSIONS.get(`session:${sessionId}`, "json"); if (!data || data.expiresAt < Date.now()) return null; return data; } ``` ## Coalesce Cold Keys ```typescript // ❌ BAD: Many individual keys await env.KV.put("user:123:name", "John"); await env.KV.put("user:123:email", "john@example.com"); // ✅ GOOD: Single coalesced object await env.USERS.put("user:123:profile", JSON.stringify({ name: "John", email: "john@example.com", role: "admin" })); // Benefits: Hot key cache, single read, reduced operations // Trade-off: Harder to update individual fields ``` ## Prefix-Based Namespacing ```typescript // Logical partitioning within single namespace const PREFIXES = { users: "user:", sessions: "session:", cache: "cache:", features: "feature:" } as const; // Write with prefix async function setUser(env: Env, id: string, data: any) { await env.KV.put(`${PREFIXES.users}${id}`, JSON.stringify(data)); } // Read with prefix async function getUser(env: Env, id: string) { return await env.KV.get(`${PREFIXES.users}${id}`, "json"); } // List by prefix async function listUserIds(env: Env): Promise { const result = await env.KV.list({ prefix: PREFIXES.users }); return result.keys.map(k => k.name.replace(PREFIXES.users, "")); } // Example hierarchy "user:123:profile" "user:123:settings" "cache:api:users" "session:abc-def" "feature:flags:beta" ``` ## Metadata Versioning ```typescript interface VersionedData { version: number; data: any; } async function migrateIfNeeded(env: Env, key: string) { const result = await env.DATA.getWithMetadata(key, "json"); if (!result.value) return null; const currentVersion = result.metadata?.version || 1; const targetVersion = 2; if (currentVersion < targetVersion) { // Migrate data format const migrated = migrate(result.value, currentVersion, targetVersion); // Store with new version await env.DATA.put(key, JSON.stringify(migrated), { metadata: { version: targetVersion, migratedAt: Date.now() } }); return migrated; } return result.value; } function migrate(data: any, from: number, to: number): any { if (from === 1 && to === 2) { // V1 → V2: Rename field return { ...data, userName: data.name }; } return data; } ``` ## Error Boundary Pattern ```typescript // Resilient get with fallback async function resilientGet( env: Env, key: string, fallback: T ): Promise { try { const value = await env.KV.get(key, "json"); return value ?? fallback; } catch (err) { console.error(`KV error for ${key}:`, err); return fallback; } } // Usage const config = await resilientGet(env, "config:app", { theme: "light", maxItems: 10 }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/miniflare/README.md ================================================ # Miniflare Local simulator for Cloudflare Workers development/testing. Runs Workers in workerd sandbox implementing runtime APIs - no internet required. ## Features - Full-featured: KV, Durable Objects, R2, D1, WebSockets, Queues - Fully-local: test without internet, instant reload - TypeScript-native: detailed logging, source maps - Advanced testing: dispatch events without HTTP, simulate Worker connections ## When to Use **Decision tree for testing Workers:** ``` Need to test Workers? │ ├─ Unit tests for business logic only? │ └─ getPlatformProxy (Vitest/Jest) → [patterns.md](./patterns.md#getplatformproxy) │ Fast, no HTTP, direct binding access │ ├─ Integration tests with full runtime? │ ├─ Single Worker? │ │ └─ Miniflare API → [Quick Start](#quick-start) │ │ Full control, programmatic access │ │ │ ├─ Multiple Workers + service bindings? │ │ └─ Miniflare workers array → [configuration.md](./configuration.md#multiple-workers) │ │ Shared storage, inter-worker calls │ │ │ └─ Vitest test runner integration? │ └─ vitest-pool-workers → [patterns.md](./patterns.md#vitest-pool-workers) │ Full Workers env in Vitest │ └─ Local dev server? └─ wrangler dev (not Miniflare) Hot reload, automatic config ``` **Use Miniflare for:** - Integration tests with full Worker runtime - Testing bindings/storage locally - Multiple Workers with service bindings - Programmatic event dispatch (fetch, queue, scheduled) **Use getPlatformProxy for:** - Fast unit tests of business logic - Testing without HTTP overhead - Vitest/Jest environments **Use Wrangler for:** - Local development workflow - Production deployments ## Setup ```bash npm i -D miniflare ``` Requires ES modules in `package.json`: ```json {"type": "module"} ``` ## Quick Start ```js import { Miniflare } from "miniflare"; const mf = new Miniflare({ modules: true, script: ` export default { async fetch(request, env, ctx) { return new Response("Hello Miniflare!"); } } `, }); const res = await mf.dispatchFetch("http://localhost:8787/"); console.log(await res.text()); // Hello Miniflare! await mf.dispose(); ``` ## Reading Order **New to Miniflare?** Start here: 1. [Quick Start](#quick-start) - Running in 2 minutes 2. [When to Use](#when-to-use) - Choose your testing approach 3. [patterns.md](./patterns.md) - Testing patterns (getPlatformProxy, Vitest, node:test) 4. [configuration.md](./configuration.md) - Configure bindings, storage, multiple workers **Troubleshooting:** - [gotchas.md](./gotchas.md) - Common errors and debugging **API reference:** - [api.md](./api.md) - Complete method reference ## See Also - [wrangler](../wrangler/) - CLI tool that embeds Miniflare for `wrangler dev` - [workerd](../workerd/) - Runtime that powers Miniflare - [workers](../workers/) - Workers runtime API documentation ================================================ FILE: skills/.curated/cloudflare-deploy/references/miniflare/api.md ================================================ # Programmatic API ## Miniflare Class ```typescript class Miniflare { constructor(options: MiniflareOptions); // Lifecycle ready: Promise; // Resolves when server ready, returns URL dispose(): Promise; // Cleanup resources setOptions(options: MiniflareOptions): Promise; // Reload config // Event dispatching dispatchFetch(url: string | URL | Request, init?: RequestInit): Promise; getWorker(name?: string): Promise; // Bindings access getBindings>(name?: string): Promise; getCf(name?: string): Promise; getKVNamespace(name: string): Promise; getR2Bucket(name: string): Promise; getDurableObjectNamespace(name: string): Promise; getDurableObjectStorage(id: DurableObjectId): Promise; getD1Database(name: string): Promise; getCaches(): Promise; getQueueProducer(name: string): Promise; // Debugging getInspectorURL(): Promise; // Chrome DevTools inspector URL } ``` ## Event Dispatching **Fetch (no HTTP server):** ```js const res = await mf.dispatchFetch("http://localhost:8787/path", { method: "POST", headers: { "Authorization": "Bearer token" }, body: JSON.stringify({ data: "value" }), }); ``` **Custom Host routing:** ```js const res = await mf.dispatchFetch("http://localhost:8787/", { headers: { "Host": "api.example.com" }, }); ``` **Scheduled:** ```js const worker = await mf.getWorker(); const result = await worker.scheduled({ cron: "30 * * * *" }); // result: { outcome: "ok", noRetry: false } ``` **Queue:** ```js const worker = await mf.getWorker(); const result = await worker.queue("queue-name", [ { id: "msg1", timestamp: new Date(), body: "data", attempts: 1 }, ]); // result: { outcome: "ok", retryAll: false, ackAll: false, ... } ``` ## Bindings Access **Environment variables:** ```js // Basic usage const bindings = await mf.getBindings(); console.log(bindings.SECRET_KEY); // With type safety (recommended): interface Env { SECRET_KEY: string; API_URL: string; KV: KVNamespace; } const env = await mf.getBindings(); env.SECRET_KEY; // string (typed!) env.KV.get("key"); // KVNamespace methods available ``` **Request.cf object:** ```js const cf = await mf.getCf(); console.log(cf?.colo); // "DFW" console.log(cf?.country); // "US" ``` **KV:** ```js const ns = await mf.getKVNamespace("TEST_NAMESPACE"); await ns.put("key", "value"); const value = await ns.get("key"); ``` **R2:** ```js const bucket = await mf.getR2Bucket("BUCKET"); await bucket.put("file.txt", "content"); const object = await bucket.get("file.txt"); ``` **Durable Objects:** ```js const ns = await mf.getDurableObjectNamespace("COUNTER"); const id = ns.idFromName("test"); const stub = ns.get(id); const res = await stub.fetch("http://localhost/"); // Access storage directly: const storage = await mf.getDurableObjectStorage(id); await storage.put("key", "value"); ``` **D1:** ```js const db = await mf.getD1Database("DB"); await db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`); await db.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice").run(); ``` **Cache:** ```js const caches = await mf.getCaches(); const defaultCache = caches.default; await defaultCache.put("http://example.com", new Response("cached")); ``` **Queue producer:** ```js const producer = await mf.getQueueProducer("QUEUE"); await producer.send({ body: "message data" }); ``` ## Lifecycle **Reload:** ```js await mf.setOptions({ scriptPath: "worker.js", bindings: { VERSION: "2.0" }, }); ``` **Watch (manual):** ```js import { watch } from "fs"; const config = { scriptPath: "worker.js" }; const mf = new Miniflare(config); watch("worker.js", async () => { console.log("Reloading..."); await mf.setOptions(config); }); ``` **Cleanup:** ```js await mf.dispose(); ``` ## Debugging **Inspector URL for DevTools:** ```js const url = await mf.getInspectorURL(); console.log(`DevTools: ${url}`); // Open in Chrome DevTools for breakpoints, profiling ``` **Wait for server ready:** ```js const mf = new Miniflare({ scriptPath: "worker.js" }); const url = await mf.ready; // Promise console.log(`Server running at ${url}`); // http://127.0.0.1:8787 // Note: dispatchFetch() waits automatically, no need to await ready const res = await mf.dispatchFetch("http://localhost/"); // Works immediately ``` See [configuration.md](./configuration.md) for all constructor options. ================================================ FILE: skills/.curated/cloudflare-deploy/references/miniflare/configuration.md ================================================ # Configuration ## Script Loading ```js // Inline new Miniflare({ modules: true, script: `export default { ... }` }); // File-based new Miniflare({ scriptPath: "worker.js" }); // Multi-module new Miniflare({ scriptPath: "src/index.js", modules: true, modulesRules: [ { type: "ESModule", include: ["**/*.js"] }, { type: "Text", include: ["**/*.txt"] }, ], }); ``` ## Compatibility ```js new Miniflare({ compatibilityDate: "2026-01-01", // Use recent date for latest features compatibilityFlags: [ "nodejs_compat", // Node.js APIs (process, Buffer, etc) "streams_enable_constructors", // Stream constructors ], upstream: "https://example.com", // Fallback for unhandled requests }); ``` **Critical:** Use `compatibilityDate: "2026-01-01"` or latest to match production runtime. Old dates limit available APIs. ## HTTP Server & Request.cf ```js new Miniflare({ port: 8787, // Default: 8787 host: "127.0.0.1", https: true, // Self-signed cert liveReload: true, // Auto-reload HTML cf: true, // Fetch live Request.cf data (cached) // cf: "./cf.json", // Or load from file // cf: { colo: "DFW" }, // Or inline mock }); ``` **Note:** For tests, use `dispatchFetch()` (no port conflicts). ## Storage Bindings ```js new Miniflare({ // KV kvNamespaces: ["TEST_NAMESPACE", "CACHE"], kvPersist: "./kv-data", // Optional: persist to disk // R2 r2Buckets: ["BUCKET", "IMAGES"], r2Persist: "./r2-data", // Durable Objects modules: true, durableObjects: { COUNTER: "Counter", // className API_OBJECT: { className: "ApiObject", scriptName: "api-worker" }, }, durableObjectsPersist: "./do-data", // D1 d1Databases: ["DB"], d1Persist: "./d1-data", // Cache cache: true, // Default cachePersist: "./cache-data", }); ``` ## Bindings ```js new Miniflare({ // Environment variables bindings: { SECRET_KEY: "my-secret-value", API_URL: "https://api.example.com", DEBUG: true, }, // Other bindings wasmBindings: { ADD_MODULE: "./add.wasm" }, textBlobBindings: { TEXT: "./data.txt" }, queueProducers: ["QUEUE"], }); ``` ## Multiple Workers ```js new Miniflare({ workers: [ { name: "main", kvNamespaces: { DATA: "shared" }, serviceBindings: { API: "api-worker" }, script: `export default { ... }`, }, { name: "api-worker", kvNamespaces: { DATA: "shared" }, // Shared storage script: `export default { ... }`, }, ], }); ``` **With routing:** ```js workers: [ { name: "api", scriptPath: "./api.js", routes: ["api.example.com/*"] }, { name: "web", scriptPath: "./web.js", routes: ["example.com/*"] }, ], ``` ## Logging & Performance ```js import { Log, LogLevel } from "miniflare"; new Miniflare({ log: new Log(LogLevel.DEBUG), // DEBUG | INFO | WARN | ERROR | NONE scriptTimeout: 30000, // CPU limit (ms) workersConcurrencyLimit: 10, // Max concurrent workers }); ``` ## Workers Sites ```js new Miniflare({ sitePath: "./public", siteInclude: ["**/*.html", "**/*.css"], siteExclude: ["**/*.map"], }); ``` ## From wrangler.toml Miniflare doesn't auto-read `wrangler.toml`: ```toml # wrangler.toml name = "my-worker" main = "src/index.ts" compatibility_date = "2026-01-01" [[kv_namespaces]] binding = "KV" ``` ```js // Miniflare equivalent new Miniflare({ scriptPath: "src/index.ts", compatibilityDate: "2026-01-01", kvNamespaces: ["KV"], }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/miniflare/gotchas.md ================================================ # Gotchas & Troubleshooting ## Miniflare Limitations **Not supported:** - Analytics Engine (use mocks) - Cloudflare Images/Stream - Browser Rendering API - Tail Workers - Workers for Platforms (partial support) **Behavior differences from production:** - Runs workerd locally, not Cloudflare edge - Storage is local (filesystem/memory), not distributed - `Request.cf` is cached/mocked, not real edge data - Performance differs from edge - Caching implementation may vary slightly ## Common Errors ### "Cannot find module" **Cause:** Module path wrong or `modulesRules` not configured **Solution:** ```js new Miniflare({ modules: true, modulesRules: [{ type: "ESModule", include: ["**/*.js"] }], }); ``` ### "Data not persisting" **Cause:** Persist paths are files, not directories **Solution:** ```js kvPersist: "./data/kv", // Directory, not file ``` ### "Cannot run TypeScript" **Cause:** Miniflare doesn't transpile TypeScript **Solution:** Build first with esbuild/tsc, then run compiled JS ### "`request.cf` is undefined" **Cause:** CF data not configured **Solution:** ```js new Miniflare({ cf: true }); // Or cf: "./cf.json" ``` ### "EADDRINUSE" port conflict **Cause:** Multiple instances using same port **Solution:** Use `dispatchFetch()` (no HTTP server) or `port: 0` for auto-assign ### "Durable Object not found" **Cause:** Class export doesn't match config name **Solution:** ```js export class Counter {} // Must match new Miniflare({ durableObjects: { COUNTER: "Counter" } }); ``` ## Debugging **Enable verbose logging:** ```js import { Log, LogLevel } from "miniflare"; new Miniflare({ log: new Log(LogLevel.DEBUG) }); ``` **Chrome DevTools:** ```js const url = await mf.getInspectorURL(); console.log(`DevTools: ${url}`); // Open in Chrome ``` **Inspect bindings:** ```js const env = await mf.getBindings(); console.log(Object.keys(env)); ``` **Verify storage:** ```js const ns = await mf.getKVNamespace("TEST"); const { keys } = await ns.list(); ``` ## Best Practices **✓ Do:** - Use `dispatchFetch()` for tests (no HTTP server) - In-memory storage for CI (omit persist options) - New instances per test for isolation - Type-safe bindings with interfaces - `await mf.dispose()` in cleanup **✗ Avoid:** - HTTP server in tests - Shared instances without cleanup - Old compatibility dates (use 2026+) ## Migration Guides ### From Miniflare 2.x to 3+ Breaking changes in v3+: | v2 | v3+ | |----|-----| | `getBindings()` sync | `getBindings()` returns Promise | | `ready` is void | `ready` returns `Promise` | | service-worker-mock | Built on workerd | | Different options | Restructured constructor | **Example migration:** ```js // v2 const bindings = mf.getBindings(); mf.ready; // void // v3+ const bindings = await mf.getBindings(); const url = await mf.ready; // Promise ``` ### From unstable_dev to Miniflare ```js // Old (deprecated) import { unstable_dev } from "wrangler"; const worker = await unstable_dev("src/index.ts"); // New import { Miniflare } from "miniflare"; const mf = new Miniflare({ scriptPath: "src/index.ts" }); ``` ### From Wrangler Dev Miniflare doesn't auto-read `wrangler.toml`: ```js // Translate manually: new Miniflare({ scriptPath: "dist/worker.js", compatibilityDate: "2026-01-01", kvNamespaces: ["KV"], bindings: { API_KEY: process.env.API_KEY }, }); ``` ## Resource Limits | Limit | Value | Notes | |-------|-------|-------| | CPU time | 30s default | Configurable via `scriptTimeout` | | Storage | Filesystem | Performance varies by disk | | Memory | System dependent | No artificial limits | | Request.cf | Cached/mocked | Not live edge data | See [patterns.md](./patterns.md) for testing examples. ================================================ FILE: skills/.curated/cloudflare-deploy/references/miniflare/patterns.md ================================================ # Testing Patterns ## Choosing a Testing Approach | Approach | Use Case | Speed | Setup | Runtime | |----------|----------|-------|-------|---------| | **getPlatformProxy** | Unit tests, logic testing | Fast | Low | Miniflare | | **Miniflare API** | Integration tests, full control | Medium | Medium | Miniflare | | **vitest-pool-workers** | Vitest runner integration | Medium | Medium | workerd | **Quick guide:** - Unit tests → getPlatformProxy - Integration tests → Miniflare API - Vitest workflows → vitest-pool-workers ## getPlatformProxy Lightweight unit testing - provides bindings without full Worker runtime. ```js // vitest.config.js export default { test: { environment: "node" } }; ``` ```js import { env } from "cloudflare:test"; import { describe, it, expect } from "vitest"; describe("Business logic", () => { it("processes data with KV", async () => { await env.KV.put("test", "value"); expect(await env.KV.get("test")).toBe("value"); }); }); ``` **Pros:** Fast, simple **Cons:** No full runtime, can't test fetch handler ## vitest-pool-workers Full Workers runtime in Vitest. Reads `wrangler.toml`. ```bash npm i -D @cloudflare/vitest-pool-workers ``` ```js // vitest.config.js import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" } } }, }, }); ``` ```js import { env, SELF } from "cloudflare:test"; import { it, expect } from "vitest"; it("handles fetch", async () => { const res = await SELF.fetch("http://example.com/"); expect(res.status).toBe(200); }); ``` **Pros:** Full runtime, uses wrangler.toml **Cons:** Requires Wrangler config ## Miniflare API (node:test) ```js import assert from "node:assert"; import test, { after, before } from "node:test"; import { Miniflare } from "miniflare"; let mf; before(() => { mf = new Miniflare({ scriptPath: "src/index.js", kvNamespaces: ["TEST_KV"] }); }); test("fetch", async () => { const res = await mf.dispatchFetch("http://localhost/"); assert.strictEqual(await res.text(), "Hello"); }); after(() => mf.dispose()); ``` ## Testing Durable Objects & Events ```js // Durable Objects const ns = await mf.getDurableObjectNamespace("COUNTER"); const stub = ns.get(ns.idFromName("test-counter")); await stub.fetch("http://localhost/increment"); // Direct storage const storage = await mf.getDurableObjectStorage(ns.idFromName("test-counter")); const count = await storage.get("count"); // Queue const worker = await mf.getWorker(); await worker.queue("my-queue", [ { id: "msg1", timestamp: new Date(), body: { userId: 123 }, attempts: 1 }, ]); // Scheduled await worker.scheduled({ cron: "0 0 * * *" }); ``` ## Test Isolation & Mocking ```js // Per-test isolation beforeEach(() => { mf = new Miniflare({ kvNamespaces: ["TEST"] }); }); afterEach(() => mf.dispose()); // Mock external APIs new Miniflare({ workers: [ { name: "main", serviceBindings: { API: "mock-api" }, script: `...` }, { name: "mock-api", script: `export default { async fetch() { return Response.json({mock: true}); } }` }, ], }); ``` ## Type Safety ```ts import type { KVNamespace } from "@cloudflare/workers-types"; interface Env { KV: KVNamespace; API_KEY: string; } const env = await mf.getBindings(); await env.KV.put("key", "value"); // Typed! export default { async fetch(req: Request, env: Env) { return new Response(await env.KV.get("key")); } } satisfies ExportedHandler; ``` ## WebSocket Testing ```js const res = await mf.dispatchFetch("http://localhost/ws", { headers: { Upgrade: "websocket" }, }); assert.strictEqual(res.status, 101); ``` ## Migration from unstable_dev ```js // Old (deprecated) import { unstable_dev } from "wrangler"; const worker = await unstable_dev("src/index.ts"); // New import { Miniflare } from "miniflare"; const mf = new Miniflare({ scriptPath: "src/index.ts" }); ``` ## CI/CD Tips ```js // In-memory storage (faster) new Miniflare({ kvNamespaces: ["TEST"] }); // No persist = in-memory // Use dispatchFetch (no port conflicts) await mf.dispatchFetch("http://localhost/"); ``` See [gotchas.md](./gotchas.md) for troubleshooting. ================================================ FILE: skills/.curated/cloudflare-deploy/references/network-interconnect/README.md ================================================ # Cloudflare Network Interconnect (CNI) Private, high-performance connectivity to Cloudflare's network. **Enterprise-only**. ## Connection Types **Direct**: Physical fiber in shared datacenter. 10/100 Gbps. You order cross-connect. **Partner**: Virtual via Console Connect, Equinix, Megaport, etc. Managed via partner SDN. **Cloud**: AWS Direct Connect or GCP Cloud Interconnect. Magic WAN only. ## Dataplane Versions **v1 (Classic)**: GRE tunnel support, VLAN/BFD/LACP, asymmetric MTU (1500↓/1476↑), peering support. **v2 (Beta)**: No GRE, 1500 MTU both ways, no VLAN/BFD/LACP yet, ECMP instead. ## Use Cases - **Magic Transit DSR**: DDoS protection, egress via ISP (v1/v2) - **Magic Transit + Egress**: DDoS + egress via CF (v1/v2) - **Magic WAN + Zero Trust**: Private backbone (v1 needs GRE, v2 native) - **Peering**: Public routes at PoP (v1 only) - **App Security**: WAF/Cache/LB (v1/v2 over Magic Transit) ## Prerequisites - Enterprise plan - IPv4 /24+ or IPv6 /48+ prefixes - BGP ASN for v1 - See [locations PDF](https://developers.cloudflare.com/network-interconnect/static/cni-locations-2026-01.pdf) ## Specs - /31 point-to-point subnets - 10km max optical distance - 10G: 10GBASE-LR single-mode - 100G: 100GBASE-LR4 single-mode - **No SLA** (free service) - Backup Internet required ## Throughput | Direction | 10G | 100G | |-----------|-----|------| | CF → Customer | 10 Gbps | 100 Gbps | | Customer → CF (peering) | 10 Gbps | 100 Gbps | | Customer → CF (Magic) | 1 Gbps/tunnel or CNI | 1 Gbps/tunnel or CNI | ## Timeline 2-4 weeks typical. Steps: request → config review → order connection → configure → test → enable health checks → activate → monitor. ## In This Reference - [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 ## Reading Order by Task | Task | Files to Load | |------|---------------| | Initial setup | README → configuration.md → api.md | | Create interconnect via API | api.md → gotchas.md | | Design HA architecture | patterns.md → README | | Troubleshoot connection | gotchas.md → configuration.md | | Cloud integration (AWS/GCP) | configuration.md → patterns.md | | Monitor + alerts | configuration.md | ## Automation Boundary **API-Automatable:** - List/create/delete interconnects (Direct, Partner) - List available slots - Get interconnect status - Download LOA PDF - Create/update CNI objects (BGP config) - Query settings **Requires Account Team:** - Initial request approval - AWS Direct Connect setup (send LOA+VLAN to CF) - GCP Cloud Interconnect final activation - Partner interconnect acceptance (Equinix, Megaport) - VLAN assignment (v1) - Configuration document generation (v1) - Escalations + troubleshooting support **Cannot Be Automated:** - Physical cross-connect installation (Direct) - Partner portal operations (virtual circuit ordering) - AWS/GCP portal operations - Maintenance window coordination ## See Also - [tunnel](../tunnel/) - Alternative for private network connectivity - [spectrum](../spectrum/) - Layer 4 proxy for TCP/UDP traffic ================================================ FILE: skills/.curated/cloudflare-deploy/references/network-interconnect/api.md ================================================ # CNI API Reference See [README.md](README.md) for overview. ## Base ``` https://api.cloudflare.com/client/v4 Auth: Authorization: Bearer ``` ## SDK Namespaces **Primary (recommended):** ```typescript client.networkInterconnects.interconnects.* client.networkInterconnects.cnis.* client.networkInterconnects.slots.* ``` **Alternate (deprecated):** ```typescript client.magicTransit.cfInterconnects.* ``` Use `networkInterconnects` namespace for all new code. ## Interconnects ```http GET /accounts/{account_id}/cni/interconnects # Query: page, per_page POST /accounts/{account_id}/cni/interconnects # Query: validate_only=true (optional) GET /accounts/{account_id}/cni/interconnects/{icon} GET /accounts/{account_id}/cni/interconnects/{icon}/status GET /accounts/{account_id}/cni/interconnects/{icon}/loa # Returns PDF DELETE /accounts/{account_id}/cni/interconnects/{icon} ``` **Create Body:** `account`, `slot_id`, `type`, `facility`, `speed`, `name`, `description` **Status Values:** `active` | `healthy` | `unhealthy` | `pending` | `down` **Response Example:** ```json {"result": [{"id": "icon_abc", "name": "prod", "type": "direct", "facility": "EWR1", "speed": "10G", "status": "active"}]} ``` ## CNI Objects (BGP config) ```http GET /accounts/{account_id}/cni/cnis POST /accounts/{account_id}/cni/cnis GET /accounts/{account_id}/cni/cnis/{cni} PUT /accounts/{account_id}/cni/cnis/{cni} DELETE /accounts/{account_id}/cni/cnis/{cni} ``` Body: `account`, `cust_ip`, `cf_ip`, `bgp_asn`, `bgp_password`, `vlan` ## Slots ```http GET /accounts/{account_id}/cni/slots GET /accounts/{account_id}/cni/slots/{slot} ``` Query: `facility`, `occupied`, `speed` ## Health Checks Configure via Magic Transit/WAN tunnel endpoints (CNI v2). ```typescript await client.magicTransit.tunnels.update(accountId, tunnelId, { health_check: { enabled: true, target: '192.0.2.1', rate: 'high', type: 'request' }, }); ``` Rates: `high` | `medium` | `low`. Types: `request` | `reply`. See [Magic Transit docs](https://developers.cloudflare.com/magic-transit/how-to/configure-tunnel-endpoints/#add-tunnels). ## Settings ```http GET /accounts/{account_id}/cni/settings PUT /accounts/{account_id}/cni/settings ``` Body: `default_asn` ## TypeScript SDK ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CF_TOKEN }); // List await client.networkInterconnects.interconnects.list({ account_id: id }); // Create with validation await client.networkInterconnects.interconnects.create({ account_id: id, account: id, slot_id: 'slot_abc', type: 'direct', facility: 'EWR1', speed: '10G', name: 'prod-interconnect', }, { query: { validate_only: true }, // Dry-run validation }); // Create without validation await client.networkInterconnects.interconnects.create({ account_id: id, account: id, slot_id: 'slot_abc', type: 'direct', facility: 'EWR1', speed: '10G', name: 'prod-interconnect', }); // Status await client.networkInterconnects.interconnects.get(accountId, iconId); // LOA (use fetch) const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${id}/cni/interconnects/${iconId}/loa`, { headers: { Authorization: `Bearer ${token}` }, }); await fs.writeFile('loa.pdf', Buffer.from(await res.arrayBuffer())); // CNI object await client.networkInterconnects.cnis.create({ account_id: id, account: id, cust_ip: '192.0.2.1/31', cf_ip: '192.0.2.0/31', bgp_asn: 65000, vlan: 100, }); // Slots (filter by facility and speed) await client.networkInterconnects.slots.list({ account_id: id, occupied: false, facility: 'EWR1', speed: '10G', }); ``` ## Python SDK ```python from cloudflare import Cloudflare client = Cloudflare(api_token=os.environ["CF_TOKEN"]) # List, create, status (same pattern as TypeScript) client.network_interconnects.interconnects.list(account_id=id) client.network_interconnects.interconnects.create(account_id=id, account=id, slot_id="slot_abc", type="direct", facility="EWR1", speed="10G") client.network_interconnects.interconnects.get(account_id=id, icon=icon_id) # CNI objects and slots client.network_interconnects.cnis.create(account_id=id, cust_ip="192.0.2.1/31", cf_ip="192.0.2.0/31", bgp_asn=65000) client.network_interconnects.slots.list(account_id=id, occupied=False) ``` ## cURL ```bash # List interconnects curl "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects" \ -H "Authorization: Bearer ${CF_TOKEN}" # Create interconnect curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects?validate_only=true" \ -H "Authorization: Bearer ${CF_TOKEN}" -H "Content-Type: application/json" \ -d '{"account": "id", "slot_id": "slot_abc", "type": "direct", "facility": "EWR1", "speed": "10G"}' # LOA PDF curl "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects/${ICON_ID}/loa" \ -H "Authorization: Bearer ${CF_TOKEN}" --output loa.pdf ``` ## Not Available via API **Missing Capabilities:** - BGP session state query (use Dashboard or BGP logs) - Bandwidth utilization metrics (use external monitoring) - Traffic statistics per interconnect - Historical uptime/downtime data - Light level readings (contact account team) - Maintenance window scheduling (notifications only) ## Resources - [API Docs](https://developers.cloudflare.com/api/resources/network_interconnects/) - [TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript) - [Python SDK](https://github.com/cloudflare/cloudflare-python) ================================================ FILE: skills/.curated/cloudflare-deploy/references/network-interconnect/configuration.md ================================================ # CNI Configuration See [README.md](README.md) for overview. ## Workflow (2-4 weeks) 1. **Submit request** (Week 1): Contact account team, provide type/location/use case 2. **Review config** (Week 1-2, v1 only): Approve IP/VLAN/spec doc 3. **Order connection** (Week 2-3): - **Direct**: Get LOA, order cross-connect from facility - **Partner**: Order virtual circuit in partner portal - **Cloud**: Order Direct Connect/Cloud Interconnect, send LOA+VLAN to CF 4. **Configure** (Week 3): Both sides configure per doc 5. **Test** (Week 3-4): Ping, verify BGP, check routes 6. **Health checks** (Week 4): Configure [Magic Transit](https://developers.cloudflare.com/magic-transit/how-to/configure-tunnel-endpoints/#add-tunnels) or [Magic WAN](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-tunnel-endpoints/#add-tunnels) health checks 7. **Activate** (Week 4): Route traffic, verify flow 8. **Monitor**: Enable [maintenance notifications](https://developers.cloudflare.com/network-interconnect/monitoring-and-alerts/#enable-cloudflare-status-maintenance-notification) ## BGP Configuration **v1 Requirements:** - BGP ASN (provide during setup) - /31 subnet for peering - Optional: BGP password **v2:** Simplified, less BGP config needed. **BGP over CNI (Dec 2024):** Magic WAN/Transit can now peer BGP directly over CNI v2 (no GRE tunnel required). **Example v1 BGP:** ``` Router ID: 192.0.2.1 Peer IP: 192.0.2.0 Remote ASN: 13335 Local ASN: 65000 Password: [optional] VLAN: 100 ``` ## Cloud Interconnect Setup ### AWS Direct Connect (Beta) **Requirements:** Magic WAN, AWS Dedicated Direct Connect 1/10 Gbps. **Process:** 1. Contact CF account team 2. Choose location 3. Order in AWS portal 4. AWS provides LOA + VLAN ID 5. Send to CF account team 6. Wait ~4 weeks **Post-setup:** Add [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes) to Magic WAN. Enable [bidirectional health checks](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-tunnel-endpoints/#legacy-bidirectional-health-checks). ### GCP Cloud Interconnect (Beta) **Setup via Dashboard:** 1. Interconnects → Create → Cloud Interconnect → Google 2. Provide name, MTU (match GCP VLAN attachment), speed (50M-50G granular options available for partner interconnects) 3. Enter VLAN attachment pairing key 4. Confirm order **Routing to GCP:** Add [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes). BGP routes from GCP Cloud Router **ignored**. **Routing to CF:** Configure [custom learned routes](https://cloud.google.com/network-connectivity/docs/router/how-to/configure-custom-learned-routes) in Cloud Router. Request prefixes from CF account team. ## Monitoring **Dashboard Status:** | Status | Meaning | |--------|---------| | **Healthy** | Link operational, traffic flowing, health checks passing | | **Active** | Link up, sufficient light, Ethernet negotiated | | **Unhealthy** | Link down, no/low light (<-20 dBm), can't negotiate | | **Pending** | Cross-connect incomplete, device unresponsive, RX/TX swapped | | **Down** | Physical link down, no connectivity | **Alerts:** **CNI Connection Maintenance** (Magic Networking only): ``` Dashboard → Notifications → Add Product: Cloudflare Network Interconnect Type: Connection Maintenance Alert ``` Warnings up to 2 weeks advance. 6hr delay for new additions. **Cloudflare Status Maintenance** (entire PoP): ``` Dashboard → Notifications → Add Product: Cloudflare Status Filter PoPs: gru,fra,lhr ``` **Find PoP code:** ``` Dashboard → Magic Transit/WAN → Configuration → Interconnects Select CNI → Note Data Center (e.g., "gru-b") Use first 3 letters: "gru" ``` ## Best Practices **Critical config-specific practices:** - /31 subnets required for BGP - BGP passwords recommended - BFD for fast failover (v1 only) - Test ping connectivity before BGP - Enable maintenance notifications immediately after activation - Monitor status programmatically via API For design patterns, HA architecture, and security best practices, see [patterns.md](./patterns.md). ================================================ FILE: skills/.curated/cloudflare-deploy/references/network-interconnect/gotchas.md ================================================ # CNI Gotchas & Troubleshooting ## Common Errors ### "Status: Pending" **Cause:** Cross-connect not installed, RX/TX fibers reversed, wrong fiber type, or low light levels **Solution:** 1. Verify cross-connect installed 2. Check fiber at patch panel 3. Swap RX/TX fibers 4. Check light with optical power meter (target > -20 dBm) 5. Contact account team ### "Status: Unhealthy" **Cause:** Physical issue, low light (<-20 dBm), optic mismatch, or dirty connectors **Solution:** 1. Check physical connections 2. Clean fiber connectors 3. Verify optic types (10GBASE-LR/100GBASE-LR4) 4. Test with known-good optics 5. Check patch panel 6. Contact account team ### "BGP Session Down" **Cause:** Wrong IP addressing, wrong ASN, password mismatch, or firewall blocking TCP/179 **Solution:** 1. Verify IPs match CNI object 2. Confirm ASN correct 3. Check BGP password 4. Verify no firewall on TCP/179 5. Check BGP logs 6. Review BGP timers ### "Low Throughput" **Cause:** MTU mismatch, fragmentation, single GRE tunnel (v1), or routing inefficiency **Solution:** 1. Check MTU (1500↓/1476↑ for v1, 1500 both for v2) 2. Test various packet sizes 3. Add more GRE tunnels (v1) 4. Consider upgrading to v2 5. Review routing tables 6. Use LACP for bundling (v1) ## API Errors ### 400 Bad Request: "slot_id already occupied" **Cause:** Another interconnect already uses this slot **Solution:** Use `occupied=false` filter when listing slots: ```typescript await client.networkInterconnects.slots.list({ account_id: id, occupied: false, facility: 'EWR1', }); ``` ### 400 Bad Request: "invalid facility code" **Cause:** Typo or unsupported facility **Solution:** Check [locations PDF](https://developers.cloudflare.com/network-interconnect/static/cni-locations-2026-01.pdf) for valid codes ### 403 Forbidden: "Enterprise plan required" **Cause:** Account not enterprise-level **Solution:** Contact account team to upgrade ### 422 Unprocessable: "validate_only request failed" **Cause:** Dry-run validation found issues (wrong slot, invalid config) **Solution:** Review error message details, fix config before real creation ### Rate Limiting **Limit:** 1200 requests/5min per token **Solution:** Implement exponential backoff, cache slot listings ## Cloud-Specific Issues ### AWS Direct Connect: "VLAN not matching" **Cause:** VLAN ID from AWS LOA doesn't match CNI config **Solution:** 1. Get VLAN from AWS Console after ordering 2. Send exact VLAN to CF account team 3. Verify match in CNI object config ### AWS: "Connection stuck in Pending" **Cause:** LOA not provided to CF or AWS connection not accepted **Solution:** 1. Verify AWS connection status is "Available" 2. Confirm LOA sent to CF account team 3. Wait for CF team acceptance (can take days) ### GCP: "BGP routes not propagating" **Cause:** BGP routes from GCP Cloud Router **ignored by design** **Solution:** Use [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes) in Magic WAN instead ### GCP: "Cannot query VLAN attachment status via API" **Cause:** GCP Cloud Interconnect Dashboard-only (no API yet) **Solution:** Check status in CF Dashboard or GCP Console ## Partner Interconnect Issues ### Equinix: "Virtual circuit not appearing" **Cause:** CF hasn't accepted Equinix connection request **Solution:** 1. Verify VC created in Equinix Fabric Portal 2. Contact CF account team to accept 3. Allow 2-3 business days ### Console Connect/Megaport: "API creation fails" **Cause:** Partner interconnects require partner portal + CF approval **Solution:** Cannot fully automate. Order in partner portal, notify CF account team. ## Anti-Patterns | Anti-Pattern | Why Bad | Solution | |--------------|---------|----------| | Single interconnect for production | No SLA, single point of failure | Use ≥2 with device diversity | | No backup Internet | CNI fails = total outage | Always maintain alternate path | | Polling status every second | Rate limits, wastes API calls | Poll every 30-60s max | | Using v1 for Magic WAN v2 workloads | GRE overhead, complexity | Use v2 for simplified routing | | Assuming BGP session = traffic flowing | BGP up ≠ routes installed | Verify routing tables + test traffic | | Not enabling maintenance alerts | Surprise downtime during maintenance | Enable notifications immediately | | Hardcoding VLAN in automation | VLAN assigned by CF (v1) | Get VLAN from CNI object response | | Using Direct without colocation | Can't access cross-connect | Use Partner or Cloud interconnect | ## What's Not Queryable via API **Cannot retrieve:** - BGP session state (use Dashboard or BGP logs) - Light levels (contact account team) - Historical metrics (uptime, traffic) - Bandwidth utilization per interconnect - Maintenance window schedules (notifications only) - Fiber path details - Cross-connect installation status **Workarounds:** - External monitoring for BGP state - Log aggregation for historical data - Notifications for maintenance windows ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | Max optical distance | 10km | Physical limit | | MTU (v1) | 1500↓ / 1476↑ | Asymmetric | | MTU (v2) | 1500 both | Symmetric | | GRE tunnel throughput | 1 Gbps | Per tunnel (v1) | | Recovery time | Days | No formal SLA | | Light level minimum | -20 dBm | Target threshold | | API rate limit | 1200 req/5min | Per token | | Health check delay | 6 hours | New maintenance alert subscriptions | ================================================ FILE: skills/.curated/cloudflare-deploy/references/network-interconnect/patterns.md ================================================ # CNI Patterns See [README.md](README.md) for overview. ## High Availability **Critical:** Design for resilience from day one. **Requirements:** - Device-level diversity (separate hardware) - Backup Internet connectivity (no SLA on CNI) - Network-resilient locations preferred - Regular failover testing **Architecture:** ``` Your Network A ──10G CNI v2──> CF CCR Device 1 │ Your Network B ──10G CNI v2──> CF CCR Device 2 │ CF Global Network (AS13335) ``` **Capacity Planning:** - Plan across all links - Account for failover scenarios - Your responsibility ## Pattern: Magic Transit + CNI v2 **Use Case:** DDoS protection, private connectivity, no GRE overhead. ```typescript // 1. Create interconnect const ic = await client.networkInterconnects.interconnects.create({ account_id: id, type: 'direct', facility: 'EWR1', speed: '10G', name: 'magic-transit-primary', }); // 2. Poll until active const status = await pollUntilActive(id, ic.id); // 3. Configure Magic Transit tunnel via Dashboard/API ``` **Benefits:** 1500 MTU both ways, simplified routing. ## Pattern: Multi-Cloud Hybrid **Use Case:** AWS/GCP workloads with Cloudflare. **AWS Direct Connect:** ```typescript // 1. Order Direct Connect in AWS Console // 2. Get LOA + VLAN from AWS // 3. Send to CF account team (no API) // 4. Configure static routes in Magic WAN await configureStaticRoutes(id, { prefix: '10.0.0.0/8', nexthop: 'aws-direct-connect', }); ``` **GCP Cloud Interconnect:** ``` 1. Get VLAN attachment pairing key from GCP Console 2. Create via Dashboard: Interconnects → Create → Cloud Interconnect → Google - Enter pairing key, name, MTU, speed 3. Configure static routes in Magic WAN (BGP routes from GCP ignored) 4. Configure custom learned routes in GCP Cloud Router ``` **Note:** Dashboard-only. No API/SDK support yet. ## Pattern: Multi-Location HA **Use Case:** 99.99%+ uptime. ```typescript // Primary (NY) const primary = await client.networkInterconnects.interconnects.create({ account_id: id, type: 'direct', facility: 'EWR1', speed: '10G', name: 'primary-ewr1', }); // Secondary (NY, different hardware) const secondary = await client.networkInterconnects.interconnects.create({ account_id: id, type: 'direct', facility: 'EWR2', speed: '10G', name: 'secondary-ewr2', }); // Tertiary (LA, different geography) const tertiary = await client.networkInterconnects.interconnects.create({ account_id: id, type: 'partner', facility: 'LAX1', speed: '10G', name: 'tertiary-lax1', }); // BGP local preferences: // Primary: 200 // Secondary: 150 // Tertiary: 100 // Internet: Last resort ``` ## Pattern: Partner Interconnect (Equinix) **Use Case:** Quick deployment, no colocation. **Setup:** 1. Order virtual circuit in Equinix Fabric Portal 2. Select Cloudflare as destination 3. Choose facility 4. Send details to CF account team 5. CF accepts in portal 6. Configure BGP **No API automation** – partner portals managed separately. ## Failover & Security **Failover Best Practices:** - Use BGP local preferences for priority - Configure BFD for fast detection (v1) - Test regularly with traffic shift - Document runbooks **Security:** - BGP password authentication - BGP route filtering - Monitor unexpected routes - Magic Firewall for DDoS/threats - Minimum API token permissions - Rotate credentials periodically ## Decision Matrix | Requirement | Recommended | |-------------|-------------| | Collocated with CF | Direct | | Not collocated | Partner | | AWS/GCP workloads | Cloud | | 1500 MTU both ways | v2 | | VLAN tagging | v1 | | Public peering | v1 | | Simplest config | v2 | | BFD fast failover | v1 | | LACP bundling | v1 | ## Resources - [Magic Transit Docs](https://developers.cloudflare.com/magic-transit/) - [Magic WAN Docs](https://developers.cloudflare.com/magic-wan/) - [Argo Smart Routing](https://developers.cloudflare.com/argo/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/observability/README.md ================================================ # Cloudflare Observability Skill Reference **Purpose**: Comprehensive guidance for implementing observability in Cloudflare Workers, covering traces, logs, metrics, and analytics. **Scope**: Cloudflare Observability features ONLY - Workers Logs, Traces, Analytics Engine, Logpush, Metrics & Analytics, and OpenTelemetry exports. --- ## Decision Tree: Which File to Load? Use this to route to the correct file without loading all content: ``` ├─ "How do I enable/configure X?" → configuration.md ├─ "What's the API/method/binding for X?" → api.md ├─ "How do I implement X pattern?" → patterns.md │ ├─ Usage tracking/billing → patterns.md │ ├─ Error tracking → patterns.md │ ├─ Performance monitoring → patterns.md │ ├─ Multi-tenant tracking → patterns.md │ ├─ Tail Worker filtering → patterns.md │ └─ OpenTelemetry export → patterns.md └─ "Why isn't X working?" / "Limits?" → gotchas.md ``` ## Reading Order Load files in this order based on task: | Task Type | Load Order | Reason | |-----------|------------|--------| | **Initial setup** | configuration.md → gotchas.md | Setup first, avoid pitfalls | | **Implement feature** | patterns.md → api.md → gotchas.md | Pattern → API details → edge cases | | **Debug issue** | gotchas.md → configuration.md | Common issues first | | **Query data** | api.md → patterns.md | API syntax → query examples | ## Product Overview ### Workers Logs - **What:** Console output from Workers (console.log/warn/error) - **Access:** Dashboard (Real-time Logs), Logpush, Tail Workers - **Cost:** Free (included with all Workers) - **Retention:** Real-time only (no historical storage in dashboard) ### Workers Traces - **What:** Execution traces with timing, CPU usage, outcome - **Access:** Dashboard (Workers Analytics → Traces), Logpush - **Cost:** $0.10/1M spans (GA pricing starts March 1, 2026), 10M free/month - **Retention:** 14 days included ### Analytics Engine - **What:** High-cardinality event storage and SQL queries - **Access:** SQL API, Dashboard (Analytics → Analytics Engine) - **Cost:** $0.25/1M writes beyond 10M free/month - **Retention:** 90 days (configurable up to 1 year) ### Tail Workers - **What:** Workers that receive logs/traces from other Workers - **Use Cases:** Log filtering, transformation, external export - **Cost:** Standard Workers pricing ### Logpush - **What:** Stream logs to external storage (S3, R2, Datadog, etc.) - **Access:** Dashboard, API - **Cost:** Requires Business/Enterprise plan ## Pricing Summary (2026) | Feature | Free Tier | Cost Beyond Free Tier | Plan Requirement | |---------|-----------|----------------------|------------------| | Workers Logs | Unlimited | Free | Any | | Workers Traces | 10M spans/month | $0.10/1M spans | Paid Workers (GA: March 1, 2026) | | Analytics Engine | 10M writes/month | $0.25/1M writes | Paid Workers | | Logpush | N/A | Included in plan | Business/Enterprise | ## In This Reference - **[configuration.md](configuration.md)** - Setup, deployment, configuration (Logs, Traces, Analytics Engine, Tail Workers, Logpush) - **[api.md](api.md)** - API endpoints, methods, interfaces (GraphQL, SQL, bindings, types) - **[patterns.md](patterns.md)** - Common patterns, use cases, examples (billing, monitoring, error tracking, exports) - **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations (common errors, performance gotchas, pricing) ## See Also - [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) - [Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/) - [Workers Traces Docs](https://developers.cloudflare.com/workers/observability/traces/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/observability/api.md ================================================ ## API Reference ### GraphQL Analytics API **Endpoint**: `https://api.cloudflare.com/client/v4/graphql` **Query Workers Metrics**: ```graphql query { viewer { accounts(filter: { accountTag: $accountId }) { workersInvocationsAdaptive( limit: 100 filter: { datetime_geq: "2025-01-01T00:00:00Z" datetime_leq: "2025-01-31T23:59:59Z" scriptName: "my-worker" } ) { sum { requests errors subrequests } quantiles { cpuTimeP50 cpuTimeP99 wallTimeP50 wallTimeP99 } } } } } ``` ### Analytics Engine SQL API **Endpoint**: `https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql` **Authentication**: `Authorization: Bearer ` (Account Analytics Read permission) **Common Queries**: ```sql -- List all datasets SHOW TABLES; -- Time-series aggregation (5-minute buckets) SELECT intDiv(toUInt32(timestamp), 300) * 300 AS time_bucket, blob1 AS endpoint, SUM(_sample_interval) AS total_requests, AVG(double1) AS avg_response_time_ms FROM api_metrics WHERE timestamp >= NOW() - INTERVAL '24' HOUR GROUP BY time_bucket, endpoint ORDER BY time_bucket DESC; -- Top customers by usage SELECT index1 AS customer_id, SUM(_sample_interval * double1) AS total_api_calls, AVG(double2) AS avg_response_time_ms FROM api_usage WHERE timestamp >= NOW() - INTERVAL '7' DAY GROUP BY customer_id ORDER BY total_api_calls DESC LIMIT 100; -- Error rate analysis SELECT blob1 AS error_type, COUNT(*) AS occurrences, MAX(timestamp) AS last_seen FROM error_tracking WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY error_type ORDER BY occurrences DESC; ``` ### Console Logging API **Methods**: ```typescript // Standard methods (all appear in Workers Logs) console.log('info message'); console.info('info message'); console.warn('warning message'); console.error('error message'); console.debug('debug message'); // Structured logging (recommended) console.log({ level: 'info', user_id: '123', action: 'checkout', amount: 99.99, currency: 'USD' }); ``` **Log Levels**: All console methods produce logs; use structured fields for filtering: ```typescript console.log({ level: 'error', message: 'Payment failed', error_code: 'CARD_DECLINED' }); ``` ### Analytics Engine Binding Types ```typescript interface AnalyticsEngineDataset { writeDataPoint(event: AnalyticsEngineDataPoint): void; } interface AnalyticsEngineDataPoint { // Indexed strings (use for filtering/grouping) indexes?: string[]; // Non-indexed strings (metadata, IDs, URLs) blobs?: string[]; // Numeric values (counts, durations, amounts) doubles?: number[]; } ``` **Field Limits**: - Max 20 indexes - Max 20 blobs - Max 20 doubles - Max 25 `writeDataPoint` calls per request ### Tail Consumer Event Type ```typescript interface TraceItem { event: TraceEvent; logs: TraceLog[]; exceptions: TraceException[]; scriptName?: string; } interface TraceEvent { outcome: 'ok' | 'exception' | 'exceededCpu' | 'exceededMemory' | 'unknown'; cpuTime: number; // microseconds wallTime: number; // microseconds } interface TraceLog { timestamp: number; level: 'log' | 'info' | 'debug' | 'warn' | 'error'; message: any; // string or structured object } interface TraceException { name: string; message: string; timestamp: number; } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/observability/configuration.md ================================================ ## Configuration Patterns ### Enable Workers Logs ```jsonc { "observability": { "enabled": true, "head_sampling_rate": 1 // 100% sampling (default) } } ``` **Best Practice**: Use structured JSON logging for better indexing ```typescript // Good - structured logging console.log({ user_id: 123, action: "login", status: "success", duration_ms: 45 }); // Avoid - unstructured string console.log("user_id: 123 logged in successfully in 45ms"); ``` ### Enable Workers Traces ```jsonc { "observability": { "traces": { "enabled": true, "head_sampling_rate": 0.05 // 5% sampling } } } ``` **Note**: Default sampling is 100%. For high-traffic Workers, use lower sampling (0.01-0.1). ### Configure Analytics Engine **Bind to Worker**: ```toml # wrangler.toml analytics_engine_datasets = [ { binding = "ANALYTICS", dataset = "api_metrics" } ] ``` **Write Data Points**: ```typescript export interface Env { ANALYTICS: AnalyticsEngineDataset; } export default { async fetch(request: Request, env: Env): Promise { // Track metrics env.ANALYTICS.writeDataPoint({ blobs: ['customer_123', 'POST', '/api/v1/users'], doubles: [1, 245.5], // request_count, response_time_ms indexes: ['customer_123'] // for efficient filtering }); return new Response('OK'); } } ``` ### Configure Tail Workers Tail Workers receive logs/traces from other Workers for filtering, transformation, or export. **Setup**: ```toml # wrangler.toml name = "log-processor" main = "src/tail.ts" [[tail_consumers]] service = "my-worker" # Worker to tail ``` **Tail Worker Example**: ```typescript export default { async tail(events: TraceItem[], env: Env, ctx: ExecutionContext) { // Filter errors only const errors = events.filter(event => event.outcome === 'exception' || event.outcome === 'exceededCpu' ); if (errors.length > 0) { // Send to external monitoring ctx.waitUntil( fetch('https://monitoring.example.com/errors', { method: 'POST', body: JSON.stringify(errors) }) ); } } } ``` ### Configure Logpush Send logs to external storage (S3, R2, GCS, Azure, Datadog, etc.). Requires Business/Enterprise plan. **Via Dashboard**: 1. Navigate to Analytics → Logs → Logpush 2. Select destination type 3. Provide credentials and bucket/endpoint 4. Choose dataset (e.g., Workers Trace Events) 5. Configure filters and fields **Via API**: ```bash curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/logpush/jobs" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "workers-logs-to-s3", "destination_conf": "s3://my-bucket/logs?region=us-east-1", "dataset": "workers_trace_events", "enabled": true, "frequency": "high", "filter": "{\"where\":{\"and\":[{\"key\":\"ScriptName\",\"operator\":\"eq\",\"value\":\"my-worker\"}]}}" }' ``` ### Environment-Specific Configuration **Development** (verbose logs, full sampling): ```jsonc // wrangler.dev.jsonc { "observability": { "enabled": true, "head_sampling_rate": 1.0, "traces": { "enabled": true } } } ``` **Production** (reduced sampling, structured logs): ```jsonc // wrangler.prod.jsonc { "observability": { "enabled": true, "head_sampling_rate": 0.1, // 10% sampling "traces": { "enabled": true } } } ``` Deploy with env-specific config: ```bash wrangler deploy --config wrangler.prod.jsonc --env production ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/observability/gotchas.md ================================================ ## Common Errors ### "Logs not appearing" **Cause:** Observability disabled, Worker not redeployed, no traffic, low sampling rate, or log size exceeds 256 KB **Solution:** ```bash # Verify config cat wrangler.jsonc | jq '.observability' # Check deployment wrangler deployments list # Test with curl curl https://your-worker.workers.dev ``` Ensure `observability.enabled = true`, redeploy Worker, check `head_sampling_rate`, verify traffic ### "Traces not being captured" **Cause:** Traces not enabled, incorrect sampling rate, Worker not redeployed, or destination unavailable **Solution:** ```jsonc // Temporarily set to 100% sampling for debugging { "observability": { "enabled": true, "head_sampling_rate": 1.0, "traces": { "enabled": true } } } ``` Ensure `observability.traces.enabled = true`, set `head_sampling_rate` to 1.0 for testing, redeploy, check destination status ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | Max log size | 256 KB | Logs exceeding this are truncated | | Default sampling rate | 1.0 (100%) | Reduce for high-traffic Workers | | Max destinations | Varies by plan | Check dashboard | | Trace context propagation | 100 spans max | Deep call chains may lose spans | | Analytics Engine write rate | 25 writes/request | Excess writes dropped silently | ## Performance Gotchas ### Spectre Mitigation Timing **Problem:** `Date.now()` and `performance.now()` have reduced precision (coarsened to 100μs) **Cause:** Spectre vulnerability mitigation in V8 **Solution:** Accept reduced precision or use Workers Traces for accurate timing ```typescript // Date.now() is coarsened - trace spans are accurate export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { // For user-facing timing, Date.now() is fine const start = Date.now(); const response = await processRequest(request); const duration = Date.now() - start; // For detailed performance analysis, use Workers Traces instead return response; } } ``` ### Analytics Engine _sample_interval Aggregation **Problem:** Queries return incorrect totals when not multiplying by `_sample_interval` **Cause:** Analytics Engine stores sampled data points, each representing multiple events **Solution:** Always multiply counts/sums by `_sample_interval` in aggregations ```sql -- WRONG: Undercounts actual events SELECT blob1 AS customer_id, COUNT(*) AS total_calls FROM api_usage GROUP BY customer_id; -- CORRECT: Accounts for sampling SELECT blob1 AS customer_id, SUM(_sample_interval) AS total_calls FROM api_usage GROUP BY customer_id; ``` ### Trace Context Propagation Limits **Problem:** Deep call chains lose trace context after 100 spans **Cause:** Cloudflare limits trace depth to prevent performance impact **Solution:** Design for flatter architectures or use custom correlation IDs for deep chains ```typescript // For deep call chains, add custom correlation ID const correlationId = crypto.randomUUID(); console.log({ correlationId, event: 'request_start' }); // Pass correlationId through headers to downstream services await fetch('https://api.example.com', { headers: { 'X-Correlation-ID': correlationId } }); ``` ## Pricing (2026) ### Workers Traces - **GA Pricing (starts March 1, 2026):** - $0.10 per 1M trace spans captured - Retention: 14 days included - **Free tier:** 10M trace spans/month - **Note:** Beta usage (before March 1, 2026) is free ### Workers Logs - **Included:** Free for all Workers - **Logpush:** Requires Business/Enterprise plan ### Analytics Engine - **Included:** 10M writes/month on Paid Workers plan - **Additional:** $0.25 per 1M writes beyond included quota ================================================ FILE: skills/.curated/cloudflare-deploy/references/observability/patterns.md ================================================ # Observability Patterns ## Usage-Based Billing ```typescript env.ANALYTICS.writeDataPoint({ blobs: [customerId, request.url, request.method], doubles: [1], // request_count indexes: [customerId] }); ``` ```sql SELECT blob1 AS customer_id, SUM(_sample_interval * double1) AS total_calls FROM api_usage WHERE timestamp >= DATE_TRUNC('month', NOW()) GROUP BY customer_id ``` ## Performance Monitoring ```typescript const start = Date.now(); const response = await fetch(url); env.ANALYTICS.writeDataPoint({ blobs: [url, response.status.toString()], doubles: [Date.now() - start, response.status] }); ``` ```sql SELECT blob1 AS url, AVG(double1) AS avg_ms, percentile(double1, 0.95) AS p95_ms FROM fetch_metrics WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY url ``` ## Error Tracking ```typescript env.ANALYTICS.writeDataPoint({ blobs: [error.name, request.url, request.method], doubles: [1], indexes: [error.name] }); ``` ## Multi-Tenant Tracking ```typescript env.ANALYTICS.writeDataPoint({ indexes: [tenantId], // efficient filtering blobs: [tenantId, url.pathname, method, status], doubles: [1, duration, bytesSize] }); ``` ## Tail Worker Log Filtering ```typescript export default { async tail(events, env, ctx) { const critical = events.filter(e => e.exceptions.length > 0 || e.event.wallTime > 1000000 ); if (critical.length === 0) return; ctx.waitUntil( fetch('https://logging.example.com/ingest', { method: 'POST', headers: { 'Authorization': `Bearer ${env.API_KEY}` }, body: JSON.stringify(critical.map(e => ({ outcome: e.event.outcome, cpu_ms: e.event.cpuTime / 1000, errors: e.exceptions }))) }) ); } }; ``` ## OpenTelemetry Export ```typescript export default { async tail(events, env, ctx) { const otelSpans = events.map(e => ({ traceId: generateId(32), spanId: generateId(16), name: e.scriptName || 'worker.request', attributes: [ { key: 'worker.outcome', value: { stringValue: e.event.outcome } }, { key: 'worker.cpu_time_us', value: { intValue: String(e.event.cpuTime) } } ] })); ctx.waitUntil( fetch('https://api.honeycomb.io/v1/traces', { method: 'POST', headers: { 'X-Honeycomb-Team': env.HONEYCOMB_KEY }, body: JSON.stringify({ resourceSpans: [{ scopeSpans: [{ spans: otelSpans }] }] }) }) ); } }; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages/README.md ================================================ # Cloudflare Pages JAMstack platform for full-stack apps on Cloudflare's global network. ## Key Features - **Git-based deploys**: Auto-deploy from GitHub/GitLab - **Preview deployments**: Unique URL per branch/PR - **Pages Functions**: File-based serverless routing (Workers runtime) - **Static + dynamic**: Smart asset caching + edge compute - **Smart Placement**: Automatic function optimization based on traffic patterns - **Framework optimized**: SvelteKit, Astro, Nuxt, Qwik, Solid Start ## Deployment Methods ### 1. Git Integration (Production) Dashboard → Workers & Pages → Create → Connect to Git → Configure build ### 2. Direct Upload ```bash npx wrangler pages deploy ./dist --project-name=my-project npx wrangler pages deploy ./dist --project-name=my-project --branch=staging ``` ### 3. C3 CLI ```bash npm create cloudflare@latest my-app # Select framework → auto-setup + deploy ``` ## vs Workers - **Pages**: Static sites, JAMstack, frameworks, git workflow, file-based routing - **Workers**: Pure APIs, complex routing, WebSockets, scheduled tasks, email handlers - **Combine**: Pages Functions use Workers runtime, can bind to Workers ## Quick Start ```bash # Create npm create cloudflare@latest # Local dev npx wrangler pages dev ./dist # Deploy npx wrangler pages deploy ./dist --project-name=my-project # Types npx wrangler types --path='./functions/types.d.ts' # Secrets echo "value" | npx wrangler pages secret put KEY --project-name=my-project # Logs npx wrangler pages deployment tail --project-name=my-project ``` ## Resources - [Pages Docs](https://developers.cloudflare.com/pages/) - [Functions API](https://developers.cloudflare.com/pages/functions/api-reference/) - [Framework Guides](https://developers.cloudflare.com/pages/framework-guides/) - [Discord #functions](https://discord.com/channels/595317990191398933/910978223968518144) ## Reading Order **New to Pages?** Start here: 1. README.md (you are here) - Overview & quick start 2. [configuration.md](./configuration.md) - Project setup, wrangler.jsonc, bindings 3. [api.md](./api.md) - Functions API, routing, context 4. [patterns.md](./patterns.md) - Common implementations 5. [gotchas.md](./gotchas.md) - Troubleshooting & pitfalls **Quick reference?** Jump to relevant file above. ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc, build, env vars, Smart Placement - [api.md](./api.md) - Functions API, bindings, context, advanced mode - [patterns.md](./patterns.md) - Full-stack patterns, framework integration - [gotchas.md](./gotchas.md) - Build issues, limits, debugging, framework warnings ## See Also - [pages-functions](../pages-functions/) - File-based routing, middleware - [d1](../d1/) - SQL database for Pages Functions - [kv](../kv/) - Key-value storage for caching/state ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages/api.md ================================================ # Functions API ## File-Based Routing ``` /functions/index.ts → example.com/ /functions/api/users.ts → example.com/api/users /functions/api/users/[id].ts → example.com/api/users/:id /functions/api/users/[[path]].ts → example.com/api/users/* (catchall) /functions/_middleware.ts → Runs before all routes ``` **Rules**: `[param]` = single segment, `[[param]]` = multi-segment catchall, more specific wins. ## Request Handlers ```typescript import type { PagesFunction } from '@cloudflare/workers-types'; interface Env { DB: D1Database; KV: KVNamespace; } // All methods export const onRequest: PagesFunction = async (context) => { return new Response('All methods'); }; // Method-specific export const onRequestGet: PagesFunction = async (context) => { const { request, env, params, data } = context; const user = await env.DB.prepare( 'SELECT * FROM users WHERE id = ?' ).bind(params.id).first(); return Response.json(user); }; export const onRequestPost: PagesFunction = async (context) => { const body = await context.request.json(); return Response.json({ success: true }); }; // Also: onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions ``` ## Context Object ```typescript interface EventContext { request: Request; // HTTP request env: Env; // Bindings (KV, D1, R2, etc.) params: Params; // Route parameters data: Data; // Middleware-shared data waitUntil: (promise: Promise) => void; // Background tasks next: () => Promise; // Next handler passThroughOnException: () => void; // Error fallback (not in advanced mode) } ``` ## Dynamic Routes ```typescript // Single segment: functions/users/[id].ts export const onRequestGet: PagesFunction = async ({ params }) => { // /users/123 → params.id = "123" return Response.json({ userId: params.id }); }; // Multi-segment: functions/files/[[path]].ts export const onRequestGet: PagesFunction = async ({ params }) => { // /files/docs/api/v1.md → params.path = ["docs", "api", "v1.md"] const filePath = (params.path as string[]).join('/'); return new Response(filePath); }; ``` ## Middleware ```typescript // functions/_middleware.ts // Single export const onRequest: PagesFunction = async (context) => { const response = await context.next(); response.headers.set('X-Custom-Header', 'value'); return response; }; // Chained (runs in order) const errorHandler: PagesFunction = async (context) => { try { return await context.next(); } catch (err) { return new Response(err.message, { status: 500 }); } }; const auth: PagesFunction = async (context) => { const token = context.request.headers.get('Authorization'); if (!token) return new Response('Unauthorized', { status: 401 }); context.data.userId = await verifyToken(token); return context.next(); }; export const onRequest = [errorHandler, auth]; ``` **Scope**: `functions/_middleware.ts` → all; `functions/api/_middleware.ts` → `/api/*` only ## Bindings Usage ```typescript export const onRequestGet: PagesFunction = async ({ env }) => { // KV const cached = await env.KV.get('key', 'json'); await env.KV.put('key', JSON.stringify({data: 'value'}), {expirationTtl: 3600}); // D1 const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); // R2, Queue, AI - see respective reference docs return Response.json({success: true}); }; ``` ## Advanced Mode Full Workers API, bypasses file-based routing: ```javascript // functions/_worker.js export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Custom routing if (url.pathname.startsWith('/api/')) { return new Response('API response'); } // REQUIRED: Serve static assets return env.ASSETS.fetch(request); } }; ``` **When to use**: WebSockets, complex routing, scheduled handlers, email handlers. ## Smart Placement Automatically optimizes function execution location based on traffic patterns. **Configuration** (in wrangler.jsonc): ```jsonc { "placement": { "mode": "smart" // Enables optimization (default: off) } } ``` **How it works**: Analyzes traffic patterns over time and places functions closer to users or data sources (e.g., D1 databases). Requires no code changes. **Trade-offs**: Initial requests may see slightly higher latency during learning period (hours-days). Performance improves as system optimizes. **When to use**: Global apps with centralized databases or geographically concentrated traffic sources. ## getRequestContext (Framework SSR) Access bindings in framework code: ```typescript // SvelteKit import type { RequestEvent } from '@sveltejs/kit'; export async function load({ platform }: RequestEvent) { const data = await platform.env.DB.prepare('SELECT * FROM users').all(); return { users: data.results }; } // Astro const { DB } = Astro.locals.runtime.env; const data = await DB.prepare('SELECT * FROM users').all(); // Solid Start (server function) import { getRequestEvent } from 'solid-js/web'; const event = getRequestEvent(); const data = await event.locals.runtime.env.DB.prepare('SELECT * FROM users').all(); ``` **✅ Supported adapters** (2026): - **SvelteKit**: `@sveltejs/adapter-cloudflare` - **Astro**: Built-in Cloudflare adapter - **Nuxt**: Set `nitro.preset: 'cloudflare-pages'` in `nuxt.config.ts` - **Qwik**: Built-in Cloudflare adapter - **Solid Start**: `@solidjs/start-cloudflare-pages` **❌ Deprecated/Unsupported**: - **Next.js**: Official adapter (`@cloudflare/next-on-pages`) deprecated. Use Vercel or self-host on Workers. - **Remix**: Official adapter (`@remix-run/cloudflare-pages`) deprecated. Migrate to supported frameworks. See [gotchas.md](./gotchas.md#framework-specific) for migration guidance. ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages/configuration.md ================================================ # Configuration ## wrangler.jsonc ```jsonc { "name": "my-pages-project", "pages_build_output_dir": "./dist", "compatibility_date": "2026-01-01", // Use current date for new projects "compatibility_flags": ["nodejs_compat"], "placement": { "mode": "smart" // Optional: Enable Smart Placement }, "kv_namespaces": [{"binding": "KV", "id": "abcd1234..."}], "d1_databases": [{"binding": "DB", "database_id": "xxxx-xxxx", "database_name": "production-db"}], "r2_buckets": [{"binding": "BUCKET", "bucket_name": "my-bucket"}], "durable_objects": {"bindings": [{"name": "COUNTER", "class_name": "Counter", "script_name": "counter-worker"}]}, "services": [{"binding": "API", "service": "api-worker"}], "queues": {"producers": [{"binding": "QUEUE", "queue": "my-queue"}]}, "vectorize": [{"binding": "VECTORIZE", "index_name": "my-index"}], "ai": {"binding": "AI"}, "analytics_engine_datasets": [{"binding": "ANALYTICS"}], "vars": {"API_URL": "https://api.example.com", "ENVIRONMENT": "production"}, "env": { "preview": { "vars": {"API_URL": "https://staging-api.example.com"}, "kv_namespaces": [{"binding": "KV", "id": "preview-namespace-id"}] } } } ``` ## Build Config **Git deployment**: Dashboard → Project → Settings → Build settings Set build command, output dir, env vars. Framework auto-detection configures automatically. ## Environment Variables ### Local (.dev.vars) ```bash # .dev.vars (never commit) SECRET_KEY="local-secret-key" API_TOKEN="dev-token-123" ``` ### Production ```bash echo "secret-value" | npx wrangler pages secret put SECRET_KEY --project-name=my-project npx wrangler pages secret list --project-name=my-project npx wrangler pages secret delete SECRET_KEY --project-name=my-project ``` Access: `env.SECRET_KEY` ## Static Config Files ### _redirects Place in build output (e.g., `dist/_redirects`): ```txt /old-page /new-page 301 # 301 redirect /blog/* /news/:splat 301 # Splat wildcard /users/:id /members/:id 301 # Placeholders /api/* /api-v2/:splat 200 # Proxy (no redirect) ``` **Limits**: 2,100 total (2,000 static + 100 dynamic), 1,000 char/line **Note**: Functions take precedence ### _headers ```txt /secure/* X-Frame-Options: DENY X-Content-Type-Options: nosniff /api/* Access-Control-Allow-Origin: * /static/* Cache-Control: public, max-age=31536000, immutable ``` **Limits**: 100 rules, 2,000 char/line **Note**: Only static assets; Functions set headers in Response ### _routes.json Controls which requests invoke Functions (auto-generated for most frameworks): ```json { "version": 1, "include": ["/*"], "exclude": ["/build/*", "/static/*", "/assets/*", "/*.{ico,png,jpg,css,js}"] } ``` **Purpose**: Functions are metered; static requests are free. `exclude` takes precedence. Max 100 rules, 100 char/rule. ## TypeScript ```bash npx wrangler types --path='./functions/types.d.ts' ``` Point `types` in `functions/tsconfig.json` to generated file. ## Smart Placement Automatically optimizes function execution location based on request patterns. ```jsonc { "placement": { "mode": "smart" // Enable optimization (default: off) } } ``` **How it works**: System analyzes traffic over hours/days and places function execution closer to: - User clusters (e.g., regional traffic) - Data sources (e.g., D1 database primary location) **Benefits**: - Lower latency for read-heavy apps with centralized databases - Better performance for apps with regional traffic patterns **Trade-offs**: - Initial learning period: First requests may be slower while system optimizes - Optimization time: Performance improves over 24-48 hours **When to enable**: Global apps with D1/Durable Objects in specific regions, or apps with concentrated geographic traffic. **When to skip**: Evenly distributed global traffic with no data locality constraints. ## Remote Bindings (Local Dev) Connect local dev server to production bindings instead of local mocks: ```bash # All bindings remote npx wrangler pages dev ./dist --remote # Specific bindings remote (others local) npx wrangler pages dev ./dist --remote --kv=KV --d1=DB ``` **Use cases**: - Test against production data (read-only operations) - Debug binding-specific behavior - Validate changes before deployment **⚠️ Warning**: - Writes affect **real production data** - Use only for read-heavy debugging or with non-production accounts - Consider creating separate preview environments instead **Requirements**: Must be logged in (`npx wrangler login`) with access to bindings. ## Local Dev ```bash # Basic npx wrangler pages dev ./dist # With bindings npx wrangler pages dev ./dist --kv KV --d1 DB=local-db-id # Remote bindings (production data) npx wrangler pages dev ./dist --remote # Persistence npx wrangler pages dev ./dist --persist-to=./.wrangler/state/v3 # Proxy mode (SSR frameworks) npx wrangler pages dev -- npm run dev ``` ## Limits (as of Jan 2026) | Resource | Free | Paid | |----------|------|------| | **Functions Requests** | 100k/day | Unlimited (metered) | | **Function CPU Time** | 10ms/req | 30ms/req (Workers Paid) | | **Function Memory** | 128MB | 128MB | | **Script Size** | 1MB compressed | 10MB compressed | | **Deployments** | 500/month | 5,000/month | | **Files per Deploy** | 20,000 | 20,000 | | **File Size** | 25MB | 25MB | | **Build Time** | 20min | 20min | | **Redirects** | 2,100 (2k static + 100 dynamic) | Same | | **Header Rules** | 100 | 100 | | **Route Rules** | 100 | 100 | | **Subrequests** | 50/request | 1,000/request (Workers Paid) | **Notes**: - Functions use Workers runtime; Workers Paid plan increases limits - Free plan sufficient for most projects - Static requests always free (not counted toward limits) [Full limits](https://developers.cloudflare.com/pages/platform/limits/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages/gotchas.md ================================================ # Gotchas ## Functions Not Running **Problem**: Function endpoints return 404 or don't execute **Causes**: `_routes.json` excludes path; wrong file extension (`.jsx`/`.tsx`); Functions dir not at output root **Solution**: Check `_routes.json`, rename to `.ts`/`.js`, verify build output structure ## 404 on Static Assets **Problem**: Static files not serving **Causes**: Build output dir misconfigured; Functions catching requests; Advanced mode missing `env.ASSETS.fetch()` **Solution**: Verify output dir, add exclusions to `_routes.json`, call `env.ASSETS.fetch()` in `_worker.js` ## Bindings Not Working **Problem**: `env.BINDING` undefined or errors **Causes**: wrangler.jsonc syntax error; wrong binding IDs; missing `.dev.vars`; out-of-sync types **Solution**: Validate config, verify IDs, create `.dev.vars`, run `npx wrangler types` ## Build Failures **Problem**: Deployment fails during build **Causes**: Wrong build command/output dir; Node version incompatibility; missing env vars; 20min timeout; OOM **Solution**: Check Dashboard → Deployments → Build log; verify settings; add `.nvmrc`; optimize build ## Middleware Not Running **Problem**: Middleware doesn't execute **Causes**: Wrong filename (not `_middleware.ts`); missing `onRequest` export; didn't call `next()` **Solution**: Rename file with underscore prefix; export handler; call `next()` or return Response ## Headers/Redirects Not Working **Problem**: `_headers` or `_redirects` not applying **Causes**: Only work for static assets; Functions override; syntax errors; exceeded limits **Solution**: Set headers in Response object for Functions; verify syntax; check limits (100 headers, 2,100 redirects) ## TypeScript Errors **Problem**: Type errors in Functions code **Causes**: Types not generated; Env interface doesn't match wrangler.jsonc **Solution**: Run `npx wrangler types --path='./functions/types.d.ts'`; update Env interface ## Local Dev Issues **Problem**: Dev server errors or bindings don't work **Causes**: Port conflict; bindings not passed; local vs HTTPS differences **Solution**: Use `--port=3000`; pass bindings via CLI or wrangler.jsonc; account for HTTP/HTTPS differences ## Performance Issues **Problem**: Slow responses or CPU limit errors **Causes**: Functions invoked for static assets; cold starts; 10ms CPU limit; large bundle **Solution**: Exclude static via `_routes.json`; optimize hot paths; keep bundle < 1MB ## Framework-Specific ### ⚠️ Deprecated Frameworks **Next.js**: Official adapter (`@cloudflare/next-on-pages`) **deprecated** and unmaintained. - **Problem**: No updates since 2024; incompatible with Next.js 15+; missing App Router features - **Cause**: Cloudflare discontinued official support; community fork exists but limited - **Solutions**: 1. **Recommended**: Use Vercel (official Next.js host) 2. **Advanced**: Self-host on Workers using custom adapter (complex, unsupported) 3. **Migration**: Switch to SvelteKit/Nuxt (similar DX, full Pages support) **Remix**: Official adapter (`@remix-run/cloudflare-pages`) **deprecated**. - **Problem**: No maintenance from Remix team; compatibility issues with Remix v2+ - **Cause**: Remix team deprecated all framework adapters - **Solutions**: 1. **Recommended**: Migrate to SvelteKit (similar file-based routing, better DX) 2. **Alternative**: Use Astro (static-first with optional SSR) 3. **Workaround**: Continue using deprecated adapter (no future support) ### ✅ Supported Frameworks **SvelteKit**: - Use `@sveltejs/adapter-cloudflare` - Access bindings via `platform.env` in server load functions - Set `platform: 'cloudflare'` in `svelte.config.js` **Astro**: - Built-in Cloudflare adapter - Access bindings via `Astro.locals.runtime.env` **Nuxt**: - Set `nitro.preset: 'cloudflare-pages'` in `nuxt.config.ts` - Access bindings via `event.context.cloudflare.env` **Qwik, Solid Start**: - Built-in or official Cloudflare adapters available - Check respective framework docs for binding access ## Debugging ```typescript // Log request details console.log('Request:', { method: request.method, url: request.url }); console.log('Env:', Object.keys(env)); console.log('Params:', params); ``` **View logs**: `npx wrangler pages deployment tail --project-name=my-project` ## Smart Placement Issues ### Increased Cold Start Latency **Problem**: First requests slower after enabling Smart Placement **Cause**: Initial optimization period while system learns traffic patterns **Solution**: Expected behavior during first 24-48 hours; monitor latency trends over time ### Inconsistent Response Times **Problem**: Latency varies significantly across requests during initial deployment **Cause**: Smart Placement testing different execution locations to find optimal placement **Solution**: Normal during learning phase; stabilizes after traffic patterns emerge (1-2 days) ### No Performance Improvement **Problem**: Smart Placement enabled but no latency reduction observed **Cause**: Traffic evenly distributed globally, or no data locality constraints **Solution**: Smart Placement most effective with centralized data (D1/DO) or regional traffic; disable if no benefit ## Remote Bindings Issues ### Accidentally Modified Production Data **Problem**: Local dev with `--remote` altered production database/KV **Cause**: Remote bindings connect directly to production resources; writes are real **Solution**: - Use `--remote` only for read-heavy debugging - Create separate preview environments for testing - Never use `--remote` for write operations during development ### Remote Binding Auth Errors **Problem**: `npx wrangler pages dev --remote` fails with "Unauthorized" or auth error **Cause**: Not logged in, session expired, or insufficient account permissions **Solution**: 1. Run `npx wrangler login` to re-authenticate 2. Verify account has access to project and bindings 3. Check binding IDs match production configuration ### Slow Local Dev with Remote Bindings **Problem**: Local dev server slow when using `--remote` **Cause**: Every request makes network calls to production bindings **Solution**: Use local bindings for development; reserve `--remote` for final validation ## Common Errors ### "Module not found" **Cause**: Dependencies not bundled or build output incorrect **Solution**: Check build output directory, ensure dependencies bundled ### "Binding not found" **Cause**: Binding not configured or types out of sync **Solution**: Verify wrangler.jsonc, run `npx wrangler types` ### "Request exceeded CPU limit" **Cause**: Code execution too slow or heavy compute **Solution**: Optimize hot paths, upgrade to Workers Paid ### "Script too large" **Cause**: Bundle size exceeds limit **Solution**: Tree-shake, use dynamic imports, code-split ### "Too many subrequests" **Cause**: Exceeded 50 subrequest limit **Solution**: Batch or reduce fetch calls ### "KV key not found" **Cause**: Key doesn't exist or wrong namespace **Solution**: Check namespace matches environment ### "D1 error" **Cause**: Wrong database_id or missing migrations **Solution**: Verify config, run `wrangler d1 migrations list` ## Limits Reference (Jan 2026) | Resource | Free | Paid | |----------|------|------| | Functions Requests | 100k/day | Unlimited | | CPU Time | 10ms/req | 30ms/req | | Memory | 128MB | 128MB | | Script Size | 1MB | 10MB | | Subrequests | 50/req | 1,000/req | | Deployments | 500/month | 5,000/month | **Tip**: Hitting CPU limit? Optimize hot paths or upgrade to Workers Paid plan. [Full limits](https://developers.cloudflare.com/pages/platform/limits/) ## Getting Help 1. Check [Pages Docs](https://developers.cloudflare.com/pages/) 2. Search [Discord #functions](https://discord.com/channels/595317990191398933/910978223968518144) 3. Review [Workers Examples](https://developers.cloudflare.com/workers/examples/) 4. Check framework-specific docs/adapters ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages/patterns.md ================================================ # Patterns ## API Routes ```typescript // functions/api/todos/[id].ts export const onRequestGet: PagesFunction = async ({ env, params }) => { const todo = await env.DB.prepare('SELECT * FROM todos WHERE id = ?').bind(params.id).first(); if (!todo) return new Response('Not found', { status: 404 }); return Response.json(todo); }; export const onRequestPut: PagesFunction = async ({ env, params, request }) => { const body = await request.json(); await env.DB.prepare('UPDATE todos SET title = ?, completed = ? WHERE id = ?') .bind(body.title, body.completed, params.id).run(); return Response.json({ success: true }); }; // Also: onRequestDelete, onRequestPost ``` ## Auth Middleware ```typescript // functions/_middleware.ts const auth: PagesFunction = async (context) => { if (context.request.url.includes('/public/')) return context.next(); const authHeader = context.request.headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return new Response('Unauthorized', { status: 401 }); } try { const payload = await verifyJWT(authHeader.substring(7), context.env.JWT_SECRET); context.data.user = payload; return context.next(); } catch (err) { return new Response('Invalid token', { status: 401 }); } }; export const onRequest = [auth]; ``` ## CORS ```typescript // functions/api/_middleware.ts const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization' }; export const onRequest: PagesFunction = async (context) => { if (context.request.method === 'OPTIONS') { return new Response(null, {headers: corsHeaders}); } const response = await context.next(); Object.entries(corsHeaders).forEach(([k, v]) => response.headers.set(k, v)); return response; }; ``` ## Form Handling ```typescript // functions/api/contact.ts export const onRequestPost: PagesFunction = async ({ request, env }) => { const formData = await request.formData(); await env.QUEUE.send({name: formData.get('name'), email: formData.get('email')}); return new Response('

Thanks!

', { headers: { 'Content-Type': 'text/html' } }); }; ``` ## Background Tasks ```typescript export const onRequestPost: PagesFunction = async ({ request, waitUntil }) => { const data = await request.json(); waitUntil(fetch('https://api.example.com/webhook', { method: 'POST', body: JSON.stringify(data) })); return Response.json({ queued: true }); }; ``` ## Error Handling ```typescript // functions/_middleware.ts const errorHandler: PagesFunction = async (context) => { try { return await context.next(); } catch (error) { console.error('Error:', error); if (context.request.url.includes('/api/')) { return Response.json({ error: error.message }, { status: 500 }); } return new Response(`

Error

${error.message}

`, { status: 500, headers: { 'Content-Type': 'text/html' } }); } }; export const onRequest = [errorHandler]; ``` ## Caching ```typescript // functions/api/data.ts export const onRequestGet: PagesFunction = async ({ env, request }) => { const cacheKey = `data:${new URL(request.url).pathname}`; const cached = await env.KV.get(cacheKey, 'json'); if (cached) return Response.json(cached, { headers: { 'X-Cache': 'HIT' } }); const data = await env.DB.prepare('SELECT * FROM data').first(); await env.KV.put(cacheKey, JSON.stringify(data), {expirationTtl: 3600}); return Response.json(data, {headers: {'X-Cache': 'MISS'}}); }; ``` ## Smart Placement for Database Apps Enable Smart Placement for apps with D1 or centralized data sources: ```jsonc // wrangler.jsonc { "name": "global-app", "placement": { "mode": "smart" }, "d1_databases": [{ "binding": "DB", "database_id": "your-db-id" }] } ``` ```typescript // functions/api/data.ts export const onRequestGet: PagesFunction = async ({ env }) => { // Smart Placement optimizes execution location over time // Balances user location vs database location const data = await env.DB.prepare('SELECT * FROM products LIMIT 10').all(); return Response.json(data); }; ``` **Best for**: Read-heavy apps with D1/Durable Objects in specific regions. **Not needed**: Apps without data locality constraints or with evenly distributed traffic. ## Framework Integration **Supported** (2026): SvelteKit, Astro, Nuxt, Qwik, Solid Start ```bash npm create cloudflare@latest my-app -- --framework=svelte ``` ### SvelteKit ```typescript // src/routes/+page.server.ts export const load = async ({ platform }) => { const todos = await platform.env.DB.prepare('SELECT * FROM todos').all(); return { todos: todos.results }; }; ``` ### Astro ```astro --- const { DB } = Astro.locals.runtime.env; const todos = await DB.prepare('SELECT * FROM todos').all(); ---
    {todos.results.map(t =>
  • {t.title}
  • )}
``` ### Nuxt ```typescript // server/api/todos.get.ts export default defineEventHandler(async (event) => { const { DB } = event.context.cloudflare.env; return await DB.prepare('SELECT * FROM todos').all(); }); ``` **⚠️ Framework Status** (2026): - ✅ **Supported**: SvelteKit, Astro, Nuxt, Qwik, Solid Start - ❌ **Deprecated**: Next.js (`@cloudflare/next-on-pages`), Remix (`@remix-run/cloudflare-pages`) For deprecated frameworks, see [gotchas.md](./gotchas.md#framework-specific) for migration options. [Framework Guides](https://developers.cloudflare.com/pages/framework-guides/) ## Monorepo Dashboard → Settings → Build → Root directory. Set to subproject (e.g., `apps/web`). ## Best Practices **Performance**: Exclude static via `_routes.json`; cache with KV; keep bundle < 1MB **Security**: Use secrets (not vars); validate inputs; rate limit with KV/DO **Workflow**: Preview per branch; local dev with `wrangler pages dev`; instant rollbacks in Dashboard ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages-functions/README.md ================================================ # Cloudflare Pages Functions Serverless functions on Cloudflare Pages using Workers runtime. Full-stack dev with file-based routing. ## Quick Navigation **Need to...** | Task | Go to | |------|-------| | Set up TypeScript types | [configuration.md](./configuration.md) - TypeScript Setup | | Configure bindings (KV, D1, R2) | [configuration.md](./configuration.md) - wrangler.jsonc | | Access request/env/params | [api.md](./api.md) - EventContext | | Add middleware or auth | [patterns.md](./patterns.md) - Middleware, Auth | | Background tasks (waitUntil) | [patterns.md](./patterns.md) - Background Tasks | | Debug errors or check limits | [gotchas.md](./gotchas.md) - Common Errors, Limits | ## Decision Tree: Is This Pages Functions? ``` Need serverless backend? ├─ Yes, for a static site → Pages Functions ├─ Yes, standalone API → Workers └─ Just static hosting → Pages (no functions) Have existing Worker? ├─ Complex routing logic → Use _worker.js (Advanced Mode) └─ Simple routes → Migrate to /functions (File-Based) Framework-based? ├─ Next.js/SvelteKit/Remix → Uses _worker.js automatically └─ Vanilla/HTML/React SPA → Use /functions ``` ## File-Based Routing ``` /functions ├── index.js → / ├── api.js → /api ├── users/ │ ├── index.js → /users/ │ ├── [user].js → /users/:user │ └── [[catchall]].js → /users/* └── _middleware.js → runs on all routes ``` **Rules:** - `index.js` → directory root - Trailing slash optional - Specific routes precede catch-alls - Falls back to static if no match ## Dynamic Routes **Single segment** `[param]` → string: ```js // /functions/users/[user].js export function onRequest(context) { return new Response(`Hello ${context.params.user}`); } // Matches: /users/nevi ``` **Multi-segment** `[[param]]` → array: ```js // /functions/users/[[catchall]].js export function onRequest(context) { return new Response(JSON.stringify(context.params.catchall)); } // Matches: /users/nevi/foobar → ["nevi", "foobar"] ``` ## Key Features - **Method handlers:** `onRequestGet`, `onRequestPost`, etc. - **Middleware:** `_middleware.js` for cross-cutting concerns - **Bindings:** KV, D1, R2, Durable Objects, Workers AI, Service bindings - **TypeScript:** Full type support via `wrangler types` command - **Advanced mode:** Use `_worker.js` for custom routing logic ## Reading Order **New to Pages Functions?** Start here: 1. [README.md](./README.md) - Overview, routing, decision tree (you are here) 2. [configuration.md](./configuration.md) - TypeScript setup, wrangler.jsonc, bindings 3. [api.md](./api.md) - EventContext, handlers, bindings reference 4. [patterns.md](./patterns.md) - Middleware, auth, CORS, rate limiting, caching 5. [gotchas.md](./gotchas.md) - Common errors, debugging, limits **Quick reference lookup:** - Bindings table → [api.md](./api.md) - Error diagnosis → [gotchas.md](./gotchas.md) - TypeScript setup → [configuration.md](./configuration.md) ## See Also - [pages](../pages/) - Pages platform overview and static site deployment - [workers](../workers/) - Workers runtime API reference - [d1](../d1/) - D1 database integration with Pages Functions ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages-functions/api.md ================================================ # Function API ## EventContext ```typescript interface EventContext { request: Request; // Incoming request functionPath: string; // Request path waitUntil(promise: Promise): void; // Background tasks (non-blocking) passThroughOnException(): void; // Fallback to static on error next(input?: Request | string, init?: RequestInit): Promise; env: Env; // Bindings, vars, secrets params: Record; // Route params ([user] or [[catchall]]) data: any; // Middleware shared state } ``` **TypeScript:** See [configuration.md](./configuration.md) for `wrangler types` setup ## Handlers ```typescript // Generic (fallback for any method) export async function onRequest(ctx: EventContext): Promise { return new Response('Any method'); } // Method-specific (takes precedence over generic) export async function onRequestGet(ctx: EventContext): Promise { return Response.json({ message: 'GET' }); } export async function onRequestPost(ctx: EventContext): Promise { const body = await ctx.request.json(); return Response.json({ received: body }); } // Also: onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions ``` ## Bindings Reference | Binding Type | Interface | Config Key | Use Case | |--------------|-----------|------------|----------| | KV | `KVNamespace` | `kv_namespaces` | Key-value cache, sessions, config | | D1 | `D1Database` | `d1_databases` | Relational data, SQL queries | | R2 | `R2Bucket` | `r2_buckets` | Large files, user uploads, assets | | Durable Objects | `DurableObjectNamespace` | `durable_objects.bindings` | Stateful coordination, websockets | | Workers AI | `Ai` | `ai.binding` | LLM inference, embeddings | | Vectorize | `VectorizeIndex` | `vectorize` | Vector search, embeddings | | Service Binding | `Fetcher` | `services` | Worker-to-worker RPC | | Analytics Engine | `AnalyticsEngineDataset` | `analytics_engine_datasets` | Event logging, metrics | | Environment Vars | `string` | `vars` | Non-sensitive config | See [configuration.md](./configuration.md) for wrangler.jsonc examples. ## Bindings ### KV ```typescript interface Env { KV: KVNamespace; } export const onRequest: PagesFunction = async (ctx) => { await ctx.env.KV.put('key', 'value', { expirationTtl: 3600 }); const val = await ctx.env.KV.get('key', { type: 'json' }); const keys = await ctx.env.KV.list({ prefix: 'user:' }); return Response.json({ val }); }; ``` ### D1 ```typescript interface Env { DB: D1Database; } export const onRequest: PagesFunction = async (ctx) => { const user = await ctx.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(123).first(); return Response.json(user); }; ``` ### R2 ```typescript interface Env { BUCKET: R2Bucket; } export const onRequest: PagesFunction = async (ctx) => { const obj = await ctx.env.BUCKET.get('file.txt'); if (!obj) return new Response('Not found', { status: 404 }); await ctx.env.BUCKET.put('file.txt', ctx.request.body); return new Response(obj.body); }; ``` ### Durable Objects ```typescript interface Env { COUNTER: DurableObjectNamespace; } export const onRequest: PagesFunction = async (ctx) => { const stub = ctx.env.COUNTER.get(ctx.env.COUNTER.idFromName('global')); return stub.fetch(ctx.request); }; ``` ### Workers AI ```typescript interface Env { AI: Ai; } export const onRequest: PagesFunction = async (ctx) => { const resp = await ctx.env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' }); return Response.json(resp); }; ``` ### Service Bindings & Env Vars ```typescript interface Env { AUTH: Fetcher; API_KEY: string; } export const onRequest: PagesFunction = async (ctx) => { // Service binding: forward to another Worker return ctx.env.AUTH.fetch(ctx.request); // Environment variable return Response.json({ key: ctx.env.API_KEY }); }; ``` ## Advanced Mode (env.ASSETS) When using `_worker.js`, access static assets via `env.ASSETS.fetch()`: ```typescript interface Env { ASSETS: Fetcher; KV: KVNamespace; } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname.startsWith('/api/')) { return Response.json({ data: await env.KV.get('key') }); } return env.ASSETS.fetch(request); // Fallback to static } } satisfies ExportedHandler; ``` **See also:** [configuration.md](./configuration.md) for TypeScript setup and wrangler.jsonc | [patterns.md](./patterns.md) for middleware and auth patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages-functions/configuration.md ================================================ # Configuration ## TypeScript Setup **Generate types from wrangler.jsonc** (replaces deprecated `@cloudflare/workers-types`): ```bash npx wrangler types ``` Creates `worker-configuration.d.ts` with typed `Env` interface based on your bindings. ```typescript // functions/api.ts export const onRequest: PagesFunction = async (ctx) => { // ctx.env.KV, ctx.env.DB, etc. are fully typed return Response.json({ ok: true }); }; ``` **Manual types** (if not using wrangler types): ```typescript interface Env { KV: KVNamespace; DB: D1Database; API_KEY: string; } export const onRequest: PagesFunction = async (ctx) => { /* ... */ }; ``` ## wrangler.jsonc ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-pages-app", "pages_build_output_dir": "./dist", "compatibility_date": "2025-01-01", "compatibility_flags": ["nodejs_compat"], "vars": { "API_URL": "https://api.example.com" }, "kv_namespaces": [{ "binding": "KV", "id": "abc123" }], "d1_databases": [{ "binding": "DB", "database_name": "prod-db", "database_id": "xyz789" }], "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-bucket" }], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter", "script_name": "counter-worker" }] }, "services": [{ "binding": "AUTH", "service": "auth-worker" }], "ai": { "binding": "AI" }, "vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }], "analytics_engine_datasets": [{ "binding": "ANALYTICS" }] } ``` ## Environment Overrides Top-level → local dev, `env.preview` → preview, `env.production` → production ```jsonc { "vars": { "API_URL": "http://localhost:8787" }, "env": { "production": { "vars": { "API_URL": "https://api.example.com" } } } } ``` **Note:** If overriding `vars`, `kv_namespaces`, `d1_databases`, etc., ALL must be redefined (non-inheritable) ## Local Secrets (.dev.vars) **Local dev only** - NOT deployed: ```bash # .dev.vars (add to .gitignore) SECRET_KEY="my-secret-value" ``` Accessed via `ctx.env.SECRET_KEY`. Set production secrets: ```bash echo "value" | npx wrangler pages secret put SECRET_KEY --project-name=my-app ``` ## Static Config Files **_routes.json** - Custom routing: ```json { "version": 1, "include": ["/api/*"], "exclude": ["/static/*"] } ``` **_headers** - Static headers: ``` /static/* Cache-Control: public, max-age=31536000 ``` **_redirects** - Redirects: ``` /old /new 301 ``` ## Local Dev & Deployment ```bash # Dev server npx wrangler pages dev ./dist # With bindings npx wrangler pages dev ./dist --kv=KV --d1=DB=db-id --r2=BUCKET # Durable Objects (2 terminals) cd do-worker && npx wrangler dev cd pages-project && npx wrangler pages dev ./dist --do COUNTER=Counter@do-worker # Deploy npx wrangler pages deploy ./dist npx wrangler pages deploy ./dist --branch preview # Download config npx wrangler pages download config my-project ``` **See also:** [api.md](./api.md) for binding usage examples ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages-functions/gotchas.md ================================================ # Gotchas & Debugging ## Error Diagnosis | Symptom | Likely Cause | Solution | |---------|--------------|----------| | **Function not invoking** | Wrong `/functions` location, wrong extension, or `_routes.json` excludes path | Check `pages_build_output_dir`, use `.js`/`.ts`, verify `_routes.json` | | **`ctx.env.BINDING` undefined** | Binding not configured or name mismatch | Add to `wrangler.jsonc`, verify exact name (case-sensitive), redeploy | | **TypeScript errors on `ctx.env`** | Missing type definition | Run `wrangler types` or define `interface Env {}` | | **Middleware not running** | Wrong filename/location or missing `ctx.next()` | Name exactly `_middleware.js`, export `onRequest`, call `ctx.next()` | | **Secrets missing in production** | `.dev.vars` not deployed | `.dev.vars` is local only - set production secrets via dashboard or `wrangler secret put` | | **Type mismatch on binding** | Wrong interface type | See [api.md](./api.md) bindings table for correct types | | **"KV key not found" but exists** | Key in wrong namespace or env | Verify namespace binding, check preview vs production env | | **Function times out** | Synchronous wait or missing `await` | All I/O must be async/await, use `ctx.waitUntil()` for background tasks | ## Common Errors ### TypeScript type errors **Problem:** `ctx.env.MY_BINDING` shows type error **Cause:** No type definition for `Env` **Solution:** Run `npx wrangler types` or manually define: ```typescript interface Env { MY_BINDING: KVNamespace; } export const onRequest: PagesFunction = async (ctx) => { /* ... */ }; ``` ### Secrets not available in production **Problem:** `ctx.env.SECRET_KEY` is undefined in production **Cause:** `.dev.vars` is local-only, not deployed **Solution:** Set production secrets: ```bash echo "value" | npx wrangler pages secret put SECRET_KEY --project-name=my-app ``` ## Debugging ```typescript // Console logging export async function onRequest(ctx) { console.log('Request:', ctx.request.method, ctx.request.url); const res = await ctx.next(); console.log('Status:', res.status); return res; } ``` ```bash # Stream real-time logs npx wrangler pages deployment tail npx wrangler pages deployment tail --status error ``` ```jsonc // Source maps (wrangler.jsonc) { "upload_source_maps": true } ``` ## Limits | Resource | Free | Paid | |----------|------|------| | CPU time | 10ms | 50ms | | Memory | 128 MB | 128 MB | | Script size | 10 MB compressed | 10 MB compressed | | Env vars | 5 KB per var, 64 max | 5 KB per var, 64 max | | Requests | 100k/day | Unlimited ($0.50/million) | ## Best Practices **Performance:** Minimize deps (cold start), use KV for cache/D1 for relational/R2 for large files, set `Cache-Control` headers, batch DB ops, handle errors gracefully **Security:** Never commit secrets (use `.dev.vars` + gitignore), validate input, sanitize before DB, implement auth middleware, set CORS headers, rate limit per-IP ## Migration **Workers → Pages Functions:** - `export default { fetch(req, env) {} }` → `export function onRequest(ctx) { const { request, env } = ctx; }` - Use `_worker.js` for complex routing: `env.ASSETS.fetch(request)` for static files **Other platforms → Pages:** - File-based routing: `/functions/api/users.js` → `/api/users` - Dynamic routes: `[param]` not `:param` - Replace Node.js deps with Workers APIs or add `nodejs_compat` flag ## Resources - [Official Docs](https://developers.cloudflare.com/pages/functions/) - [Workers APIs](https://developers.cloudflare.com/workers/runtime-apis/) - [Examples](https://github.com/cloudflare/pages-example-projects) - [Discord](https://discord.gg/cloudflaredev) **See also:** [configuration.md](./configuration.md) for TypeScript setup | [patterns.md](./patterns.md) for middleware/auth | [api.md](./api.md) for bindings ================================================ FILE: skills/.curated/cloudflare-deploy/references/pages-functions/patterns.md ================================================ # Common Patterns ## Background Tasks (waitUntil) Non-blocking tasks after response sent (analytics, cleanup, webhooks): ```typescript export async function onRequest(ctx: EventContext) { const res = Response.json({ success: true }); ctx.waitUntil(ctx.env.KV.put('last-visit', new Date().toISOString())); ctx.waitUntil(Promise.all([ ctx.env.ANALYTICS.writeDataPoint({ event: 'view' }), fetch('https://webhook.site/...', { method: 'POST' }) ])); return res; // Returned immediately } ``` ## Middleware & Auth ```typescript // functions/_middleware.js (global) or functions/users/_middleware.js (scoped) export async function onRequest(ctx) { try { return await ctx.next(); } catch (err) { return new Response(err.message, { status: 500 }); } } // Chained: export const onRequest = [errorHandler, auth, logger]; // Auth async function auth(ctx: EventContext) { const token = ctx.request.headers.get('authorization')?.replace('Bearer ', ''); if (!token) return new Response('Unauthorized', { status: 401 }); const session = await ctx.env.KV.get(`session:${token}`); if (!session) return new Response('Invalid', { status: 401 }); ctx.data.user = JSON.parse(session); return ctx.next(); } ``` ## CORS & Rate Limiting ```typescript // CORS middleware const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST' }; export async function onRequestOptions() { return new Response(null, { headers: cors }); } export async function onRequest(ctx) { const res = await ctx.next(); Object.entries(cors).forEach(([k, v]) => res.headers.set(k, v)); return res; } // Rate limiting (KV-based) async function rateLimit(ctx: EventContext) { const ip = ctx.request.headers.get('CF-Connecting-IP') || 'unknown'; const count = parseInt(await ctx.env.KV.get(`rate:${ip}`) || '0'); if (count >= 100) return new Response('Rate limited', { status: 429 }); await ctx.env.KV.put(`rate:${ip}`, (count + 1).toString(), { expirationTtl: 3600 }); return ctx.next(); } ``` ## Forms, Caching, Redirects ```typescript // JSON & file upload export async function onRequestPost(ctx) { const ct = ctx.request.headers.get('content-type') || ''; if (ct.includes('application/json')) return Response.json(await ctx.request.json()); if (ct.includes('multipart/form-data')) { const file = (await ctx.request.formData()).get('file') as File; await ctx.env.BUCKET.put(file.name, file.stream()); return Response.json({ uploaded: file.name }); } } // Cache API export async function onRequest(ctx) { let res = await caches.default.match(ctx.request); if (!res) { res = new Response('Data'); res.headers.set('Cache-Control', 'public, max-age=3600'); ctx.waitUntil(caches.default.put(ctx.request, res.clone())); } return res; } // Redirects export async function onRequest(ctx) { if (new URL(ctx.request.url).pathname === '/old') { return Response.redirect(new URL('/new', ctx.request.url), 301); } return ctx.next(); } ``` ## Testing **Unit tests** (Vitest + cloudflare:test): ```typescript import { env } from 'cloudflare:test'; import { it, expect } from 'vitest'; import { onRequest } from '../functions/api'; it('returns JSON', async () => { const req = new Request('http://localhost/api'); const ctx = { request: req, env, params: {}, data: {} } as EventContext; const res = await onRequest(ctx); expect(res.status).toBe(200); }); ``` **Integration:** `wrangler pages dev` + Playwright/Cypress ## Advanced Mode (_worker.js) Use `_worker.js` for complex routing (replaces `/functions`): ```typescript interface Env { ASSETS: Fetcher; KV: KVNamespace; } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname.startsWith('/api/')) { return Response.json({ data: await env.KV.get('key') }); } return env.ASSETS.fetch(request); // Static files } } satisfies ExportedHandler; ``` **When:** Existing Worker, framework-generated (Next.js/SvelteKit), custom routing logic **See also:** [api.md](./api.md) for `env.ASSETS.fetch()` | [gotchas.md](./gotchas.md) for debugging ================================================ FILE: skills/.curated/cloudflare-deploy/references/pipelines/README.md ================================================ # Cloudflare Pipelines ETL streaming platform for ingesting, transforming, and loading data into R2 with SQL transformations. ## Overview Pipelines provides: - **Streams**: Durable event buffers (HTTP/Workers ingestion) - **Pipelines**: SQL-based transformations - **Sinks**: R2 destinations (Iceberg tables or Parquet/JSON files) **Status**: Open beta (Workers Paid plan) **Pricing**: No charge beyond standard R2 storage/operations ## Architecture ``` Data Sources → Streams → Pipelines (SQL) → Sinks → R2 ↑ ↓ ↓ HTTP/Workers Transform Iceberg/Parquet ``` | Component | Purpose | Key Feature | |-----------|---------|-------------| | Streams | Event ingestion | Structured (validated) or unstructured | | Pipelines | Transform with SQL | Immutable after creation | | Sinks | Write to R2 | Exactly-once delivery | ## Quick Start ```bash # Interactive setup (recommended) npx wrangler pipelines setup ``` **Minimal Worker example:** ```typescript interface Env { STREAM: Pipeline; } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const event = { user_id: "123", event_type: "purchase", amount: 29.99 }; // Fire-and-forget pattern ctx.waitUntil(env.STREAM.send([event])); return new Response('OK'); } } satisfies ExportedHandler; ``` ## Which Sink Type? ``` Need SQL queries on data? → R2 Data Catalog (Iceberg) ✅ ACID transactions, time-travel, schema evolution ❌ More setup complexity (namespace, table, catalog token) Just file storage/archival? → R2 Storage (Parquet) ✅ Simple, direct file access ❌ No built-in SQL queries Using external tools (Spark/Athena)? → R2 Storage (Parquet with partitioning) ✅ Standard format, partition pruning for performance ❌ Must manage schema compatibility yourself ``` ## Common Use Cases - **Analytics pipelines**: Clickstream, telemetry, server logs - **Data warehousing**: ETL into queryable Iceberg tables - **Event processing**: Mobile/IoT with enrichment - **Ecommerce analytics**: User events, purchases, views ## Reading Order **New to Pipelines?** Start here: 1. [configuration.md](./configuration.md) - Setup streams, sinks, pipelines 2. [api.md](./api.md) - Send events, TypeScript types, SQL functions 3. [patterns.md](./patterns.md) - Best practices, integrations, complete example 4. [gotchas.md](./gotchas.md) - Critical warnings, troubleshooting **Task-based routing:** - Setup pipeline → [configuration.md](./configuration.md) - Send/query data → [api.md](./api.md) - Implement pattern → [patterns.md](./patterns.md) - Debug issue → [gotchas.md](./gotchas.md) ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc bindings, schema definition, sink options, CLI commands - [api.md](./api.md) - Pipeline binding interface, send() method, HTTP ingest, SQL function reference - [patterns.md](./patterns.md) - Fire-and-forget, schema validation with Zod, integrations, performance tuning - [gotchas.md](./gotchas.md) - Silent validation failures, immutable pipelines, latency expectations, limits ## See Also - [r2](../r2/) - R2 storage backend for sinks - [queues](../queues/) - Compare with Queues for async processing - [workers](../workers/) - Worker runtime for event ingestion ================================================ FILE: skills/.curated/cloudflare-deploy/references/pipelines/api.md ================================================ # Pipelines API Reference ## Pipeline Binding Interface ```typescript // From @cloudflare/workers-types interface Pipeline { send(data: object | object[]): Promise; } interface Env { STREAM: Pipeline; } export default { async fetch(request: Request, env: Env): Promise { // send() returns Promise - no result data await env.STREAM.send([event]); return new Response('OK'); } } satisfies ExportedHandler; ``` **Key points:** - `send()` accepts single object or array - Always returns `Promise` (no confirmation data) - Throws on network/validation errors (wrap in try/catch) - Use `ctx.waitUntil()` for fire-and-forget pattern ## Writing Events ### Single Event ```typescript await env.STREAM.send([{ user_id: "12345", event_type: "purchase", product_id: "widget-001", amount: 29.99 }]); ``` ### Batch Events ```typescript const events = [ { user_id: "user1", event_type: "view" }, { user_id: "user2", event_type: "purchase", amount: 50 } ]; await env.STREAM.send(events); ``` **Limits:** - Max 1 MB per request - 5 MB/s per stream ### Fire-and-Forget Pattern ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const event = { /* ... */ }; // Don't block response on send ctx.waitUntil(env.STREAM.send([event])); return new Response('OK'); } }; ``` ### Error Handling ```typescript try { await env.STREAM.send([event]); } catch (error) { console.error('Pipeline send failed:', error); // Log to another system, retry, or return error response return new Response('Failed to track event', { status: 500 }); } ``` ## HTTP Ingest API ### Endpoint Format ``` https://{stream-id}.ingest.cloudflare.com ``` Get `{stream-id}` from: `npx wrangler pipelines streams list` ### Request Format **CRITICAL:** Must send array, not single object ```bash # ✅ Correct curl -X POST https://{stream-id}.ingest.cloudflare.com \ -H "Content-Type: application/json" \ -d '[{"user_id": "123", "event_type": "purchase"}]' # ❌ Wrong - will fail curl -X POST https://{stream-id}.ingest.cloudflare.com \ -H "Content-Type: application/json" \ -d '{"user_id": "123", "event_type": "purchase"}' ``` ### Authentication ```bash curl -X POST https://{stream-id}.ingest.cloudflare.com \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -d '[{"event": "data"}]' ``` **Required permission:** Workers Pipeline Send Create token: Dashboard → Workers → API tokens → Create with Pipeline Send permission ### Response Codes | Code | Meaning | Action | |------|---------|--------| | 200 | Accepted | Success | | 400 | Invalid format | Check JSON array, schema match | | 401 | Auth failed | Verify token valid | | 413 | Payload too large | Split into smaller batches (<1 MB) | | 429 | Rate limited | Back off, retry with delay | | 5xx | Server error | Retry with exponential backoff | ## SQL Functions Quick Reference Available in `INSERT INTO sink SELECT ... FROM stream` transformations: | Function | Example | Use Case | |----------|---------|----------| | `UPPER(s)` | `UPPER(event_type)` | Normalize strings | | `LOWER(s)` | `LOWER(email)` | Case-insensitive matching | | `CONCAT(...)` | `CONCAT(user_id, '_', product_id)` | Generate composite keys | | `CASE WHEN ... THEN ... END` | `CASE WHEN amount > 100 THEN 'high' ELSE 'low' END` | Conditional enrichment | | `CAST(x AS type)` | `CAST(timestamp AS string)` | Type conversion | | `COALESCE(x, y)` | `COALESCE(amount, 0.0)` | Default values | | Math operators | `amount * 1.1`, `price / quantity` | Calculations | | Comparison | `amount > 100`, `status IN ('active', 'pending')` | Filtering | **String types for CAST:** `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp` Full reference: [Pipelines SQL Reference](https://developers.cloudflare.com/pipelines/sql-reference/) ## SQL Transform Examples ### Filter Events ```sql INSERT INTO my_sink SELECT * FROM my_stream WHERE event_type = 'purchase' AND amount > 100 ``` ### Select Specific Fields ```sql INSERT INTO my_sink SELECT user_id, event_type, timestamp, amount FROM my_stream ``` ### Transform and Enrich ```sql INSERT INTO my_sink SELECT user_id, UPPER(event_type) as event_type, timestamp, amount * 1.1 as amount_with_tax, CONCAT(user_id, '_', product_id) as unique_key, CASE WHEN amount > 1000 THEN 'high_value' WHEN amount > 100 THEN 'medium_value' ELSE 'low_value' END as customer_tier FROM my_stream WHERE event_type IN ('purchase', 'refund') ``` ## Querying Results (R2 Data Catalog) ```bash export WRANGLER_R2_SQL_AUTH_TOKEN=YOUR_CATALOG_TOKEN npx wrangler r2 sql query "warehouse_name" " SELECT event_type, COUNT(*) as event_count, SUM(amount) as total_revenue FROM default.my_table WHERE event_type = 'purchase' AND timestamp >= '2025-01-01' GROUP BY event_type ORDER BY total_revenue DESC LIMIT 100" ``` **Note:** Iceberg tables support standard SQL queries with GROUP BY, JOINs, WHERE, ORDER BY, etc. ================================================ FILE: skills/.curated/cloudflare-deploy/references/pipelines/configuration.md ================================================ # Pipelines Configuration ## Worker Binding ```jsonc // wrangler.jsonc { "pipelines": [ { "pipeline": "", "binding": "STREAM" } ] } ``` Get stream ID: `npx wrangler pipelines streams list` ## Schema (Structured Streams) ```json { "fields": [ { "name": "user_id", "type": "string", "required": true }, { "name": "event_type", "type": "string", "required": true }, { "name": "amount", "type": "float64", "required": false }, { "name": "timestamp", "type": "timestamp", "required": true } ] } ``` **Types:** `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp`, `json`, `binary`, `list`, `struct` ## Stream Setup ```bash # With schema npx wrangler pipelines streams create my-stream --schema-file schema.json # Unstructured (no validation) npx wrangler pipelines streams create my-stream # List/get/delete npx wrangler pipelines streams list npx wrangler pipelines streams get npx wrangler pipelines streams delete ``` ## Sink Configuration **R2 Data Catalog (Iceberg):** ```bash npx wrangler pipelines sinks create my-sink \ --type r2-data-catalog \ --bucket my-bucket --namespace default --table events \ --catalog-token $TOKEN \ --compression zstd --roll-interval 60 ``` **R2 Raw (Parquet):** ```bash npx wrangler pipelines sinks create my-sink \ --type r2 --bucket my-bucket --format parquet \ --path analytics/events \ --partitioning "year=%Y/month=%m/day=%d" \ --access-key-id $KEY --secret-access-key $SECRET ``` | Option | Values | Guidance | |--------|--------|----------| | `--compression` | `zstd`, `snappy`, `gzip` | `zstd` best ratio, `snappy` fastest | | `--roll-interval` | Seconds | Low latency: 10-60, Query perf: 300 | | `--roll-size` | MB | Larger = better compression | ## Pipeline Creation ```bash npx wrangler pipelines create my-pipeline \ --sql "INSERT INTO my_sink SELECT * FROM my_stream WHERE event_type = 'purchase'" ``` **⚠️ Pipelines are immutable** - cannot modify SQL. Must delete/recreate. ## Credentials | Type | Permission | Get From | |------|------------|----------| | Catalog token | R2 Admin Read & Write | Dashboard → R2 → API tokens | | R2 credentials | Object Read & Write | `wrangler r2 bucket create` output | | HTTP ingest token | Workers Pipeline Send | Dashboard → Workers → API tokens | ## Complete Example ```bash npx wrangler r2 bucket create my-bucket npx wrangler r2 bucket catalog enable my-bucket npx wrangler pipelines streams create my-stream --schema-file schema.json npx wrangler pipelines sinks create my-sink --type r2-data-catalog --bucket my-bucket ... npx wrangler pipelines create my-pipeline --sql "INSERT INTO my_sink SELECT * FROM my_stream" npx wrangler deploy ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/pipelines/gotchas.md ================================================ # Pipelines Gotchas ## Critical Issues ### Events Silently Dropped **Most common issue.** Events accepted (HTTP 200) but never appear in sink. **Causes:** 1. Schema validation fails - structured streams drop invalid events silently 2. Waiting for roll interval (10-300s) - expected behavior **Solution:** Validate client-side with Zod: ```typescript const EventSchema = z.object({ user_id: z.string(), amount: z.number() }); try { const validated = EventSchema.parse(rawEvent); await env.STREAM.send([validated]); } catch (e) { /* get immediate feedback */ } ``` ### Pipelines Are Immutable Cannot modify SQL after creation. Must delete and recreate. ```bash npx wrangler pipelines delete old-pipeline npx wrangler pipelines create new-pipeline --sql "..." ``` **Tip:** Use version naming (`events-pipeline-v1`) and keep SQL in version control. ### Worker Binding Not Found **`env.STREAM is undefined`** 1. Use **stream ID** (not pipeline ID) in `wrangler.jsonc` 2. Redeploy after adding binding ```bash npx wrangler pipelines streams list # Get stream ID npx wrangler deploy ``` ## Common Errors | Error | Cause | Fix | |-------|-------|-----| | Events not in R2 | Roll interval not elapsed | Wait 10-300s, check `roll_interval` | | Schema validation failures | Type mismatch, missing fields | Validate client-side | | Rate limit (429) | >5 MB/s per stream | Batch events, request increase | | Payload too large (413) | >1 MB request | Split into smaller batches | | Cannot delete stream | Pipeline references it | Delete pipelines first | | Sink credential errors | Token expired | Recreate sink with new credentials | ## Limits (Open Beta) | Resource | Limit | |----------|-------| | Streams/Sinks/Pipelines per account | 20 each | | Payload size | 1 MB | | Ingest rate per stream | 5 MB/s | | Event retention | 24 hours | | Recommended batch size | 100 events | ## SQL Limitations - **No JOINs** - single stream per pipeline - **No window functions** - basic SQL only - **No subqueries** - must use `INSERT INTO ... SELECT ... FROM` - **No schema evolution** - cannot modify after creation ## Debug Checklist - [ ] Stream exists: `npx wrangler pipelines streams list` - [ ] Pipeline healthy: `npx wrangler pipelines get ` - [ ] SQL syntax matches schema - [ ] Worker redeployed after binding added - [ ] Waited for roll interval - [ ] Accepted vs processed count matches (no validation drops) ================================================ FILE: skills/.curated/cloudflare-deploy/references/pipelines/patterns.md ================================================ # Pipelines Patterns ## Fire-and-Forget ```typescript export default { async fetch(request, env, ctx) { const event = { user_id: '...', event_type: 'page_view', timestamp: new Date().toISOString() }; ctx.waitUntil(env.STREAM.send([event])); // Don't block response return new Response('OK'); } }; ``` ## Schema Validation with Zod ```typescript import { z } from 'zod'; const EventSchema = z.object({ user_id: z.string(), event_type: z.enum(['purchase', 'view']), amount: z.number().positive().optional() }); const validated = EventSchema.parse(rawEvent); // Throws on invalid await env.STREAM.send([validated]); ``` **Why:** Structured streams drop invalid events silently. Client validation gives immediate feedback. ## SQL Transform Patterns ```sql -- Filter early (reduce storage) INSERT INTO my_sink SELECT user_id, event_type, amount FROM my_stream WHERE event_type = 'purchase' AND amount > 10 -- Select only needed fields INSERT INTO my_sink SELECT user_id, event_type, timestamp FROM my_stream -- Enrich with CASE INSERT INTO my_sink SELECT user_id, amount, CASE WHEN amount > 1000 THEN 'vip' ELSE 'standard' END as tier FROM my_stream ``` ## Pipelines + Queues Fan-out ```typescript await Promise.all([ env.ANALYTICS_STREAM.send([event]), // Long-term storage env.PROCESS_QUEUE.send(event) // Immediate processing ]); ``` | Need | Use | |------|-----| | Long-term storage, SQL queries | Pipelines | | Immediate processing, retries | Queues | | Both | Fan-out pattern | ## Performance Tuning | Goal | Config | |------|--------| | Low latency | `--roll-interval 10` | | Query performance | `--roll-interval 300 --roll-size 100` | | Cost optimal | `--compression zstd --roll-interval 300` | ## Schema Evolution Pipelines are immutable. Use versioning: ```bash # Create v2 stream/sink/pipeline npx wrangler pipelines streams create events-v2 --schema-file v2.json # Dual-write during transition await Promise.all([env.EVENTS_V1.send([event]), env.EVENTS_V2.send([event])]); # Query across versions with UNION ALL ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/pulumi/README.md ================================================ # Cloudflare Pulumi Provider Expert guidance for Cloudflare Pulumi Provider (@pulumi/cloudflare). ## Overview Programmatic management of Cloudflare resources: Workers, Pages, D1, KV, R2, DNS, Queues, etc. **Packages:** - TypeScript/JS: `@pulumi/cloudflare` - Python: `pulumi-cloudflare` - Go: `github.com/pulumi/pulumi-cloudflare/sdk/v6/go/cloudflare` - .NET: `Pulumi.Cloudflare` **Version:** v6.x ## Core Principles 1. Use API tokens (not legacy API keys) 2. Store accountId in stack config 3. Match binding names across code/config 4. Use `module: true` for ES modules 5. Set `compatibilityDate` to lock behavior ## Authentication ```typescript import * as cloudflare from "@pulumi/cloudflare"; // API Token (recommended): CLOUDFLARE_API_TOKEN env const provider = new cloudflare.Provider("cf", { apiToken: process.env.CLOUDFLARE_API_TOKEN }); // API Key (legacy): CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL env const provider = new cloudflare.Provider("cf", { apiKey: process.env.CLOUDFLARE_API_KEY, email: process.env.CLOUDFLARE_EMAIL }); // API User Service Key: CLOUDFLARE_API_USER_SERVICE_KEY env const provider = new cloudflare.Provider("cf", { apiUserServiceKey: process.env.CLOUDFLARE_API_USER_SERVICE_KEY }); ``` ## Setup **Pulumi.yaml:** ```yaml name: my-cloudflare-app runtime: nodejs config: cloudflare:apiToken: value: ${CLOUDFLARE_API_TOKEN} ``` **Pulumi..yaml:** ```yaml config: cloudflare:accountId: "abc123..." ``` **index.ts:** ```typescript import * as pulumi from "@pulumi/pulumi"; import * as cloudflare from "@pulumi/cloudflare"; const accountId = new pulumi.Config("cloudflare").require("accountId"); ``` ## Common Resource Types - `Provider` - Provider config - `WorkerScript` - Worker - `WorkersKvNamespace` - KV - `R2Bucket` - R2 - `D1Database` - D1 - `Queue` - Queue - `PagesProject` - Pages - `DnsRecord` - DNS - `WorkerRoute` - Worker route - `WorkersDomain` - Custom domain ## Key Properties - `accountId` - Required for most resources - `zoneId` - Required for DNS/domain - `name`/`title` - Resource identifier - `*Bindings` - Connect resources to Workers ## Reading Order | Order | File | What | When to Read | |-------|------|------|--------------| | 1 | [configuration.md](./configuration.md) | Resource config for Workers/KV/D1/R2/Queues/Pages | First time setup, resource reference | | 2 | [patterns.md](./patterns.md) | Architecture patterns, multi-env, component resources | Building complex apps, best practices | | 3 | [api.md](./api.md) | Outputs, dependencies, imports, dynamic providers | Advanced features, integrations | | 4 | [gotchas.md](./gotchas.md) | Common errors, troubleshooting, limits | Debugging, deployment issues | ## In This Reference - [configuration.md](./configuration.md) - Provider config, stack setup, Workers/bindings - [api.md](./api.md) - Resource types, Workers script, KV/D1/R2/queues/Pages - [patterns.md](./patterns.md) - Multi-env, secrets, CI/CD, stack management - [gotchas.md](./gotchas.md) - State issues, deployment failures, limits ## See Also - [terraform](../terraform/) - Alternative IaC for Cloudflare - [wrangler](../wrangler/) - CLI deployment alternative - [workers](../workers/) - Worker runtime documentation ================================================ FILE: skills/.curated/cloudflare-deploy/references/pulumi/api.md ================================================ # API & Data Sources ## Outputs and Exports Export resource identifiers: ```typescript export const kvId = kv.id; export const bucketName = bucket.name; export const workerUrl = worker.subdomain; export const dbId = db.id; ``` ## Resource Dependencies Implicit dependencies via outputs: ```typescript const kv = new cloudflare.WorkersKvNamespace("kv", { accountId: accountId, title: "my-kv", }); // Worker depends on KV (implicit via kv.id) const worker = new cloudflare.WorkerScript("worker", { accountId: accountId, name: "my-worker", content: code, kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], // Creates dependency }); ``` Explicit dependencies: ```typescript const migration = new command.local.Command("migration", { create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`, }, {dependsOn: [db]}); const worker = new cloudflare.WorkerScript("worker", { accountId: accountId, name: "worker", content: code, d1DatabaseBindings: [{name: "DB", databaseId: db.id}], }, {dependsOn: [migration]}); // Ensure migrations run first ``` ## Using Outputs with API Calls ```typescript const db = new cloudflare.D1Database("db", {accountId, name: "my-db"}); db.id.apply(async (dbId) => { const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${dbId}/query`, {method: "POST", headers: {"Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json"}, body: JSON.stringify({sql: "CREATE TABLE users (id INT)"})} ); return response.json(); }); ``` ## Custom Dynamic Providers For resources not in provider: ```typescript import * as pulumi from "@pulumi/pulumi"; class D1MigrationProvider implements pulumi.dynamic.ResourceProvider { async create(inputs: any): Promise { const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${inputs.accountId}/d1/database/${inputs.databaseId}/query`, {method: "POST", headers: {"Authorization": `Bearer ${inputs.apiToken}`, "Content-Type": "application/json"}, body: JSON.stringify({sql: inputs.sql})} ); return {id: `${inputs.databaseId}-${Date.now()}`, outs: await response.json()}; } async update(id: string, olds: any, news: any): Promise { if (olds.sql !== news.sql) await this.create(news); return {}; } async delete(id: string, props: any): Promise {} } class D1Migration extends pulumi.dynamic.Resource { constructor(name: string, args: any, opts?: pulumi.CustomResourceOptions) { super(new D1MigrationProvider(), name, args, opts); } } const migration = new D1Migration("migration", { accountId, databaseId: db.id, apiToken, sql: "CREATE TABLE users (id INT)", }, {dependsOn: [db]}); ``` ## Data Sources **Get Zone:** ```typescript const zone = cloudflare.getZone({name: "example.com"}); const zoneId = zone.then(z => z.id); ``` **Get Accounts (via API):** Use Cloudflare API directly or custom dynamic resources. ## Import Existing Resources ```bash # Import worker pulumi import cloudflare:index/workerScript:WorkerScript my-worker / # Import KV namespace pulumi import cloudflare:index/workersKvNamespace:WorkersKvNamespace my-kv # Import R2 bucket pulumi import cloudflare:index/r2Bucket:R2Bucket my-bucket / # Import D1 database pulumi import cloudflare:index/d1Database:D1Database my-db / # Import DNS record pulumi import cloudflare:index/dnsRecord:DnsRecord my-record / ``` ## Secrets Management ```typescript import * as pulumi from "@pulumi/pulumi"; const config = new pulumi.Config(); const apiKey = config.requireSecret("apiKey"); // Encrypted in state const worker = new cloudflare.WorkerScript("worker", { accountId: accountId, name: "my-worker", content: code, secretTextBindings: [{name: "API_KEY", text: apiKey}], }); ``` Store secrets: ```bash pulumi config set --secret apiKey "secret-value" ``` ## Transform Pattern Modify resource args before creation: ```typescript import {Transform} from "@pulumi/pulumi"; interface BucketArgs { accountId: pulumi.Input; transform?: {bucket?: Transform}; } function createBucket(name: string, args: BucketArgs) { const bucketArgs: cloudflare.R2BucketArgs = { accountId: args.accountId, name: name, location: "auto", }; const finalArgs = args.transform?.bucket?.(bucketArgs) ?? bucketArgs; return new cloudflare.R2Bucket(name, finalArgs); } ``` ## v6.x Worker Versioning Resources **Worker** - Container for versions: ```typescript const worker = new cloudflare.Worker("api", {accountId, name: "api-worker"}); export const workerId = worker.id; ``` **WorkerVersion** - Immutable code + config: ```typescript const version = new cloudflare.WorkerVersion("v1", { accountId, workerId: worker.id, content: fs.readFileSync("./dist/worker.js", "utf8"), compatibilityDate: "2025-01-01", }); export const versionId = version.id; ``` **WorkersDeployment** - Active deployment with bindings: ```typescript const deployment = new cloudflare.WorkersDeployment("prod", { accountId, workerId: worker.id, versionId: version.id, kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], }); ``` **Use:** Advanced deployments (canary, blue-green). Most apps should use `WorkerScript` (auto-versioning). --- See: [README.md](./README.md), [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/pulumi/configuration.md ================================================ # Resource Configuration ## Workers (cloudflare.WorkerScript) ```typescript import * as cloudflare from "@pulumi/cloudflare"; import * as fs from "fs"; const worker = new cloudflare.WorkerScript("my-worker", { accountId: accountId, name: "my-worker", content: fs.readFileSync("./dist/worker.js", "utf8"), module: true, // ES modules compatibilityDate: "2025-01-01", compatibilityFlags: ["nodejs_compat"], // v6.x: Observability logpush: true, // Enable Workers Logpush tailConsumers: [{service: "log-consumer"}], // Stream logs to Worker // v6.x: Placement placement: {mode: "smart"}, // Smart placement for latency optimization // Bindings kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], r2BucketBindings: [{name: "MY_BUCKET", bucketName: bucket.name}], d1DatabaseBindings: [{name: "DB", databaseId: db.id}], queueBindings: [{name: "MY_QUEUE", queue: queue.id}], serviceBindings: [{name: "OTHER_SERVICE", service: other.name}], plainTextBindings: [{name: "ENV_VAR", text: "value"}], secretTextBindings: [{name: "API_KEY", text: secret}], // v6.x: Advanced bindings analyticsEngineBindings: [{name: "ANALYTICS", dataset: "my-dataset"}], browserBinding: {name: "BROWSER"}, // Browser Rendering aiBinding: {name: "AI"}, // Workers AI hyperdriveBindings: [{name: "HYPERDRIVE", id: hyperdriveConfig.id}], }); ``` ## Workers KV (cloudflare.WorkersKvNamespace) ```typescript const kv = new cloudflare.WorkersKvNamespace("my-kv", { accountId: accountId, title: "my-kv-namespace", }); // Write values const kvValue = new cloudflare.WorkersKvValue("config", { accountId: accountId, namespaceId: kv.id, key: "config", value: JSON.stringify({foo: "bar"}), }); ``` ## R2 Buckets (cloudflare.R2Bucket) ```typescript const bucket = new cloudflare.R2Bucket("my-bucket", { accountId: accountId, name: "my-bucket", location: "auto", // or "wnam", etc. }); ``` ## D1 Databases (cloudflare.D1Database) ```typescript const db = new cloudflare.D1Database("my-db", {accountId, name: "my-database"}); // Migrations via wrangler import * as command from "@pulumi/command"; const migration = new command.local.Command("d1-migration", { create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`, }, {dependsOn: [db]}); ``` ## Queues (cloudflare.Queue) ```typescript const queue = new cloudflare.Queue("my-queue", {accountId, name: "my-queue"}); // Producer const producer = new cloudflare.WorkerScript("producer", { accountId, name: "producer", content: code, queueBindings: [{name: "MY_QUEUE", queue: queue.id}], }); // Consumer const consumer = new cloudflare.WorkerScript("consumer", { accountId, name: "consumer", content: code, queueConsumers: [{queue: queue.name, maxBatchSize: 10, maxRetries: 3}], }); ``` ## Pages Projects (cloudflare.PagesProject) ```typescript const pages = new cloudflare.PagesProject("my-site", { accountId, name: "my-site", productionBranch: "main", buildConfig: {buildCommand: "npm run build", destinationDir: "dist"}, source: { type: "github", config: {owner: "my-org", repoName: "my-repo", productionBranch: "main"}, }, deploymentConfigs: { production: { environmentVariables: {NODE_VERSION: "18"}, kvNamespaces: {MY_KV: kv.id}, d1Databases: {DB: db.id}, }, }, }); ``` ## DNS Records (cloudflare.DnsRecord) ```typescript const zone = cloudflare.getZone({name: "example.com"}); const record = new cloudflare.DnsRecord("www", { zoneId: zone.then(z => z.id), name: "www", type: "A", content: "192.0.2.1", ttl: 3600, proxied: true, }); ``` ## Workers Domains/Routes ```typescript // Route (pattern-based) const route = new cloudflare.WorkerRoute("my-route", { zoneId: zoneId, pattern: "example.com/api/*", scriptName: worker.name, }); // Domain (dedicated subdomain) const domain = new cloudflare.WorkersDomain("my-domain", { accountId: accountId, hostname: "api.example.com", service: worker.name, zoneId: zoneId, }); ``` ## Assets Configuration (v6.x) Serve static assets from Workers: ```typescript const worker = new cloudflare.WorkerScript("app", { accountId: accountId, name: "my-app", content: code, assets: { path: "./public", // Local directory // Assets uploaded and served from Workers }, }); ``` ## v6.x Versioned Deployments (Advanced) For gradual rollouts, use 3-resource pattern: ```typescript // 1. Worker (container for versions) const worker = new cloudflare.Worker("api", { accountId: accountId, name: "api-worker", }); // 2. Version (immutable code + config) const version = new cloudflare.WorkerVersion("v1", { accountId: accountId, workerId: worker.id, content: fs.readFileSync("./dist/worker.js", "utf8"), compatibilityDate: "2025-01-01", compatibilityFlags: ["nodejs_compat"], // Note: Bindings configured at deployment level }); // 3. Deployment (version + bindings + traffic split) const deployment = new cloudflare.WorkersDeployment("prod", { accountId: accountId, workerId: worker.id, versionId: version.id, // Bindings applied to deployment kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], }); ``` **When to use:** Blue-green deployments, canary releases, gradual rollouts **When NOT to use:** Simple single-version deployments (use WorkerScript) --- See: [README.md](./README.md), [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/pulumi/gotchas.md ================================================ # Troubleshooting & Best Practices ## Common Errors ### "No bundler/build step" - Pulumi uploads raw code **Problem:** Worker fails with "Cannot use import statement outside a module" **Cause:** Pulumi doesn't bundle Worker code - uploads exactly what you provide **Solution:** Build Worker BEFORE Pulumi deploy ```typescript // WRONG: Pulumi won't bundle this const worker = new cloudflare.WorkerScript("worker", { content: fs.readFileSync("./src/index.ts", "utf8"), // Raw TS file }); // RIGHT: Build first, then deploy import * as command from "@pulumi/command"; const build = new command.local.Command("build", { create: "npm run build", dir: "./worker", }); const worker = new cloudflare.WorkerScript("worker", { content: build.stdout.apply(() => fs.readFileSync("./worker/dist/index.js", "utf8")), }, {dependsOn: [build]}); ``` ### "wrangler.toml not consumed" - Config drift **Problem:** Local wrangler dev works, Pulumi deploy fails **Cause:** Pulumi ignores wrangler.toml - must duplicate config **Solution:** Generate wrangler.toml from Pulumi or keep synced manually ```typescript // Pattern: Export Pulumi config to wrangler.toml const workerConfig = { name: "my-worker", compatibilityDate: "2025-01-01", compatibilityFlags: ["nodejs_compat"], }; new command.local.Command("generate-wrangler", { create: pulumi.interpolate`cat > wrangler.toml <.yaml config: cloudflare:accountId: "abc123..." ``` ### "Binding name mismatch" **Problem:** Worker fails with "env.MY_KV is undefined" **Cause:** Binding name in Pulumi != name in Worker code **Solution:** Match exactly (case-sensitive) ```typescript // Pulumi kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}] // Worker code export default { async fetch(request, env) { await env.MY_KV.get("key"); }} ``` ### "API token permissions insufficient" **Problem:** `Error: authentication error (10000)` **Cause:** Token lacks required permissions **Solution:** Grant token permissions: Account.Workers Scripts:Edit, Account.Account Settings:Read ### "Resource not found after import" **Problem:** Imported resource shows as changed on next `pulumi up` **Cause:** State mismatch between actual resource and Pulumi config **Solution:** Check property names/types match exactly ```bash pulumi import cloudflare:index/workerScript:WorkerScript my-worker / pulumi preview # If shows changes, adjust Pulumi code to match actual resource ``` ### "v6.x Worker versioning confusion" **Problem:** Worker deployed but not receiving traffic **Cause:** v6.x requires Worker + WorkerVersion + WorkersDeployment (3 resources) **Solution:** Use WorkerScript (auto-versioning) OR full versioning pattern ```typescript // SIMPLE: WorkerScript auto-versions (default behavior) const worker = new cloudflare.WorkerScript("worker", { accountId, name: "my-worker", content: code, }); // ADVANCED: Manual versioning for gradual rollouts (v6.x) const worker = new cloudflare.Worker("worker", {accountId, name: "my-worker"}); const version = new cloudflare.WorkerVersion("v1", { accountId, workerId: worker.id, content: code, compatibilityDate: "2025-01-01", }); const deployment = new cloudflare.WorkersDeployment("prod", { accountId, workerId: worker.id, versionId: version.id, }); ``` ## Best Practices 1. **Always set compatibilityDate** - Locks Worker behavior, prevents breaking changes 2. **Build before deploy** - Pulumi doesn't bundle; use Command resource or CI build step 3. **Match binding names** - Case-sensitive, must match between Pulumi and Worker code 4. **Use dependsOn for migrations** - Ensure D1 migrations run before Worker deploys 5. **Version Worker content** - Add VERSION binding to force redeployment on content changes 6. **Store secrets in stack config** - Use `pulumi config set --secret` for API keys ## Limits | Resource | Limit | Notes | |----------|-------|-------| | Worker script size | 10 MB | Includes all dependencies, after compression | | Worker CPU time | 50ms (free), 30s (paid) | Per request | | KV keys per namespace | Unlimited | 1000 ops/sec write, 100k ops/sec read | | R2 storage | Unlimited | Class A ops: 1M/mo free, Class B: 10M/mo free | | D1 databases | 50,000 per account | Free: 10 per account, 5 GB each | | Queues | 10,000 per account | Free: 1M ops/day | | Pages projects | 500 per account | Free: 100 projects | | API requests | Varies by plan | ~1200 req/5min on free | ## Resources - **Pulumi Registry:** https://www.pulumi.com/registry/packages/cloudflare/ - **API Docs:** https://www.pulumi.com/registry/packages/cloudflare/api-docs/ - **GitHub:** https://github.com/pulumi/pulumi-cloudflare - **Cloudflare Docs:** https://developers.cloudflare.com/ - **Workers Docs:** https://developers.cloudflare.com/workers/ --- See: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/pulumi/patterns.md ================================================ # Architecture Patterns ## Component Resources ```typescript class WorkerApp extends pulumi.ComponentResource { constructor(name: string, args: WorkerAppArgs, opts?) { super("custom:cloudflare:WorkerApp", name, {}, opts); const defaultOpts = {parent: this}; this.kv = new cloudflare.WorkersKvNamespace(`${name}-kv`, {accountId: args.accountId, title: `${name}-kv`}, defaultOpts); this.worker = new cloudflare.WorkerScript(`${name}-worker`, { accountId: args.accountId, name: `${name}-worker`, content: args.workerCode, module: true, kvNamespaceBindings: [{name: "KV", namespaceId: this.kv.id}], }, defaultOpts); this.domain = new cloudflare.WorkersDomain(`${name}-domain`, { accountId: args.accountId, hostname: args.domain, service: this.worker.name, }, defaultOpts); } } ``` ## Full-Stack Worker App ```typescript const kv = new cloudflare.WorkersKvNamespace("cache", {accountId, title: "api-cache"}); const db = new cloudflare.D1Database("db", {accountId, name: "app-database"}); const bucket = new cloudflare.R2Bucket("assets", {accountId, name: "app-assets"}); const apiWorker = new cloudflare.WorkerScript("api", { accountId, name: "api-worker", content: fs.readFileSync("./dist/api.js", "utf8"), module: true, kvNamespaceBindings: [{name: "CACHE", namespaceId: kv.id}], d1DatabaseBindings: [{name: "DB", databaseId: db.id}], r2BucketBindings: [{name: "ASSETS", bucketName: bucket.name}], }); ``` ## Multi-Environment Setup ```typescript const stack = pulumi.getStack(); const worker = new cloudflare.WorkerScript(`worker-${stack}`, { accountId, name: `my-worker-${stack}`, content: code, plainTextBindings: [{name: "ENVIRONMENT", text: stack}], }); ``` ## Queue-Based Processing ```typescript const queue = new cloudflare.Queue("processing-queue", {accountId, name: "image-processing"}); // Producer: API receives requests const apiWorker = new cloudflare.WorkerScript("api", { accountId, name: "api-worker", content: apiCode, queueBindings: [{name: "PROCESSING_QUEUE", queue: queue.id}], }); // Consumer: Process async const processorWorker = new cloudflare.WorkerScript("processor", { accountId, name: "processor-worker", content: processorCode, queueConsumers: [{queue: queue.name, maxBatchSize: 10, maxRetries: 3, maxWaitTimeMs: 5000}], r2BucketBindings: [{name: "OUTPUT_BUCKET", bucketName: outputBucket.name}], }); ``` ## Microservices with Service Bindings ```typescript const authWorker = new cloudflare.WorkerScript("auth", {accountId, name: "auth-service", content: authCode}); const apiWorker = new cloudflare.WorkerScript("api", { accountId, name: "api-service", content: apiCode, serviceBindings: [{name: "AUTH", service: authWorker.name}], }); ``` ## Event-Driven Architecture ```typescript const eventQueue = new cloudflare.Queue("events", {accountId, name: "event-bus"}); const producer = new cloudflare.WorkerScript("producer", { accountId, name: "api-producer", content: producerCode, queueBindings: [{name: "EVENTS", queue: eventQueue.id}], }); const consumer = new cloudflare.WorkerScript("consumer", { accountId, name: "email-consumer", content: consumerCode, queueConsumers: [{queue: eventQueue.name, maxBatchSize: 10}], }); ``` ## v6.x Versioned Deployments (Blue-Green/Canary) ```typescript const worker = new cloudflare.Worker("api", {accountId, name: "api-worker"}); const v1 = new cloudflare.WorkerVersion("v1", {accountId, workerId: worker.id, content: fs.readFileSync("./dist/v1.js", "utf8"), compatibilityDate: "2025-01-01"}); const v2 = new cloudflare.WorkerVersion("v2", {accountId, workerId: worker.id, content: fs.readFileSync("./dist/v2.js", "utf8"), compatibilityDate: "2025-01-01"}); // Gradual rollout: 10% v2, 90% v1 const deployment = new cloudflare.WorkersDeployment("canary", { accountId, workerId: worker.id, versions: [{versionId: v2.id, percentage: 10}, {versionId: v1.id, percentage: 90}], kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], }); ``` **Use:** Canary releases, A/B testing, blue-green. Most apps use `WorkerScript` (auto-versioning). ## Wrangler.toml Generation (Bridge IaC with Local Dev) Generate wrangler.toml from Pulumi config to keep local dev in sync: ```typescript import * as command from "@pulumi/command"; const workerConfig = { name: "my-worker", compatibilityDate: "2025-01-01", compatibilityFlags: ["nodejs_compat"], }; // Create resources const kv = new cloudflare.WorkersKvNamespace("kv", {accountId, title: "my-kv"}); const db = new cloudflare.D1Database("db", {accountId, name: "my-db"}); const bucket = new cloudflare.R2Bucket("bucket", {accountId, name: "my-bucket"}); // Generate wrangler.toml after resources created const wranglerGen = new command.local.Command("gen-wrangler", { create: pulumi.interpolate`cat > wrangler.toml < fs.readFileSync("./worker/dist/index.js", "utf8")), }, {dependsOn: [build]}); ``` ## Content SHA Pattern (Force Updates) Prevent false "no changes" detections: ```typescript const version = Date.now().toString(); const worker = new cloudflare.WorkerScript("worker", { accountId, name: "my-worker", content: code, plainTextBindings: [{name: "VERSION", text: version}], // Forces deployment }); ``` --- See: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/queues/README.md ================================================ # Cloudflare Queues Flexible message queuing for async task processing with guaranteed at-least-once delivery and configurable batching. ## Overview Queues provide: - At-least-once delivery guarantee - Push-based (Worker) and pull-based (HTTP) consumers - Configurable batching and retries - Dead Letter Queues (DLQ) - Delays up to 12 hours **Use cases:** Async processing, API buffering, rate limiting, event workflows, deferred jobs ## Quick Start ```bash wrangler queues create my-queue wrangler queues consumer add my-queue my-worker ``` ```typescript // Producer await env.MY_QUEUE.send({ userId: 123, action: 'notify' }); // Consumer (with proper error handling) export default { async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { try { await process(msg.body); msg.ack(); } catch (error) { msg.retry({ delaySeconds: 60 }); } } } }; ``` ## Critical Warnings **Before using Queues, understand these production mistakes:** 1. **Uncaught errors retry ENTIRE batch** (not just failed message). Always use per-message try/catch. 2. **Messages not ack'd/retry'd will auto-retry forever** until max_retries. Always explicitly handle each message. See [gotchas.md](./gotchas.md) for detailed solutions. ## Core Operations | Operation | Purpose | Limit | |-----------|---------|-------| | `send(body, options?)` | Publish message | 128 KB | | `sendBatch(messages)` | Bulk publish | 100 msgs/256 KB | | `message.ack()` | Acknowledge success | - | | `message.retry(options?)` | Retry with delay | - | | `batch.ackAll()` | Ack entire batch | - | ## Architecture ``` [Producer Worker] → [Queue] → [Consumer Worker/HTTP] → [Processing] ``` - Max 10,000 queues per account - 5,000 msgs/second per queue - 4-14 day retention (configurable) ## Reading Order **New to Queues?** Start here: 1. [configuration.md](./configuration.md) - Set up queues, bindings, consumers 2. [api.md](./api.md) - Send messages, handle batches, ack/retry patterns 3. [patterns.md](./patterns.md) - Real-world examples and integrations 4. [gotchas.md](./gotchas.md) - Critical warnings and troubleshooting **Task-based routing:** - Setup queue → [configuration.md](./configuration.md) - Send/receive messages → [api.md](./api.md) - Implement specific pattern → [patterns.md](./patterns.md) - Debug/troubleshoot → [gotchas.md](./gotchas.md) ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc setup, producer/consumer config, DLQ, content types - [api.md](./api.md) - Send/batch methods, queue handler, ack/retry rules, type-safe patterns - [patterns.md](./patterns.md) - Async tasks, buffering, rate limiting, D1/Workflows/DO integrations - [gotchas.md](./gotchas.md) - Critical batch error handling, idempotency, error classification ## See Also - [workers](../workers/) - Worker runtime for producers/consumers - [r2](../r2/) - Process R2 event notifications via queues - [d1](../d1/) - Batch write to D1 from queue consumers ================================================ FILE: skills/.curated/cloudflare-deploy/references/queues/api.md ================================================ # Queues API Reference ## Producer: Send Messages ```typescript // Basic send await env.MY_QUEUE.send({ url: request.url, timestamp: Date.now() }); // Options: delay (max 43200s), contentType (json|text|bytes|v8) await env.MY_QUEUE.send(message, { delaySeconds: 600 }); await env.MY_QUEUE.send(message, { delaySeconds: 0 }); // Override queue default // Batch (up to 100 msgs or 256 KB) await env.MY_QUEUE.sendBatch([ { body: 'msg1' }, { body: 'msg2' }, { body: 'msg3', options: { delaySeconds: 300 } } ]); // Non-blocking with ctx.waitUntil - send continues after response ctx.waitUntil(env.MY_QUEUE.send({ data: 'async' })); // Background tasks in queue consumer export default { async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise { for (const msg of batch.messages) { await processMessage(msg.body); // Fire-and-forget analytics (doesn't block ack) ctx.waitUntil( env.ANALYTICS_QUEUE.send({ messageId: msg.id, processedAt: Date.now() }) ); msg.ack(); } } }; ``` ## Consumer: Push-based (Worker) ```typescript // Type-safe handler with ExportedHandler interface Env { MY_QUEUE: Queue; DB: D1Database; } export default { async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise { // batch.queue, batch.messages.length for (const msg of batch.messages) { // msg.id, msg.body, msg.timestamp, msg.attempts try { await processMessage(msg.body); msg.ack(); } catch (error) { msg.retry({ delaySeconds: 600 }); } } } } satisfies ExportedHandler; ``` **CRITICAL WARNINGS:** 1. **Messages not explicitly ack'd or retry'd will auto-retry indefinitely** until `max_retries` is reached. Always call `msg.ack()` or `msg.retry()` for each message. 2. **Throwing uncaught errors retries the ENTIRE batch**, not just the failed message. Always wrap individual message processing in try/catch and call `msg.retry()` explicitly per message. ```typescript // ❌ BAD: Uncaught error retries entire batch async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { await riskyOperation(msg.body); // If this throws, entire batch retries msg.ack(); } } // ✅ GOOD: Catch per message, handle individually async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { try { await riskyOperation(msg.body); msg.ack(); } catch (error) { msg.retry({ delaySeconds: 60 }); } } } ``` ## Ack/Retry Precedence Rules 1. **Per-message calls take precedence**: If you call both `msg.ack()` and `msg.retry()`, last call wins 2. **Batch calls don't override**: `batch.ackAll()` only affects messages without explicit ack/retry 3. **No action = automatic retry**: Messages with no explicit action retry with configured delay ```typescript async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { msg.ack(); // Message marked for ack msg.retry(); // Overrides ack - message will retry } batch.ackAll(); // Only affects messages not explicitly handled above } ``` ## Batch Operations ```typescript // Acknowledge entire batch try { await bulkProcess(batch.messages); batch.ackAll(); } catch (error) { batch.retryAll({ delaySeconds: 300 }); } ``` ## Exponential Backoff ```typescript async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { try { await processMessage(msg.body); msg.ack(); } catch (error) { // 30s, 60s, 120s, 240s, 480s, ... up to 12h max const delay = Math.min(30 * (2 ** msg.attempts), 43200); msg.retry({ delaySeconds: delay }); } } } ``` ## Multiple Queues, Single Consumer ```typescript export default { async queue(batch: MessageBatch, env: Env): Promise { switch (batch.queue) { case 'high-priority': await processUrgent(batch.messages); break; case 'low-priority': await processDeferred(batch.messages); break; case 'email': await sendEmails(batch.messages); break; default: batch.retryAll(); } } }; ``` ## Consumer: Pull-based (HTTP) ```typescript // Pull messages const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/queues/${QUEUE_ID}/messages/pull`, { method: 'POST', headers: { 'authorization': `Bearer ${API_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ visibility_timeout_ms: 6000, batch_size: 50 }) } ); const data = await response.json(); // Acknowledge await fetch( `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/queues/${QUEUE_ID}/messages/ack`, { method: 'POST', headers: { 'authorization': `Bearer ${API_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ acks: [{ lease_id: msg.lease_id }], retries: [{ lease_id: msg2.lease_id, delay_seconds: 600 }] }) } ); ``` ## Interfaces ```typescript interface MessageBatch { readonly queue: string; readonly messages: Message[]; ackAll(): void; retryAll(options?: QueueRetryOptions): void; } interface Message { readonly id: string; readonly timestamp: Date; readonly body: Body; readonly attempts: number; ack(): void; retry(options?: QueueRetryOptions): void; } interface QueueSendOptions { contentType?: 'text' | 'bytes' | 'json' | 'v8'; delaySeconds?: number; // 0-43200 } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/queues/configuration.md ================================================ # Queues Configuration ## Create Queue ```bash wrangler queues create my-queue wrangler queues create my-queue --retention-period-hours=336 # 14 days wrangler queues create my-queue --delivery-delay-secs=300 ``` ## Producer Binding **wrangler.jsonc:** ```jsonc { "queues": { "producers": [ { "queue": "my-queue-name", "binding": "MY_QUEUE", "delivery_delay": 60 // Optional: default delay in seconds } ] } } ``` ## Consumer Configuration (Push-based) **wrangler.jsonc:** ```jsonc { "queues": { "consumers": [ { "queue": "my-queue-name", "max_batch_size": 10, // 1-100, default 10 "max_batch_timeout": 5, // 0-60s, default 5 "max_retries": 3, // default 3, max 100 "dead_letter_queue": "my-dlq", // optional "retry_delay": 300 // optional: delay retries in seconds } ] } } ``` ## Consumer Configuration (Pull-based) **wrangler.jsonc:** ```jsonc { "queues": { "consumers": [ { "queue": "my-queue-name", "type": "http_pull", "visibility_timeout_ms": 5000, // default 30000, max 12h "max_retries": 5, "dead_letter_queue": "my-dlq" } ] } } ``` ## TypeScript Types ```typescript interface Env { MY_QUEUE: Queue; ANALYTICS_QUEUE: Queue; } interface MessageBody { id: string; action: 'create' | 'update' | 'delete'; data: Record; } export default { async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { console.log(msg.body.action); msg.ack(); } } } satisfies ExportedHandler; ``` ## Content Type Selection Choose content type based on consumer type and data requirements: | Content Type | Use When | Readable By | Supports | Size | |--------------|----------|-------------|----------|------| | `json` | Pull consumers, dashboard visibility, simple objects | All (push/pull/dashboard) | JSON-serializable types only | Medium | | `v8` | Push consumers only, complex JS objects | Push consumers only | Date, Map, Set, BigInt, typed arrays | Small | | `text` | String-only payloads | All | Strings only | Smallest | | `bytes` | Binary data (images, files) | All | ArrayBuffer, Uint8Array | Variable | **Decision tree:** 1. Need to view in dashboard or use pull consumer? → Use `json` 2. Need Date, Map, Set, or other V8 types? → Use `v8` (push consumers only) 3. Just strings? → Use `text` 4. Binary data? → Use `bytes` ```typescript // JSON: Good for simple objects, pull consumers, dashboard visibility await env.QUEUE.send({ id: 123, name: 'test' }, { contentType: 'json' }); // V8: Good for Date, Map, Set (push consumers only) await env.QUEUE.send({ created: new Date(), tags: new Set(['a', 'b']) }, { contentType: 'v8' }); // Text: Simple strings await env.QUEUE.send('process-user-123', { contentType: 'text' }); // Bytes: Binary data await env.QUEUE.send(imageBuffer, { contentType: 'bytes' }); ``` **Default behavior:** If not specified, Cloudflare auto-selects `json` for JSON-serializable objects and `v8` for complex types. **IMPORTANT:** `v8` messages cannot be read by pull consumers or viewed in the dashboard. Use `json` if you need visibility or pull-based consumption. ## CLI Commands ```bash # Consumer management wrangler queues consumer add my-queue my-worker --batch-size=50 --max-retries=5 wrangler queues consumer http add my-queue wrangler queues consumer worker remove my-queue my-worker wrangler queues consumer http remove my-queue # Queue operations wrangler queues list wrangler queues pause my-queue wrangler queues resume my-queue wrangler queues purge my-queue wrangler queues delete my-queue ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/queues/gotchas.md ================================================ # Queues Gotchas & Troubleshooting ## CRITICAL: Top Production Mistakes ### 1. "Entire Batch Retried After Single Error" **Problem:** Throwing uncaught error in queue handler retries the entire batch, not just the failed message **Cause:** Uncaught exceptions propagate to the runtime, triggering batch-level retry **Solution:** Always wrap individual message processing in try/catch and call `msg.retry()` explicitly ```typescript // ❌ BAD: Throws error, retries entire batch async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { await riskyOperation(msg.body); // If this throws, entire batch retries msg.ack(); } } // ✅ GOOD: Catch per message, handle individually async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { try { await riskyOperation(msg.body); msg.ack(); } catch (error) { msg.retry({ delaySeconds: 60 }); } } } ``` ### 2. "Messages Retry Forever" **Problem:** Messages not explicitly ack'd or retry'd will auto-retry indefinitely **Cause:** Runtime default behavior retries unhandled messages until `max_retries` reached **Solution:** Always call `msg.ack()` or `msg.retry()` for each message. Never leave messages unhandled. ```typescript // ❌ BAD: Skipped messages auto-retry forever async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { if (shouldProcess(msg.body)) { await process(msg.body); msg.ack(); } // Missing: msg.ack() for skipped messages - they will retry! } } // ✅ GOOD: Explicitly handle all messages async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { if (shouldProcess(msg.body)) { await process(msg.body); msg.ack(); } else { msg.ack(); // Explicitly ack even if not processing } } } ``` ## Common Errors ### "Duplicate Message Processing" **Problem:** Same message processed multiple times **Cause:** At-least-once delivery guarantee means duplicates are possible during retries **Solution:** Design consumers to be idempotent by tracking processed message IDs in KV with expiration TTL ```typescript async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { const processed = await env.PROCESSED_KV.get(msg.id); if (processed) { msg.ack(); continue; } await processMessage(msg.body); await env.PROCESSED_KV.put(msg.id, '1', { expirationTtl: 86400 }); msg.ack(); } } ``` ### "Pull Consumer Can't Decode Messages" **Problem:** Pull consumer or dashboard shows unreadable message bodies **Cause:** Messages sent with `v8` content type are only decodable by Workers push consumers **Solution:** Use `json` content type for pull consumers or dashboard visibility ```typescript // Use json for pull consumers await env.MY_QUEUE.send(data, { contentType: 'json' }); // Use v8 only for push consumers with complex JS types await env.MY_QUEUE.send({ date: new Date(), tags: new Set() }, { contentType: 'v8' }); ``` ### "Messages Not Being Delivered" **Problem:** Messages sent but consumer not processing **Cause:** Queue paused, consumer not configured, or consumer errors **Solution:** Check queue status with `wrangler queues list`, verify consumer configured with `wrangler queues consumer add`, and check logs with `wrangler tail` ### "High Dead Letter Queue Rate" **Problem:** Many messages ending up in DLQ **Cause:** Consumer repeatedly failing to process messages after max retries **Solution:** Review consumer error logs, check external dependency availability, verify message format matches expectations, or increase retry delay ## Error Classification Patterns Classify errors to decide whether to retry or DLQ: ```typescript async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { try { await processMessage(msg.body); msg.ack(); } catch (error) { // Transient errors: retry with backoff if (isRetryable(error)) { const delay = Math.min(30 * (2 ** msg.attempts), 43200); msg.retry({ delaySeconds: delay }); } // Permanent errors: ack to avoid infinite retries else { console.error('Permanent error, sending to DLQ:', error); await env.ERROR_LOG.put(msg.id, JSON.stringify({ msg: msg.body, error: String(error) })); msg.ack(); // Prevent further retries } } } } function isRetryable(error: unknown): boolean { if (error instanceof Response) { // Retry: rate limits, timeouts, server errors return error.status === 429 || error.status >= 500; } if (error instanceof Error) { // Don't retry: validation, auth, not found return !error.message.includes('validation') && !error.message.includes('unauthorized') && !error.message.includes('not found'); } return false; // Unknown errors don't retry } ``` ### "CPU Time Exceeded in Consumer" **Problem:** Consumer fails with CPU time limit exceeded **Cause:** Consumer processing exceeding 30s default CPU time limit **Solution:** Increase CPU limit in wrangler.jsonc: `{ "limits": { "cpu_ms": 300000 } }` (5 minutes max) ## Content Type Decision Guide **When to use each content type:** | Content Type | Use When | Readable By | Supports | |--------------|----------|-------------|----------| | `json` (default) | Pull consumers, dashboard visibility, simple objects | All (push/pull/dashboard) | JSON-serializable types only | | `v8` | Push consumers only, complex JS objects | Push consumers only | Date, Map, Set, BigInt, typed arrays | | `text` | String-only payloads | All | Strings only | | `bytes` | Binary data (images, files) | All | ArrayBuffer, Uint8Array | **Decision tree:** 1. Need to view in dashboard or use pull consumer? → Use `json` 2. Need Date, Map, Set, or other V8 types? → Use `v8` (push consumers only) 3. Just strings? → Use `text` 4. Binary data? → Use `bytes` ```typescript // Dashboard/pull: use json await env.QUEUE.send({ id: 123, name: 'test' }, { contentType: 'json' }); // Complex JS types (push only): use v8 await env.QUEUE.send({ created: new Date(), tags: new Set(['a', 'b']) }, { contentType: 'v8' }); ``` ## Limits | Limit | Value | Notes | |-------|-------|-------| | Max queues | 10,000 | Per account | | Message size | 128 KB | Maximum per message | | Batch size (consumer) | 100 messages | Maximum messages per batch | | Batch size (sendBatch) | 100 msgs or 256 KB | Whichever limit reached first | | Throughput | 5,000 msgs/sec | Per queue | | Retention | 4-14 days | Configurable retention period | | Max backlog | 25 GB | Maximum queue backlog size | | Max delay | 12 hours (43,200s) | Maximum message delay | | Max retries | 100 | Maximum retry attempts | | CPU time default | 30s | Per consumer invocation | | CPU time max | 300s (5 min) | Configurable via `limits.cpu_ms` | | Operations per message | 3 (write + read + delete) | Base cost per message | | Pricing | $0.40 per 1M operations | After 1M free operations | | Message charging | Per 64 KB chunk | Messages charged in 64 KB increments | ================================================ FILE: skills/.curated/cloudflare-deploy/references/queues/patterns.md ================================================ # Queues Patterns & Best Practices ## Async Task Processing ```typescript // Producer: Accept request, queue work export default { async fetch(request: Request, env: Env): Promise { const { userId, reportType } = await request.json(); await env.REPORT_QUEUE.send({ userId, reportType, requestedAt: Date.now() }); return Response.json({ message: 'Report queued', status: 'pending' }); } }; // Consumer: Process reports export default { async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { const { userId, reportType } = msg.body; const report = await generateReport(userId, reportType, env); await env.REPORTS_BUCKET.put(`${userId}/${reportType}.pdf`, report); msg.ack(); } } }; ``` ## Buffering API Calls ```typescript // Producer: Queue log entries ctx.waitUntil(env.LOGS_QUEUE.send({ method: request.method, url: request.url, timestamp: Date.now() })); // Consumer: Batch write to external API async queue(batch: MessageBatch, env: Env): Promise { const logs = batch.messages.map(m => m.body); await fetch(env.LOG_ENDPOINT, { method: 'POST', body: JSON.stringify({ logs }) }); batch.ackAll(); } ``` ## Rate Limiting Upstream ```typescript async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { try { await callRateLimitedAPI(msg.body); msg.ack(); } catch (error) { if (error.status === 429) { const retryAfter = parseInt(error.headers.get('Retry-After') || '60'); msg.retry({ delaySeconds: retryAfter }); } else throw error; } } } ``` ## Event-Driven Workflows ```typescript // R2 event → Queue → Worker export default { async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { const event = msg.body; if (event.action === 'PutObject') { await processNewFile(event.object.key, env); } else if (event.action === 'DeleteObject') { await cleanupReferences(event.object.key, env); } msg.ack(); } } }; ``` ## Dead Letter Queue Pattern ```typescript // Main queue: After max_retries, goes to DLQ automatically export default { async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { try { await riskyOperation(msg.body); msg.ack(); } catch (error) { console.error(`Failed after ${msg.attempts} attempts:`, error); } } } }; // DLQ consumer: Log and store failed messages export default { async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { await env.FAILED_KV.put(msg.id, JSON.stringify(msg.body)); msg.ack(); } } }; ``` ## Priority Queues High priority: `max_batch_size: 5, max_batch_timeout: 1`. Low priority: `max_batch_size: 100, max_batch_timeout: 30`. ## Delayed Job Processing ```typescript await env.EMAIL_QUEUE.send({ to, template, userId }, { delaySeconds: 3600 }); ``` ## Fan-out Pattern ```typescript async fetch(request: Request, env: Env): Promise { const event = await request.json(); // Send to multiple queues for parallel processing await Promise.all([ env.ANALYTICS_QUEUE.send(event), env.NOTIFICATIONS_QUEUE.send(event), env.AUDIT_LOG_QUEUE.send(event) ]); return Response.json({ status: 'processed' }); } ``` ## Idempotency Pattern ```typescript async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { // Check if already processed const processed = await env.PROCESSED_KV.get(msg.id); if (processed) { msg.ack(); continue; } await processMessage(msg.body); await env.PROCESSED_KV.put(msg.id, '1', { expirationTtl: 86400 }); msg.ack(); } } ``` ## Integration: D1 Batch Writes ```typescript async queue(batch: MessageBatch, env: Env): Promise { // Collect all inserts for single D1 batch const statements = batch.messages.map(msg => env.DB.prepare('INSERT INTO events (id, data, created) VALUES (?, ?, ?)') .bind(msg.id, JSON.stringify(msg.body), Date.now()) ); try { await env.DB.batch(statements); batch.ackAll(); } catch (error) { console.error('D1 batch failed:', error); batch.retryAll({ delaySeconds: 60 }); } } ``` ## Integration: Workflows ```typescript // Queue triggers Workflow for long-running tasks async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { try { const instance = await env.MY_WORKFLOW.create({ id: msg.id, params: msg.body }); console.log('Workflow started:', instance.id); msg.ack(); } catch (error) { msg.retry({ delaySeconds: 30 }); } } } ``` ## Integration: Durable Objects ```typescript // Queue distributes work to Durable Objects by ID async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { const { userId, action } = msg.body; // Route to user-specific DO const id = env.USER_DO.idFromName(userId); const stub = env.USER_DO.get(id); try { await stub.fetch(new Request('https://do/process', { method: 'POST', body: JSON.stringify({ action, messageId: msg.id }) })); msg.ack(); } catch (error) { msg.retry({ delaySeconds: 60 }); } } } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2/README.md ================================================ # Cloudflare R2 Object Storage S3-compatible object storage with zero egress fees, optimized for large file storage and delivery. ## Overview R2 provides: - S3-compatible API (Workers API + S3 REST) - Zero egress fees globally - Strong consistency for writes/deletes - Storage classes (Standard/Infrequent Access) - SSE-C encryption support **Use cases:** Media storage, backups, static assets, user uploads, data lakes ## Quick Start ```bash wrangler r2 bucket create my-bucket --location=enam wrangler r2 object put my-bucket/file.txt --file=./local.txt ``` ```typescript // Upload await env.MY_BUCKET.put(key, data, { httpMetadata: { contentType: 'image/jpeg' } }); // Download const object = await env.MY_BUCKET.get(key); if (object) return new Response(object.body); ``` ## Core Operations | Method | Purpose | Returns | |--------|---------|---------| | `put(key, value, options?)` | Upload object | `R2Object \| null` | | `get(key, options?)` | Download object | `R2ObjectBody \| R2Object \| null` | | `head(key)` | Get metadata only | `R2Object \| null` | | `delete(keys)` | Delete object(s) | `Promise` | | `list(options?)` | List objects | `R2Objects` | ## Storage Classes - **Standard**: Frequent access, low latency reads - **InfrequentAccess**: 30-day minimum storage, retrieval fees, lower storage cost ## Event Notifications R2 integrates with Cloudflare Queues for reactive workflows: ```typescript // wrangler.jsonc { "event_notifications": [{ "queue": "r2-notifications", "actions": ["PutObject", "DeleteObject"] }] } // Consumer async queue(batch: MessageBatch, env: Env) { for (const message of batch.messages) { const event = message.body; // { action, bucket, object, timestamps } if (event.action === 'PutObject') { // Process upload: thumbnail generation, virus scan, etc. } } } ``` ## Reading Order **First-time users:** README → configuration.md → api.md → patterns.md **Specific tasks:** - Setup: configuration.md - Client uploads: patterns.md (presigned URLs) - Public static site: patterns.md (public access + custom domain) - Processing uploads: README (event notifications) + queues reference - Debugging: gotchas.md ## In This Reference - [configuration.md](./configuration.md) - Bindings, S3 SDK, CORS, lifecycles, token scopes - [api.md](./api.md) - Workers API, multipart, conditional requests, presigned URLs - [patterns.md](./patterns.md) - Streaming, caching, client uploads, public buckets - [gotchas.md](./gotchas.md) - List truncation, etag format, stream length, S3 SDK region ## See Also - [workers](../workers/) - Worker runtime and fetch handlers - [kv](../kv/) - Metadata storage for R2 objects - [d1](../d1/) - Store R2 URLs in relational database - [queues](../queues/) - Process R2 uploads asynchronously ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2/api.md ================================================ # R2 API Reference ## PUT (Upload) ```typescript // Basic await env.MY_BUCKET.put(key, value); // With metadata await env.MY_BUCKET.put(key, value, { httpMetadata: { contentType: 'image/jpeg', contentDisposition: 'attachment; filename="photo.jpg"', cacheControl: 'max-age=3600' }, customMetadata: { userId: '123', version: '2' }, storageClass: 'Standard', // or 'InfrequentAccess' sha256: arrayBufferOrHex, // Integrity check ssecKey: arrayBuffer32bytes // SSE-C encryption }); // Value types: ReadableStream | ArrayBuffer | string | Blob ``` ## GET (Download) ```typescript const object = await env.MY_BUCKET.get(key); if (!object) return new Response('Not found', { status: 404 }); // Body: arrayBuffer(), text(), json(), blob(), body (ReadableStream) // Ranged reads const object = await env.MY_BUCKET.get(key, { range: { offset: 0, length: 1024 } }); // Conditional GET const object = await env.MY_BUCKET.get(key, { onlyIf: { etagMatches: '"abc123"' } }); ``` ## HEAD (Metadata Only) ```typescript const object = await env.MY_BUCKET.head(key); // Returns R2Object without body ``` ## DELETE ```typescript await env.MY_BUCKET.delete(key); await env.MY_BUCKET.delete([key1, key2, key3]); // Batch (max 1000) ``` ## LIST ```typescript const listed = await env.MY_BUCKET.list({ limit: 1000, prefix: 'photos/', cursor: cursorFromPrevious, delimiter: '/', include: ['httpMetadata', 'customMetadata'] }); // Pagination (always use truncated flag) while (listed.truncated) { const next = await env.MY_BUCKET.list({ cursor: listed.cursor }); listed.objects.push(...next.objects); listed.truncated = next.truncated; listed.cursor = next.cursor; } ``` ## Multipart Uploads ```typescript const multipart = await env.MY_BUCKET.createMultipartUpload(key, { httpMetadata: { contentType: 'video/mp4' } }); const uploadedParts: R2UploadedPart[] = []; for (let i = 0; i < partCount; i++) { const part = await multipart.uploadPart(i + 1, partData); uploadedParts.push(part); } const object = await multipart.complete(uploadedParts); // OR: await multipart.abort(); // Resume const multipart = env.MY_BUCKET.resumeMultipartUpload(key, uploadId); ``` ## Presigned URLs (S3 SDK) ```typescript import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; const s3 = new S3Client({ region: 'auto', endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY } }); const uploadUrl = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: key }), { expiresIn: 3600 }); return Response.json({ uploadUrl }); ``` ## TypeScript Interfaces ```typescript interface R2Bucket { head(key: string): Promise; get(key: string, options?: R2GetOptions): Promise; put(key: string, value: ReadableStream | ArrayBuffer | string | Blob, options?: R2PutOptions): Promise; delete(keys: string | string[]): Promise; list(options?: R2ListOptions): Promise; createMultipartUpload(key: string, options?: R2MultipartOptions): Promise; resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload; } interface R2Object { key: string; version: string; size: number; etag: string; httpEtag: string; // httpEtag is quoted, use for headers uploaded: Date; httpMetadata?: R2HTTPMetadata; customMetadata?: Record; storageClass: 'Standard' | 'InfrequentAccess'; checksums: R2Checksums; writeHttpMetadata(headers: Headers): void; } interface R2ObjectBody extends R2Object { body: ReadableStream; bodyUsed: boolean; arrayBuffer(): Promise; text(): Promise; json(): Promise; blob(): Promise; } interface R2HTTPMetadata { contentType?: string; contentDisposition?: string; contentEncoding?: string; contentLanguage?: string; cacheControl?: string; cacheExpiry?: Date; } interface R2PutOptions { httpMetadata?: R2HTTPMetadata | Headers; customMetadata?: Record; sha256?: ArrayBuffer | string; // Only ONE checksum allowed storageClass?: 'Standard' | 'InfrequentAccess'; ssecKey?: ArrayBuffer; } interface R2GetOptions { onlyIf?: R2Conditional | Headers; range?: R2Range | Headers; ssecKey?: ArrayBuffer; } interface R2ListOptions { limit?: number; prefix?: string; cursor?: string; delimiter?: string; startAfter?: string; include?: ('httpMetadata' | 'customMetadata')[]; } interface R2Objects { objects: R2Object[]; truncated: boolean; cursor?: string; delimitedPrefixes: string[]; } interface R2Conditional { etagMatches?: string; etagDoesNotMatch?: string; uploadedBefore?: Date; uploadedAfter?: Date; } interface R2Range { offset?: number; length?: number; suffix?: number; } interface R2Checksums { md5?: ArrayBuffer; sha1?: ArrayBuffer; sha256?: ArrayBuffer; sha384?: ArrayBuffer; sha512?: ArrayBuffer; } interface R2MultipartUpload { key: string; uploadId: string; uploadPart(partNumber: number, value: ReadableStream | ArrayBuffer | string | Blob): Promise; abort(): Promise; complete(uploadedParts: R2UploadedPart[]): Promise; } interface R2UploadedPart { partNumber: number; etag: string; } ``` ## CLI Operations ```bash wrangler r2 object put my-bucket/file.txt --file=./local.txt wrangler r2 object get my-bucket/file.txt --file=./download.txt wrangler r2 object delete my-bucket/file.txt wrangler r2 object list my-bucket --prefix=photos/ ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2/configuration.md ================================================ # R2 Configuration ## Workers Binding **wrangler.jsonc:** ```jsonc { "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "my-bucket-name" } ] } ``` ## TypeScript Types ```typescript interface Env { MY_BUCKET: R2Bucket; } export default { async fetch(request: Request, env: Env): Promise { const object = await env.MY_BUCKET.get('file.txt'); return new Response(object?.body); } } ``` ## S3 SDK Setup ```typescript import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'auto', endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY } }); await s3.send(new PutObjectCommand({ Bucket: 'my-bucket', Key: 'file.txt', Body: data, StorageClass: 'STANDARD' // or 'STANDARD_IA' })); ``` ## Location Hints ```bash wrangler r2 bucket create my-bucket --location=enam # Hints: wnam, enam, weur, eeur, apac, oc # Jurisdictions (override hint): --jurisdiction=eu (or fedramp) ``` ## CORS Configuration CORS must be configured via S3 SDK or dashboard (not available in Workers API): ```typescript import { S3Client, PutBucketCorsCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'auto', endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY } }); await s3.send(new PutBucketCorsCommand({ Bucket: 'my-bucket', CORSConfiguration: { CORSRules: [{ AllowedOrigins: ['https://example.com'], AllowedMethods: ['GET', 'PUT', 'HEAD'], AllowedHeaders: ['*'], ExposeHeaders: ['ETag'], MaxAgeSeconds: 3600 }] } })); ``` ## Object Lifecycles ```typescript import { PutBucketLifecycleConfigurationCommand } from '@aws-sdk/client-s3'; await s3.send(new PutBucketLifecycleConfigurationCommand({ Bucket: 'my-bucket', LifecycleConfiguration: { Rules: [ { ID: 'expire-old-logs', Status: 'Enabled', Prefix: 'logs/', Expiration: { Days: 90 } }, { ID: 'transition-to-ia', Status: 'Enabled', Prefix: 'archives/', Transitions: [{ Days: 30, StorageClass: 'STANDARD_IA' }] } ] } })); ``` ## API Token Scopes When creating R2 tokens, set minimal permissions: | Permission | Use Case | |------------|----------| | Object Read | Public serving, downloads | | Object Write | Uploads only | | Object Read & Write | Full object operations | | Admin Read & Write | Bucket management, CORS, lifecycles | **Best practice:** Separate tokens for Workers (read/write) vs admin tasks (CORS, lifecycles). ## Event Notifications ```jsonc // wrangler.jsonc { "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "my-bucket", "event_notifications": [ { "queue": "r2-events", "actions": ["PutObject", "DeleteObject", "CompleteMultipartUpload"] } ] } ], "queues": { "producers": [{ "binding": "R2_EVENTS", "queue": "r2-events" }], "consumers": [{ "queue": "r2-events", "max_batch_size": 10 }] } } ``` ## Bucket Management ```bash wrangler r2 bucket create my-bucket --location=enam --storage-class=Standard wrangler r2 bucket list wrangler r2 bucket info my-bucket wrangler r2 bucket delete my-bucket # Must be empty wrangler r2 bucket update-storage-class my-bucket --storage-class=InfrequentAccess # Public bucket via dashboard wrangler r2 bucket domain add my-bucket --domain=files.example.com ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2/gotchas.md ================================================ # R2 Gotchas & Troubleshooting ## List Truncation ```typescript // ❌ WRONG: Don't compare object count when using include while (listed.objects.length < options.limit) { ... } // ✅ CORRECT: Always use truncated property while (listed.truncated) { const next = await env.MY_BUCKET.list({ cursor: listed.cursor }); // ... } ``` **Reason:** `include` with metadata may return fewer objects per page to fit metadata. ## ETag Format ```typescript // ❌ WRONG: Using etag (unquoted) in headers headers.set('etag', object.etag); // Missing quotes // ✅ CORRECT: Use httpEtag (quoted) headers.set('etag', object.httpEtag); ``` ## Checksum Limits Only ONE checksum algorithm allowed per PUT: ```typescript // ❌ WRONG: Multiple checksums await env.MY_BUCKET.put(key, data, { md5: hash1, sha256: hash2 }); // Error // ✅ CORRECT: Pick one await env.MY_BUCKET.put(key, data, { sha256: hash }); ``` ## Multipart Requirements - All parts must be uniform size (except last part) - Part numbers start at 1 (not 0) - Uncompleted uploads auto-abort after 7 days - `resumeMultipartUpload` doesn't validate uploadId existence ## Conditional Operations ```typescript // Precondition failure returns object WITHOUT body const object = await env.MY_BUCKET.get(key, { onlyIf: { etagMatches: '"wrong"' } }); // Check for body, not just null if (!object) return new Response('Not found', { status: 404 }); if (!object.body) return new Response(null, { status: 304 }); // Precondition failed ``` ## Key Validation ```typescript // ❌ DANGEROUS: Path traversal const key = url.pathname.slice(1); // Could be ../../../etc/passwd await env.MY_BUCKET.get(key); // ✅ SAFE: Validate keys if (!key || key.includes('..') || key.startsWith('/')) { return new Response('Invalid key', { status: 400 }); } ``` ## Storage Class Pitfalls - InfrequentAccess: 30-day minimum billing (even if deleted early) - Can't transition IA → Standard via lifecycle (use S3 CopyObject) - Retrieval fees apply for IA reads ## Stream Length Requirement ```typescript // ❌ WRONG: Streaming unknown length fails silently const response = await fetch(url); await env.MY_BUCKET.put(key, response.body); // May fail without error // ✅ CORRECT: Buffer or use Content-Length const data = await response.arrayBuffer(); await env.MY_BUCKET.put(key, data); // OR: Pass Content-Length if known const object = await env.MY_BUCKET.put(key, request.body, { httpMetadata: { contentLength: parseInt(request.headers.get('content-length') || '0') } }); ``` **Reason:** R2 requires known length for streams. Unknown length may cause silent truncation. ## S3 SDK Region Configuration ```typescript // ❌ WRONG: Missing region breaks ALL S3 SDK calls const s3 = new S3Client({ endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { ... } }); // ✅ CORRECT: MUST set region='auto' const s3 = new S3Client({ region: 'auto', // REQUIRED endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { ... } }); ``` **Reason:** S3 SDK requires region. R2 uses 'auto' as placeholder. ## Local Development Limits ```typescript // ❌ Miniflare/wrangler dev: Limited R2 support // - No multipart uploads // - No presigned URLs (requires S3 SDK + network) // - Memory-backed storage (lost on restart) // ✅ Use remote bindings for full features wrangler dev --remote // OR: Conditional logic if (env.ENVIRONMENT === 'development') { // Fallback for local dev } else { // Full R2 features } ``` ## Presigned URL Expiry ```typescript // ❌ WRONG: URL expires but no client validation const url = await getSignedUrl(s3, command, { expiresIn: 60 }); // 61 seconds later: 403 Forbidden // ✅ CORRECT: Return expiry to client return Response.json({ uploadUrl: url, expiresAt: new Date(Date.now() + 60000).toISOString() }); ``` ## Limits | Limit | Value | |-------|-------| | Object size | 5 TB | | Multipart part count | 10,000 | | Multipart part min size | 5 MB (except last) | | Batch delete | 1,000 keys | | List limit | 1,000 per request | | Key size | 1024 bytes | | Custom metadata | 2 KB per object | | Presigned URL max expiry | 7 days | ## Common Errors ### "Stream upload failed" / Silent Truncation **Cause:** Stream length unknown or Content-Length missing **Solution:** Buffer data or pass explicit Content-Length ### "Invalid credentials" / S3 SDK **Cause:** Missing `region: 'auto'` in S3Client config **Solution:** Always set `region: 'auto'` for R2 ### "Object not found" **Cause:** Object key doesn't exist or was deleted **Solution:** Verify object key correct, check if object was deleted, ensure bucket correct ### "List compatibility error" **Cause:** Missing or old compatibility_date, or flag not enabled **Solution:** Set `compatibility_date >= 2022-08-04` or enable `r2_list_honor_include` flag ### "Multipart upload failed" **Cause:** Part sizes not uniform or incorrect part number **Solution:** Ensure uniform size except final part, verify part numbers start at 1 ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2/patterns.md ================================================ # R2 Patterns & Best Practices ## Streaming Large Files ```typescript const object = await env.MY_BUCKET.get(key); if (!object) return new Response('Not found', { status: 404 }); const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); return new Response(object.body, { headers }); ``` ## Conditional GET (304 Not Modified) ```typescript const ifNoneMatch = request.headers.get('if-none-match'); const object = await env.MY_BUCKET.get(key, { onlyIf: { etagDoesNotMatch: ifNoneMatch?.replace(/"/g, '') || '' } }); if (!object) return new Response('Not found', { status: 404 }); if (!object.body) return new Response(null, { status: 304, headers: { 'etag': object.httpEtag } }); return new Response(object.body, { headers: { 'etag': object.httpEtag } }); ``` ## Upload with Validation ```typescript const key = url.pathname.slice(1); if (!key || key.includes('..')) return new Response('Invalid key', { status: 400 }); const object = await env.MY_BUCKET.put(key, request.body, { httpMetadata: { contentType: request.headers.get('content-type') || 'application/octet-stream' }, customMetadata: { uploadedAt: new Date().toISOString(), ip: request.headers.get('cf-connecting-ip') || 'unknown' } }); return Response.json({ key: object.key, size: object.size, etag: object.httpEtag }); ``` ## Multipart with Progress ```typescript const PART_SIZE = 5 * 1024 * 1024; // 5MB const partCount = Math.ceil(file.size / PART_SIZE); const multipart = await env.MY_BUCKET.createMultipartUpload(key, { httpMetadata: { contentType: file.type } }); const uploadedParts: R2UploadedPart[] = []; try { for (let i = 0; i < partCount; i++) { const start = i * PART_SIZE; const part = await multipart.uploadPart(i + 1, file.slice(start, start + PART_SIZE)); uploadedParts.push(part); onProgress?.(Math.round(((i + 1) / partCount) * 100)); } return await multipart.complete(uploadedParts); } catch (error) { await multipart.abort(); throw error; } ``` ## Batch Delete ```typescript async function deletePrefix(prefix: string, env: Env) { let cursor: string | undefined; let truncated = true; while (truncated) { const listed = await env.MY_BUCKET.list({ prefix, limit: 1000, cursor }); if (listed.objects.length > 0) { await env.MY_BUCKET.delete(listed.objects.map(o => o.key)); } truncated = listed.truncated; cursor = listed.cursor; } } ``` ## Checksum Validation & Storage Transitions ```typescript // Upload with checksum const hash = await crypto.subtle.digest('SHA-256', data); await env.MY_BUCKET.put(key, data, { sha256: hash }); // Transition storage class (requires S3 SDK) import { S3Client, CopyObjectCommand } from '@aws-sdk/client-s3'; await s3.send(new CopyObjectCommand({ Bucket: 'my-bucket', Key: key, CopySource: `/my-bucket/${key}`, StorageClass: 'STANDARD_IA' })); ``` ## Client-Side Uploads (Presigned URLs) ```typescript import { S3Client } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { PutObjectCommand } from '@aws-sdk/client-s3'; // Worker: Generate presigned upload URL const s3 = new S3Client({ region: 'auto', endpoint: `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY } }); const url = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: key }), { expiresIn: 3600 }); return Response.json({ uploadUrl: url }); // Client: Upload directly const { uploadUrl } = await fetch('/api/upload-url').then(r => r.json()); await fetch(uploadUrl, { method: 'PUT', body: file }); ``` ## Caching with Cache API ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const cache = caches.default; const url = new URL(request.url); const cacheKey = new Request(url.toString(), request); // Check cache first let response = await cache.match(cacheKey); if (response) return response; // Fetch from R2 const key = url.pathname.slice(1); const object = await env.MY_BUCKET.get(key); if (!object) return new Response('Not found', { status: 404 }); const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); headers.set('cache-control', 'public, max-age=31536000, immutable'); response = new Response(object.body, { headers }); // Cache for subsequent requests ctx.waitUntil(cache.put(cacheKey, response.clone())); return response; } }; ``` ## Public Bucket with Custom Domain ```typescript export default { async fetch(request: Request, env: Env): Promise { // CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, HEAD', 'access-control-max-age': '86400' } }); } const key = new URL(request.url).pathname.slice(1); if (!key) return Response.redirect('/index.html', 302); const object = await env.MY_BUCKET.get(key); if (!object) return new Response('Not found', { status: 404 }); const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); headers.set('access-control-allow-origin', '*'); headers.set('cache-control', 'public, max-age=31536000, immutable'); return new Response(object.body, { headers }); } }; ``` ## r2.dev Public URLs Enable r2.dev in dashboard for simple public access: `https://pub-${hashId}.r2.dev/${key}` Or add custom domain via dashboard: `https://files.example.com/${key}` **Limitations:** No auth, bucket-level CORS, no cache override. ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-data-catalog/README.md ================================================ # Cloudflare R2 Data Catalog Skill Reference Expert guidance for Cloudflare R2 Data Catalog - Apache Iceberg catalog built into R2 buckets. ## Reading Order **New to R2 Data Catalog?** Start here: 1. Read "What is R2 Data Catalog?" and "When to Use" below 2. [configuration.md](configuration.md) - Enable catalog, create tokens 3. [patterns.md](patterns.md) - PyIceberg setup and common patterns 4. [api.md](api.md) - REST API reference as needed 5. [gotchas.md](gotchas.md) - Troubleshooting when issues arise **Quick reference?** Jump to: - [Enable catalog on bucket](configuration.md#enable-catalog-on-bucket) - [PyIceberg connection pattern](patterns.md#pyiceberg-connection-pattern) - [Permission errors](gotchas.md#permission-errors) ## What is R2 Data Catalog? R2 Data Catalog is a **managed Apache Iceberg REST catalog** built directly into R2 buckets. It provides: - **Apache Iceberg tables** - ACID transactions, schema evolution, time-travel queries - **Zero-egress costs** - Query from any cloud/region without data transfer fees - **Standard REST API** - Works with Spark, PyIceberg, Snowflake, Trino, DuckDB - **No infrastructure** - Fully managed, no catalog servers to run - **Public beta** - Available to all R2 subscribers, no extra cost beyond R2 storage ### What is Apache Iceberg? Open table format for analytics datasets in object storage. Features: - **ACID transactions** - Safe concurrent reads/writes - **Metadata optimization** - Fast queries without full scans - **Schema evolution** - Add/rename/delete columns without rewrites - **Time-travel** - Query historical snapshots - **Partitioning** - Organize data for efficient queries ## When to Use **Use R2 Data Catalog for:** - **Log analytics** - Store and query application/system logs - **Data lakes/warehouses** - Analytical datasets queried by multiple engines - **BI pipelines** - Aggregate data for dashboards and reports - **Multi-cloud analytics** - Share data across clouds without egress fees - **Time-series data** - Event streams, metrics, sensor data **Don't use for:** - **Transactional workloads** - Use D1 or external database instead - **Sub-second latency** - Iceberg optimized for batch/analytical queries - **Small datasets (<1GB)** - Setup overhead not worth it - **Unstructured data** - Store files directly in R2, not as Iceberg tables ## Architecture ``` ┌─────────────────────────────────────────────────┐ │ Query Engines │ │ (PyIceberg, Spark, Trino, Snowflake, DuckDB) │ └────────────────┬────────────────────────────────┘ │ │ REST API (OAuth2 token) ▼ ┌─────────────────────────────────────────────────┐ │ R2 Data Catalog (Managed Iceberg REST Catalog)│ │ • Namespace/table metadata │ │ • Transaction coordination │ │ • Snapshot management │ └────────────────┬────────────────────────────────┘ │ │ Vended credentials ▼ ┌─────────────────────────────────────────────────┐ │ R2 Bucket Storage │ │ • Parquet data files │ │ • Metadata files │ │ • Manifest files │ └─────────────────────────────────────────────────┘ ``` **Key concepts:** - **Catalog URI** - REST endpoint for catalog operations (e.g., `https://.r2.cloudflarestorage.com/iceberg/`) - **Warehouse** - Logical grouping of tables (typically same as bucket name) - **Namespace** - Schema/database containing tables (e.g., `logs`, `analytics`) - **Table** - Iceberg table with schema, data files, snapshots - **Vended credentials** - Temporary S3 credentials catalog provides for data access ## Limits | Resource | Limit | Notes | |----------|-------|-------| | Namespaces per catalog | No hard limit | Organize tables logically | | Tables per namespace | <10,000 recommended | Performance degrades beyond this | | Files per table | <100,000 recommended | Run compaction regularly | | Snapshots per table | Configurable retention | Expire >7 days old | | Partitions per table | 100-1,000 optimal | Too many = slow metadata ops | | Table size | Same as R2 bucket | 10GB-10TB+ common | | API rate limits | Standard R2 API limits | Shared with R2 storage operations | | Target file size | 128-512 MB | After compaction | ## Current Status **Public Beta** (as of Jan 2026) - Available to all R2 subscribers - No extra cost beyond standard R2 storage/operations - Production-ready, but breaking changes possible - Supports: namespaces, tables, snapshots, compaction, time-travel, table maintenance ## Decision Tree: Is R2 Data Catalog Right For You? ``` Start → Need analytics on object storage data? │ ├─ No → Use R2 directly for object storage │ └─ Yes → Dataset >1GB with structured schema? │ ├─ No → Too small, use R2 + ad-hoc queries │ └─ Yes → Need ACID transactions or schema evolution? │ ├─ No → Consider simpler solutions (Parquet on R2) │ └─ Yes → Need multi-cloud/multi-tool access? │ ├─ No → D1 or external DB may be simpler │ └─ Yes → ✅ Use R2 Data Catalog ``` **Quick check:** If you answer "yes" to all: - Dataset >1GB and growing - Structured/tabular data (logs, events, metrics) - Multiple query tools or cloud environments - Need versioning, schema changes, or concurrent access → R2 Data Catalog is a good fit. ## In This Reference - **[configuration.md](configuration.md)** - Enable catalog, create API tokens, connect clients - **[api.md](api.md)** - REST endpoints, operations, maintenance - **[patterns.md](patterns.md)** - PyIceberg examples, common use cases - **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations ## See Also - [Cloudflare R2 Data Catalog Docs](https://developers.cloudflare.com/r2/data-catalog/) - [Apache Iceberg Docs](https://iceberg.apache.org/) - [PyIceberg Docs](https://py.iceberg.apache.org/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-data-catalog/api.md ================================================ # API Reference R2 Data Catalog exposes standard [Apache Iceberg REST Catalog API](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml). ## Quick Reference **Most common operations:** | Task | PyIceberg Code | |------|----------------| | Connect | `RestCatalog(name="r2", warehouse=bucket, uri=uri, token=token)` | | List namespaces | `catalog.list_namespaces()` | | Create namespace | `catalog.create_namespace("logs")` | | Create table | `catalog.create_table(("ns", "table"), schema=schema)` | | Load table | `catalog.load_table(("ns", "table"))` | | Append data | `table.append(pyarrow_table)` | | Query data | `table.scan().to_pandas()` | | Compact files | `table.rewrite_data_files(target_file_size_bytes=128*1024*1024)` | | Expire snapshots | `table.expire_snapshots(older_than=timestamp_ms, retain_last=10)` | ## REST Endpoints Base: `https://.r2.cloudflarestorage.com/iceberg/` | Operation | Method | Path | |-----------|--------|------| | Catalog config | GET | `/v1/config` | | List namespaces | GET | `/v1/namespaces` | | Create namespace | POST | `/v1/namespaces` | | Delete namespace | DELETE | `/v1/namespaces/{ns}` | | List tables | GET | `/v1/namespaces/{ns}/tables` | | Create table | POST | `/v1/namespaces/{ns}/tables` | | Load table | GET | `/v1/namespaces/{ns}/tables/{table}` | | Update table | POST | `/v1/namespaces/{ns}/tables/{table}` | | Delete table | DELETE | `/v1/namespaces/{ns}/tables/{table}` | | Rename table | POST | `/v1/tables/rename` | **Authentication:** Bearer token in header: `Authorization: Bearer ` ## PyIceberg Client API Most users use PyIceberg, not raw REST. ### Connection ```python from pyiceberg.catalog.rest import RestCatalog catalog = RestCatalog( name="my_catalog", warehouse="", uri="", token="", ) ``` ### Namespace Operations ```python from pyiceberg.exceptions import NamespaceAlreadyExistsError namespaces = catalog.list_namespaces() # [('default',), ('logs',)] catalog.create_namespace("logs", properties={"owner": "team"}) catalog.drop_namespace("logs") # Must be empty ``` ### Table Operations ```python from pyiceberg.schema import Schema from pyiceberg.types import NestedField, StringType, IntegerType schema = Schema( NestedField(1, "id", IntegerType(), required=True), NestedField(2, "name", StringType(), required=False), ) table = catalog.create_table(("logs", "app_logs"), schema=schema) tables = catalog.list_tables("logs") table = catalog.load_table(("logs", "app_logs")) catalog.rename_table(("logs", "old"), ("logs", "new")) ``` ### Data Operations ```python import pyarrow as pa data = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) table.append(data) table.overwrite(data) # Read with filters scan = table.scan(row_filter="id > 100", selected_fields=["id", "name"]) df = scan.to_pandas() ``` ### Schema Evolution ```python from pyiceberg.types import IntegerType, LongType with table.update_schema() as update: update.add_column("user_id", IntegerType(), doc="User ID") update.rename_column("msg", "message") update.delete_column("old_field") update.update_column("id", field_type=LongType()) # int→long only ``` ### Time-Travel ```python from datetime import datetime, timedelta # Query specific snapshot or timestamp scan = table.scan(snapshot_id=table.snapshots()[-2].snapshot_id) yesterday_ms = int((datetime.now() - timedelta(days=1)).timestamp() * 1000) scan = table.scan(as_of_timestamp=yesterday_ms) ``` ### Partitioning ```python from pyiceberg.partitioning import PartitionSpec, PartitionField from pyiceberg.transforms import DayTransform from pyiceberg.types import TimestampType partition_spec = PartitionSpec( PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name="day") ) table = catalog.create_table(("events", "actions"), schema=schema, partition_spec=partition_spec) scan = table.scan(row_filter="day = '2026-01-27'") # Prunes partitions ``` ## Table Maintenance ### Compaction ```python files = table.scan().plan_files() avg_mb = sum(f.file_size_in_bytes for f in files) / len(files) / (1024**2) print(f"Files: {len(files)}, Avg: {avg_mb:.1f} MB") table.rewrite_data_files(target_file_size_bytes=128 * 1024 * 1024) ``` **When:** Avg <10MB or >1000 files. **Frequency:** High-write daily, medium weekly. ### Snapshot Expiration ```python from datetime import datetime, timedelta seven_days_ms = int((datetime.now() - timedelta(days=7)).timestamp() * 1000) table.expire_snapshots(older_than=seven_days_ms, retain_last=10) ``` **Retention:** Production 7-30d, dev 1-7d, audit 90+d. ### Orphan Cleanup ```python three_days_ms = int((datetime.now() - timedelta(days=3)).timestamp() * 1000) table.delete_orphan_files(older_than=three_days_ms) ``` ⚠️ Always expire snapshots first, use 3+ day threshold, run during low traffic. ### Full Maintenance ```python # Compact → Expire → Cleanup (in order) if len(table.scan().plan_files()) > 1000: table.rewrite_data_files(target_file_size_bytes=128 * 1024 * 1024) seven_days_ms = int((datetime.now() - timedelta(days=7)).timestamp() * 1000) table.expire_snapshots(older_than=seven_days_ms, retain_last=10) three_days_ms = int((datetime.now() - timedelta(days=3)).timestamp() * 1000) table.delete_orphan_files(older_than=three_days_ms) ``` ## Metadata Inspection ```python table = catalog.load_table(("logs", "app_logs")) print(table.schema()) print(table.current_snapshot()) print(table.properties) print(f"Files: {len(table.scan().plan_files())}") ``` ## Error Codes | Code | Meaning | Common Causes | |------|---------|---------------| | 401 | Unauthorized | Invalid/missing token | | 404 | Not Found | Catalog not enabled, namespace/table missing | | 409 | Conflict | Already exists, concurrent update | | 422 | Validation | Invalid schema, incompatible type | See [gotchas.md](gotchas.md) for detailed troubleshooting. ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-data-catalog/configuration.md ================================================ # Configuration How to enable R2 Data Catalog and configure authentication. ## Prerequisites - Cloudflare account with [R2 subscription](https://developers.cloudflare.com/r2/pricing/) - R2 bucket created - Access to Cloudflare dashboard or Wrangler CLI ## Enable Catalog on Bucket Choose one method: ### Via Wrangler (Recommended) ```bash npx wrangler r2 bucket catalog enable ``` **Output:** ``` ✅ Data Catalog enabled for bucket 'my-bucket' Catalog URI: https://.r2.cloudflarestorage.com/iceberg/my-bucket Warehouse: my-bucket ``` ### Via Dashboard 1. Navigate to **R2** → Select your bucket → **Settings** tab 2. Scroll to "R2 Data Catalog" section → Click **Enable** 3. Note the **Catalog URI** and **Warehouse name** shown **Result:** - Catalog URI: `https://.r2.cloudflarestorage.com/iceberg/` - Warehouse: `` (same as bucket name) ### Via API (Programmatic) ```bash curl -X POST \ "https://api.cloudflare.com/client/v4/accounts//r2/buckets//catalog" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` **Response:** ```json { "result": { "catalog_uri": "https://.r2.cloudflarestorage.com/iceberg/", "warehouse": "" }, "success": true } ``` ## Check Catalog Status ```bash npx wrangler r2 bucket catalog status ``` **Output:** ``` Catalog Status: enabled Catalog URI: https://.r2.cloudflarestorage.com/iceberg/my-bucket Warehouse: my-bucket ``` ## Disable Catalog (If Needed) ```bash npx wrangler r2 bucket catalog disable ``` ⚠️ **Warning:** Disabling does NOT delete tables/data. Files remain in bucket. Metadata becomes inaccessible until re-enabled. ## API Token Creation R2 Data Catalog requires API token with **both** R2 Storage + R2 Data Catalog permissions. ### Dashboard Method (Recommended) 1. Go to **R2** → **Manage R2 API Tokens** → **Create API Token** 2. Select permission level: - **Admin Read & Write** - Full catalog + storage access (read/write) - **Admin Read only** - Read-only access (for query engines) 3. Copy token value immediately (shown only once) **Permission groups included:** - `Workers R2 Data Catalog Write` (or Read) - `Workers R2 Storage Bucket Item Write` (or Read) ### API Method (Programmatic) Use Cloudflare API to create tokens programmatically. Required permissions: - `Workers R2 Data Catalog Write` (or Read) - `Workers R2 Storage Bucket Item Write` (or Read) ## Client Configuration ### PyIceberg ```python from pyiceberg.catalog.rest import RestCatalog catalog = RestCatalog( name="my_catalog", warehouse="", # Same as bucket name uri="", # From enable command token="", # From token creation ) ``` **Full example with credentials:** ```python import os from pyiceberg.catalog.rest import RestCatalog # Store credentials in environment variables WAREHOUSE = os.getenv("R2_WAREHOUSE") # e.g., "my-bucket" CATALOG_URI = os.getenv("R2_CATALOG_URI") # e.g., "https://abc123.r2.cloudflarestorage.com/iceberg/my-bucket" TOKEN = os.getenv("R2_TOKEN") # API token catalog = RestCatalog( name="r2_catalog", warehouse=WAREHOUSE, uri=CATALOG_URI, token=TOKEN, ) # Test connection print(catalog.list_namespaces()) ``` ### Spark / Trino / DuckDB See [patterns.md](patterns.md) for integration examples with other query engines. ## Connection String Format For quick reference: ``` Catalog URI: https://.r2.cloudflarestorage.com/iceberg/ Warehouse: Token: ``` **Where to find values:** | Value | Source | |-------|--------| | `` | Dashboard URL or `wrangler whoami` | | `` | R2 bucket name | | Catalog URI | Output from `wrangler r2 bucket catalog enable` | | Token | R2 API Token creation page | ## Security Best Practices 1. **Store tokens securely** - Use environment variables or secret managers, never hardcode 2. **Use least privilege** - Read-only tokens for query engines, write tokens only where needed 3. **Rotate tokens regularly** - Create new tokens, test, then revoke old ones 4. **One token per application** - Easier to track and revoke if compromised 5. **Monitor token usage** - Check R2 analytics for unexpected patterns 6. **Bucket-scoped tokens** - Create tokens per bucket, not account-wide ## Environment Variables Pattern ```bash # .env (never commit) R2_CATALOG_URI=https://.r2.cloudflarestorage.com/iceberg/ R2_WAREHOUSE= R2_TOKEN= ``` ```python import os from pyiceberg.catalog.rest import RestCatalog catalog = RestCatalog( name="r2", uri=os.getenv("R2_CATALOG_URI"), warehouse=os.getenv("R2_WAREHOUSE"), token=os.getenv("R2_TOKEN"), ) ``` ## Troubleshooting | Problem | Solution | |---------|----------| | 404 "catalog not found" | Run `wrangler r2 bucket catalog enable ` | | 401 "unauthorized" | Check token has both Catalog + Storage permissions | | 403 on data files | Token needs both permission groups | See [gotchas.md](gotchas.md) for detailed troubleshooting. ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-data-catalog/gotchas.md ================================================ # Gotchas & Troubleshooting Common problems → causes → solutions. ## Permission Errors ### 401 Unauthorized **Error:** `"401 Unauthorized"` **Cause:** Token missing R2 Data Catalog permissions. **Solution:** Use "Admin Read & Write" token (includes catalog + storage permissions). Test with `catalog.list_namespaces()`. ### 403 Forbidden **Error:** `"403 Forbidden"` on data files **Cause:** Token lacks storage permissions. **Solution:** Token needs both R2 Data Catalog + R2 Storage Bucket Item permissions. ### Token Rotation Issues **Error:** New token fails after rotation. **Solution:** Create new token → test in staging → update prod → monitor 24h → revoke old. ## Catalog URI Issues ### 404 Not Found **Error:** `"404 Catalog not found"` **Cause:** Catalog not enabled or wrong URI. **Solution:** Run `wrangler r2 bucket catalog enable `. URI must be HTTPS with `/iceberg/` and case-sensitive bucket name. ### Wrong Warehouse **Error:** Cannot create/load tables. **Cause:** Warehouse ≠ bucket name. **Solution:** Set `warehouse="bucket-name"` to match bucket exactly. ## Table and Schema Issues ### Table/Namespace Already Exists **Error:** `"TableAlreadyExistsError"` **Solution:** Use try/except to load existing or check first. ### Namespace Not Found **Error:** Cannot create table. **Solution:** Create namespace first: `catalog.create_namespace("ns")` ### Schema Evolution Errors **Error:** `"422 Validation"` on schema update. **Cause:** Incompatible change (required field, type shrink). **Solution:** Only add nullable columns, compatible type widening (int→long, float→double). ## Data and Query Issues ### Empty Scan Results **Error:** Scan returns no data. **Cause:** Incorrect filter or partition column. **Solution:** Test without filter first: `table.scan().to_pandas()`. Verify partition column names. ### Slow Queries **Error:** Performance degrades over time. **Cause:** Too many small files. **Solution:** Check file count, compact if >1000 or avg <10MB. See [api.md](api.md#compaction). ### Type Mismatch **Error:** `"Cannot cast"` on append. **Cause:** PyArrow types don't match Iceberg schema. **Solution:** Cast to int64 (Iceberg default), not int32. Check `table.schema()`. ## Compaction Issues ### Compaction Issues **Problem:** File count unchanged or compaction takes hours. **Cause:** Target size too large, or table too big for PyIceberg. **Solution:** Only compact if avg <50MB. For >1TB tables, use Spark. Run during low-traffic periods. ## Maintenance Issues ### Snapshot/Orphan Issues **Problem:** Expiration fails or orphan cleanup deletes active data. **Cause:** Too aggressive retention or wrong order. **Solution:** Always expire snapshots first with `retain_last=10`, then cleanup orphans with 3+ day threshold. ## Concurrency Issues ### Concurrent Write Conflicts **Problem:** `CommitFailedException` with multiple writers. **Cause:** Optimistic locking - simultaneous commits. **Solution:** Add retry with exponential backoff (see [patterns.md](patterns.md#pattern-6-concurrent-writes-with-retry)). ### Stale Metadata **Problem:** Old schema/data after external update. **Cause:** Cached metadata. **Solution:** Reload table: `table = catalog.load_table(("ns", "table"))` ## Performance Optimization ### Performance Tips **Scans:** Use `row_filter` and `selected_fields` to reduce data scanned. **Partitions:** 100-1000 optimal. Avoid high cardinality (millions) or low (<10). **Files:** Keep 100-500MB avg. Compact if <10MB or >10k files. ## Limits | Resource | Recommended | Impact if Exceeded | |----------|-------------|-------------------| | Tables/namespace | <10k | Slow list ops | | Files/table | <100k | Slow query planning | | Partitions/table | 100-1k | Metadata overhead | | Snapshots/table | Expire >7d | Metadata bloat | ## Common Error Messages Reference | Error Message | Likely Cause | Fix | |---------------|--------------|-----| | `401 Unauthorized` | Missing/invalid token | Check token has catalog+storage permissions | | `403 Forbidden` | Token lacks storage permissions | Add R2 Storage Bucket Item permission | | `404 Not Found` | Catalog not enabled or wrong URI | Run `wrangler r2 bucket catalog enable` | | `409 Conflict` | Table/namespace already exists | Use try/except or load existing | | `422 Unprocessable Entity` | Schema validation failed | Check type compatibility, required fields | | `CommitFailedException` | Concurrent write conflict | Add retry logic with backoff | | `NamespaceAlreadyExistsError` | Namespace exists | Use try/except or load existing | | `NoSuchTableError` | Table doesn't exist | Check namespace+table name, create first | | `TypeError: Cannot cast` | PyArrow type mismatch | Cast data to match Iceberg schema | ## Debugging Checklist When things go wrong, check in order: 1. ✅ **Catalog enabled:** `npx wrangler r2 bucket catalog status ` 2. ✅ **Token permissions:** Both R2 Data Catalog + R2 Storage in dashboard 3. ✅ **Connection test:** `catalog.list_namespaces()` succeeds 4. ✅ **URI format:** HTTPS, includes `/iceberg/`, correct bucket name 5. ✅ **Warehouse name:** Matches bucket name exactly 6. ✅ **Namespace exists:** Create before `create_table()` 7. ✅ **Enable debug logging:** `logging.basicConfig(level=logging.DEBUG)` 8. ✅ **PyIceberg version:** `pip install --upgrade pyiceberg` (≥0.5.0) 9. ✅ **File health:** Compact if >1000 files or avg <10MB 10. ✅ **Snapshot count:** Expire if >100 snapshots ## Enable Debug Logging ```python import logging logging.basicConfig(level=logging.DEBUG) # Now operations show HTTP requests/responses ``` ## Resources - [Cloudflare Community](https://community.cloudflare.com/c/developers/workers/40) - [Cloudflare Discord](https://discord.cloudflare.com) - #r2 channel - [PyIceberg GitHub](https://github.com/apache/iceberg-python/issues) - [Apache Iceberg Slack](https://iceberg.apache.org/community/) ## Next Steps - [patterns.md](patterns.md) - Working examples - [api.md](api.md) - API reference ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-data-catalog/patterns.md ================================================ # Common Patterns Practical patterns for R2 Data Catalog with PyIceberg. ## PyIceberg Connection ```python import os from pyiceberg.catalog.rest import RestCatalog from pyiceberg.exceptions import NamespaceAlreadyExistsError catalog = RestCatalog( name="r2_catalog", warehouse=os.getenv("R2_WAREHOUSE"), # bucket name uri=os.getenv("R2_CATALOG_URI"), # catalog endpoint token=os.getenv("R2_TOKEN"), # API token ) # Create namespace (idempotent) try: catalog.create_namespace("default") except NamespaceAlreadyExistsError: pass ``` ## Pattern 1: Log Analytics Pipeline Ingest logs incrementally, query by time/level. ```python import pyarrow as pa from datetime import datetime from pyiceberg.schema import Schema from pyiceberg.types import NestedField, TimestampType, StringType, IntegerType from pyiceberg.partitioning import PartitionSpec, PartitionField from pyiceberg.transforms import DayTransform # Create partitioned table (once) schema = Schema( NestedField(1, "timestamp", TimestampType(), required=True), NestedField(2, "level", StringType(), required=True), NestedField(3, "service", StringType(), required=True), NestedField(4, "message", StringType(), required=False), ) partition_spec = PartitionSpec( PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name="day") ) catalog.create_namespace("logs") table = catalog.create_table(("logs", "app_logs"), schema=schema, partition_spec=partition_spec) # Append logs (incremental) data = pa.table({ "timestamp": [datetime(2026, 1, 27, 10, 30, 0)], "level": ["ERROR"], "service": ["auth-service"], "message": ["Failed login"], }) table.append(data) # Query by time + level (leverages partitioning) scan = table.scan(row_filter="level = 'ERROR' AND day = '2026-01-27'") errors = scan.to_pandas() ``` ## Pattern 2: Time-Travel Queries ```python from datetime import datetime, timedelta table = catalog.load_table(("logs", "app_logs")) # Query specific snapshot snapshot_id = table.current_snapshot().snapshot_id data = table.scan(snapshot_id=snapshot_id).to_pandas() # Query as of timestamp (yesterday) yesterday_ms = int((datetime.now() - timedelta(days=1)).timestamp() * 1000) data = table.scan(as_of_timestamp=yesterday_ms).to_pandas() ``` ## Pattern 3: Schema Evolution ```python from pyiceberg.types import StringType table = catalog.load_table(("users", "profiles")) with table.update_schema() as update: update.add_column("email", StringType(), required=False) update.rename_column("name", "full_name") # Old readers ignore new columns, new readers see nulls for old data ``` ## Pattern 4: Partitioned Tables ```python from pyiceberg.partitioning import PartitionSpec, PartitionField from pyiceberg.transforms import DayTransform, IdentityTransform # Partition by day + country partition_spec = PartitionSpec( PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name="day"), PartitionField(source_id=2, field_id=1001, transform=IdentityTransform(), name="country"), ) table = catalog.create_table(("events", "user_events"), schema=schema, partition_spec=partition_spec) # Queries prune partitions automatically scan = table.scan(row_filter="country = 'US' AND day = '2026-01-27'") ``` ## Pattern 5: Table Maintenance ```python from datetime import datetime, timedelta table = catalog.load_table(("logs", "app_logs")) # Compact → expire → cleanup (in order) table.rewrite_data_files(target_file_size_bytes=128 * 1024 * 1024) seven_days_ms = int((datetime.now() - timedelta(days=7)).timestamp() * 1000) table.expire_snapshots(older_than=seven_days_ms, retain_last=10) three_days_ms = int((datetime.now() - timedelta(days=3)).timestamp() * 1000) table.delete_orphan_files(older_than=three_days_ms) ``` See [api.md](api.md#table-maintenance) for detailed parameters. ## Pattern 6: Concurrent Writes with Retry ```python from pyiceberg.exceptions import CommitFailedException import time def append_with_retry(table, data, max_retries=3): for attempt in range(max_retries): try: table.append(data) return except CommitFailedException: if attempt == max_retries - 1: raise time.sleep(2 ** attempt) ``` ## Pattern 7: Upsert Simulation ```python import pandas as pd import pyarrow as pa # Read → merge → overwrite (not atomic, use Spark MERGE INTO for production) existing = table.scan().to_pandas() new_data = pd.DataFrame({"id": [1, 3], "value": [100, 300]}) merged = pd.concat([existing, new_data]).drop_duplicates(subset=["id"], keep="last") table.overwrite(pa.Table.from_pandas(merged)) ``` ## Pattern 8: DuckDB Integration ```python import duckdb arrow_table = table.scan().to_arrow() con = duckdb.connect() con.register("logs", arrow_table) result = con.execute("SELECT level, COUNT(*) FROM logs GROUP BY level").fetchdf() ``` ## Pattern 9: Monitor Table Health ```python files = table.scan().plan_files() avg_mb = sum(f.file_size_in_bytes for f in files) / len(files) / (1024**2) print(f"Files: {len(files)}, Avg: {avg_mb:.1f}MB, Snapshots: {len(table.snapshots())}") if avg_mb < 10 or len(files) > 1000: print("⚠️ Needs compaction") ``` ## Best Practices | Area | Guideline | |------|-----------| | **Partitioning** | Use day/hour for time-series; 100-1000 partitions; avoid high cardinality | | **File sizes** | Target 128-512MB; compact when avg <10MB or >10k files | | **Schema** | Add columns as nullable (`required=False`); batch changes | | **Maintenance** | Compact high-write daily/weekly; expire snapshots 7-30d; cleanup orphans after | | **Concurrency** | Reads automatic; writes to different partitions safe; retry same partition | | **Performance** | Filter on partitions; select only needed columns; batch appends 100MB+ | ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-sql/README.md ================================================ # Cloudflare R2 SQL Skill Reference Expert guidance for Cloudflare R2 SQL - serverless distributed query engine for Apache Iceberg tables. ## Reading Order **New to R2 SQL?** Start here: 1. Read "What is R2 SQL?" and "When to Use" below 2. [configuration.md](configuration.md) - Enable catalog, create tokens 3. [patterns.md](patterns.md) - Wrangler CLI and integration examples 4. [api.md](api.md) - SQL syntax and query reference 5. [gotchas.md](gotchas.md) - Limitations and troubleshooting **Quick reference?** Jump to: - [Run a query via Wrangler](patterns.md#wrangler-cli-query) - [SQL syntax reference](api.md#sql-syntax) - [ORDER BY limitations](gotchas.md#order-by-limitations) ## What is R2 SQL? R2 SQL is Cloudflare's **serverless distributed analytics query engine** for querying Apache Iceberg tables in R2 Data Catalog. Features: - **Serverless** - No clusters to manage, no infrastructure - **Distributed** - Leverages Cloudflare's global network for parallel execution - **SQL interface** - Familiar SQL syntax for analytics queries - **Zero egress fees** - Query from any cloud/region without data transfer costs - **Open beta** - Free during beta (standard R2 storage costs apply) ### What is Apache Iceberg? Open table format for large-scale analytics datasets in object storage: - **ACID transactions** - Safe concurrent reads/writes - **Metadata optimization** - Fast queries without full table scans - **Schema evolution** - Add/rename/drop columns without rewrites - **Partitioning** - Organize data for efficient pruning ## When to Use **Use R2 SQL for:** - **Log analytics** - Query application/system logs with WHERE filters and aggregations - **BI dashboards** - Generate reports from large analytical datasets - **Fraud detection** - Analyze transaction patterns with GROUP BY/HAVING - **Multi-cloud analytics** - Query data from any cloud without egress fees - **Ad-hoc exploration** - Run SQL queries on Iceberg tables via Wrangler CLI **Don't use R2 SQL for:** - **Workers/Pages runtime** - R2 SQL has no Workers binding, use HTTP API from external systems - **Real-time queries (<100ms)** - Optimized for analytical batch queries, not OLTP - **Complex joins/CTEs** - Limited SQL feature set (no JOINs, subqueries, CTEs currently) - **Small datasets (<1GB)** - Setup overhead not justified ## Decision Tree: Need to Query R2 Data? ``` Do you need to query structured data in R2? ├─ YES, data is in Iceberg tables │ ├─ Need SQL interface? → Use R2 SQL (this reference) │ ├─ Need Python API? → See r2-data-catalog reference (PyIceberg) │ └─ Need other engine? → See r2-data-catalog reference (Spark, Trino, etc.) │ ├─ YES, but not in Iceberg format │ ├─ Streaming data? → Use Pipelines to write to Data Catalog, then R2 SQL │ └─ Static files? → Use PyIceberg to create Iceberg tables, then R2 SQL │ └─ NO, just need object storage → Use R2 reference (not R2 SQL) ``` ## Architecture Overview **Query Planner:** - Top-down metadata investigation with multi-layer pruning - Partition-level, column-level, and row-group pruning - Streaming pipeline - execution starts before planning completes - Early termination with LIMIT - stops when result complete **Query Execution:** - Coordinator distributes work to workers across Cloudflare network - Workers run Apache DataFusion for parallel query execution - Parquet column pruning - reads only required columns - Ranged reads from R2 for efficiency **Aggregation Strategies:** - Scatter-gather - simple aggregations (SUM, COUNT, AVG) - Shuffling - ORDER BY/HAVING on aggregates via hash partitioning ## Quick Start ```bash # 1. Enable R2 Data Catalog on bucket npx wrangler r2 bucket catalog enable my-bucket # 2. Create API token (Admin Read & Write) # Dashboard: R2 → Manage API tokens → Create API token # 3. Set environment variable export WRANGLER_R2_SQL_AUTH_TOKEN= # 4. Run query npx wrangler r2 sql query "my-bucket" "SELECT * FROM default.my_table LIMIT 10" ``` ## Important Limitations **CRITICAL: No Workers Binding** - R2 SQL cannot be called directly from Workers/Pages code - For programmatic access, use HTTP API from external systems - Or query via PyIceberg, Spark, etc. (see r2-data-catalog reference) **SQL Feature Set:** - No JOINs, CTEs, subqueries, window functions - ORDER BY supports aggregation columns (not just partition keys) - LIMIT max 10,000 (default 500) - See [gotchas.md](gotchas.md) for complete limitations ## In This Reference - **[configuration.md](configuration.md)** - Enable catalog, create API tokens - **[api.md](api.md)** - SQL syntax, functions, operators, data types - **[patterns.md](patterns.md)** - Wrangler CLI, HTTP API, Pipelines, PyIceberg - **[gotchas.md](gotchas.md)** - Limitations, troubleshooting, performance tips ## See Also - [r2-data-catalog](../r2-data-catalog/) - PyIceberg, REST API, external engines - [pipelines](../pipelines/) - Streaming ingestion to Iceberg tables - [r2](../r2/) - R2 object storage fundamentals - [Cloudflare R2 SQL Docs](https://developers.cloudflare.com/r2-sql/) - [R2 SQL Deep Dive Blog](https://blog.cloudflare.com/r2-sql-deep-dive/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-sql/api.md ================================================ # R2 SQL API Reference SQL syntax, functions, operators, and data types for R2 SQL queries. ## SQL Syntax ```sql SELECT column_list | aggregation_function FROM [namespace.]table_name WHERE conditions [GROUP BY column_list] [HAVING conditions] [ORDER BY column | aggregation_function [DESC | ASC]] [LIMIT number] ``` ## Schema Discovery ```sql SHOW DATABASES; -- List namespaces SHOW NAMESPACES; -- Alias for SHOW DATABASES SHOW SCHEMAS; -- Alias for SHOW DATABASES SHOW TABLES IN namespace; -- List tables in namespace DESCRIBE namespace.table; -- Show table schema, partition keys ``` ## SELECT Clause ```sql -- All columns SELECT * FROM logs.http_requests; -- Specific columns SELECT user_id, timestamp, status FROM logs.http_requests; ``` **Limitations:** No column aliases, expressions, or nested column access ## WHERE Clause ### Operators | Operator | Example | |----------|---------| | `=`, `!=`, `<`, `<=`, `>`, `>=` | `status = 200` | | `LIKE` | `user_agent LIKE '%Chrome%'` | | `BETWEEN` | `timestamp BETWEEN '2025-01-01T00:00:00Z' AND '2025-01-31T23:59:59Z'` | | `IS NULL`, `IS NOT NULL` | `email IS NOT NULL` | | `AND`, `OR` | `status = 200 AND method = 'GET'` | Use parentheses for precedence: `(status = 404 OR status = 500) AND method = 'POST'` ## Aggregation Functions | Function | Description | |----------|-------------| | `COUNT(*)` | Count all rows | | `COUNT(column)` | Count non-null values | | `COUNT(DISTINCT column)` | Count unique values | | `SUM(column)`, `AVG(column)` | Numeric aggregations | | `MIN(column)`, `MAX(column)` | Min/max values | ```sql -- Multiple aggregations with GROUP BY SELECT region, COUNT(*), SUM(amount), AVG(amount) FROM sales.transactions WHERE sale_date >= '2024-01-01' GROUP BY region; ``` ## HAVING Clause Filter aggregated results (after GROUP BY): ```sql SELECT category, SUM(amount) FROM sales.transactions GROUP BY category HAVING SUM(amount) > 10000; ``` ## ORDER BY Clause Sort results by: - **Partition key columns** - Always supported - **Aggregation functions** - Supported via shuffle strategy ```sql -- Order by partition key SELECT * FROM logs.requests ORDER BY timestamp DESC LIMIT 100; -- Order by aggregation (repeat function, aliases not supported) SELECT region, SUM(amount) FROM sales.transactions GROUP BY region ORDER BY SUM(amount) DESC; ``` **Limitations:** Cannot order by non-partition columns. See [gotchas.md](gotchas.md#order-by-limitations) ## LIMIT Clause ```sql SELECT * FROM logs.requests LIMIT 100; ``` | Setting | Value | |---------|-------| | Min | 1 | | Max | 10,000 | | Default | 500 | **Always use LIMIT** to enable early termination optimization. ## Data Types | Type | SQL Literal | Example | |------|-------------|---------| | `integer` | Unquoted number | `42`, `-10` | | `float` | Decimal number | `3.14`, `-0.5` | | `string` | Single quotes | `'hello'`, `'GET'` | | `boolean` | Keyword | `true`, `false` | | `timestamp` | RFC3339 string | `'2025-01-01T00:00:00Z'` | | `date` | ISO 8601 date | `'2025-01-01'` | ### Type Safety - Quote strings with single quotes: `'value'` - Timestamps must be RFC3339: `'2025-01-01T00:00:00Z'` (include timezone) - Dates must be ISO 8601: `'2025-01-01'` (YYYY-MM-DD) - No implicit conversions ```sql -- ✅ Correct WHERE status = 200 AND method = 'GET' AND timestamp > '2025-01-01T00:00:00Z' -- ❌ Wrong WHERE status = '200' -- string instead of integer WHERE timestamp > '2025-01-01' -- missing time/timezone WHERE method = GET -- unquoted string ``` ## Query Result Format JSON array of objects: ```json [ {"user_id": "user_123", "timestamp": "2025-01-15T10:30:00Z", "status": 200}, {"user_id": "user_456", "timestamp": "2025-01-15T10:31:00Z", "status": 404} ] ``` ## See Also - [patterns.md](patterns.md) - Query examples and use cases - [gotchas.md](gotchas.md) - SQL limitations and error handling - [configuration.md](configuration.md) - Setup and authentication ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-sql/configuration.md ================================================ # R2 SQL Configuration Setup and configuration for R2 SQL queries. ## Prerequisites - R2 bucket with Data Catalog enabled - API token with R2 permissions - Wrangler CLI installed (for CLI queries) ## Enable R2 Data Catalog R2 SQL queries Apache Iceberg tables in R2 Data Catalog. Must enable catalog on bucket first. ### Via Wrangler CLI ```bash npx wrangler r2 bucket catalog enable ``` Output includes: - **Warehouse name** - Typically same as bucket name - **Catalog URI** - REST endpoint for catalog operations Example output: ``` Catalog enabled successfully Warehouse: my-bucket Catalog URI: https://abc123.r2.cloudflarestorage.com/iceberg/my-bucket ``` ### Via Dashboard 1. Navigate to **R2 Object Storage** → Select your bucket 2. Click **Settings** tab 3. Scroll to **R2 Data Catalog** section 4. Click **Enable** 5. Note the **Catalog URI** and **Warehouse** name **Important:** Enabling catalog creates metadata directories in bucket but does not modify existing objects. ## Create API Token R2 SQL requires API token with R2 permissions. ### Required Permission **R2 Admin Read & Write** (includes R2 SQL Read permission) ### Via Dashboard 1. Navigate to **R2 Object Storage** 2. Click **Manage API tokens** (top right) 3. Click **Create API token** 4. Select **Admin Read & Write** permission 5. Click **Create API Token** 6. **Copy token value** - shown only once ### Permission Scope | Permission | Grants Access To | |------------|------------------| | R2 Admin Read & Write | R2 storage operations + R2 SQL queries + Data Catalog operations | | R2 SQL Read | SQL queries only (no storage writes) | **Note:** R2 SQL Read permission not yet available via Dashboard - use Admin Read & Write. ## Configure Environment ### Wrangler CLI Set environment variable for Wrangler to use: ```bash export WRANGLER_R2_SQL_AUTH_TOKEN= ``` Or create `.env` file in project directory: ``` WRANGLER_R2_SQL_AUTH_TOKEN= ``` Wrangler automatically loads `.env` file when running commands. ### HTTP API For programmatic access (non-Wrangler), pass token in Authorization header: ```bash curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/r2/sql/query \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "warehouse": "my-bucket", "query": "SELECT * FROM default.my_table LIMIT 10" }' ``` **Note:** HTTP API endpoint URL may vary - see [patterns.md](patterns.md#http-api-query) for current endpoint. ## Verify Setup Test configuration by querying system tables: ```bash # List namespaces npx wrangler r2 sql query "my-bucket" "SHOW DATABASES" # List tables in namespace npx wrangler r2 sql query "my-bucket" "SHOW TABLES IN default" ``` If successful, returns JSON array of results. ## Troubleshooting ### "Token authentication failed" **Cause:** Invalid or missing token **Solution:** - Verify `WRANGLER_R2_SQL_AUTH_TOKEN` environment variable set - Check token has Admin Read & Write permission - Create new token if expired ### "Catalog not enabled on bucket" **Cause:** Data Catalog not enabled **Solution:** - Run `npx wrangler r2 bucket catalog enable ` - Or enable via Dashboard (R2 → bucket → Settings → R2 Data Catalog) ### "Permission denied" **Cause:** Token lacks required permissions **Solution:** - Verify token has **Admin Read & Write** permission - Create new token with correct permissions ## See Also - [r2-data-catalog/configuration.md](../r2-data-catalog/configuration.md) - Detailed token setup and PyIceberg connection - [patterns.md](patterns.md) - Query examples using configuration - [gotchas.md](gotchas.md) - Common configuration errors ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-sql/gotchas.md ================================================ # R2 SQL Gotchas Limitations, troubleshooting, and common pitfalls for R2 SQL. ## Critical Limitations ### No Workers Binding **Cannot call R2 SQL from Workers/Pages code** - no binding exists. ```typescript // ❌ This doesn't exist export default { async fetch(request, env) { const result = await env.R2_SQL.query("SELECT * FROM table"); // Not possible return Response.json(result); } }; ``` **Solutions:** - HTTP API from external systems (not Workers) - PyIceberg/Spark via r2-data-catalog REST API - For Workers, use D1 or external databases ### ORDER BY Limitations Can only order by: 1. **Partition key columns** - Always supported 2. **Aggregation functions** - Supported via shuffle strategy **Cannot order by** regular non-partition columns. ```sql -- ✅ Valid: ORDER BY partition key SELECT * FROM logs.requests ORDER BY timestamp DESC LIMIT 100; -- ✅ Valid: ORDER BY aggregation SELECT region, SUM(amount) FROM sales.transactions GROUP BY region ORDER BY SUM(amount) DESC; -- ❌ Invalid: ORDER BY non-partition column SELECT * FROM logs.requests ORDER BY user_id; -- ❌ Invalid: ORDER BY alias (must repeat function) SELECT region, SUM(amount) as total FROM sales.transactions GROUP BY region ORDER BY total; -- Use ORDER BY SUM(amount) ``` Check partition spec: `DESCRIBE namespace.table_name` ## SQL Feature Limitations | Feature | Supported | Notes | |---------|-----------|-------| | SELECT, WHERE, GROUP BY, HAVING | ✅ | Standard support | | COUNT, SUM, AVG, MIN, MAX | ✅ | Standard aggregations | | ORDER BY partition/aggregation | ✅ | See above | | LIMIT | ✅ | Max 10,000 | | Column aliases | ❌ | No AS alias | | Expressions in SELECT | ❌ | No col1 + col2 | | ORDER BY non-partition | ❌ | Fails at runtime | | JOINs, subqueries, CTEs | ❌ | Denormalize at write time | | Window functions, UNION | ❌ | Use external engines | | INSERT/UPDATE/DELETE | ❌ | Use PyIceberg/Pipelines | | Nested columns, arrays, JSON | ❌ | Flatten at write time | **Workarounds:** - No JOINs: Denormalize data or use Spark/PyIceberg - No subqueries: Split into multiple queries - No aliases: Accept generated names, transform in app ## Common Errors ### "Column not found" **Cause:** Typo, column doesn't exist, or case mismatch **Solution:** `DESCRIBE namespace.table_name` to check schema ### "Type mismatch" ```sql -- ❌ Wrong types WHERE status = '200' -- string instead of integer WHERE timestamp > '2025-01-01' -- missing time/timezone -- ✅ Correct types WHERE status = 200 WHERE timestamp > '2025-01-01T00:00:00Z' ``` ### "ORDER BY column not in partition key" **Cause:** Ordering by non-partition column **Solution:** Use partition key, aggregation, or remove ORDER BY. Check: `DESCRIBE table` ### "Token authentication failed" ```bash # Check/set token echo $WRANGLER_R2_SQL_AUTH_TOKEN export WRANGLER_R2_SQL_AUTH_TOKEN= # Or .env file echo "WRANGLER_R2_SQL_AUTH_TOKEN=" > .env ``` ### "Table not found" ```sql -- Verify catalog and tables SHOW DATABASES; SHOW TABLES IN namespace_name; ``` Enable catalog: `npx wrangler r2 bucket catalog enable ` ### "LIMIT exceeds maximum" Max LIMIT is 10,000. For pagination, use WHERE filters with partition keys. ### "No data returned" (unexpected) **Debug steps:** 1. `SELECT COUNT(*) FROM table` - verify data exists 2. Remove WHERE filters incrementally 3. `SELECT * FROM table LIMIT 10` - inspect actual data/types ## Performance Issues ### Slow Queries **Causes:** Too many partitions, large LIMIT, no filters, small files ```sql -- ❌ Slow: No filters SELECT * FROM logs.requests LIMIT 10000; -- ✅ Fast: Filter on partition key SELECT * FROM logs.requests WHERE timestamp >= '2025-01-15T00:00:00Z' AND timestamp < '2025-01-16T00:00:00Z' LIMIT 1000; -- ✅ Faster: Multiple filters SELECT * FROM logs.requests WHERE timestamp >= '2025-01-15T00:00:00Z' AND status = 404 AND method = 'GET' LIMIT 1000; ``` **File optimization:** - Target Parquet size: 100-500MB compressed - Pipelines roll interval: 300+ sec (prod), 10 sec (dev) - Run compaction to merge small files ### Query Timeout **Solution:** Add restrictive WHERE filters, reduce time range, query smaller intervals ```sql -- ❌ Times out: Year-long aggregation SELECT status, COUNT(*) FROM logs.requests WHERE timestamp >= '2024-01-01T00:00:00Z' GROUP BY status; -- ✅ Faster: Month-long aggregation SELECT status, COUNT(*) FROM logs.requests WHERE timestamp >= '2025-01-01T00:00:00Z' AND timestamp < '2025-02-01T00:00:00Z' GROUP BY status; ``` ## Best Practices ### Partitioning - **Time-series:** Partition by day/hour on timestamp - **Avoid:** High-cardinality keys (user_id), >10,000 partitions ```python from pyiceberg.partitioning import PartitionSpec, PartitionField from pyiceberg.transforms import DayTransform PartitionSpec(PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name="day")) ``` ### Query Writing - **Always use LIMIT** for early termination - **Filter on partition keys first** for pruning - **Combine filters with AND** for more pruning ```sql -- Good WHERE timestamp >= '2025-01-15T00:00:00Z' AND status = 404 AND method = 'GET' LIMIT 100 ``` ### Type Safety - Quote strings: `'GET'` not `GET` - RFC3339 timestamps: `'2025-01-01T00:00:00Z'` not `'2025-01-01'` - ISO dates: `'2025-01-15'` not `'01/15/2025'` ### Data Organization - **Pipelines:** Dev `roll_file_time: 10`, Prod `roll_file_time: 300+` - **Compression:** Use `zstd` - **Maintenance:** Compaction for small files, expire old snapshots ## Debugging Checklist 1. `npx wrangler r2 bucket catalog enable ` - Verify catalog 2. `echo $WRANGLER_R2_SQL_AUTH_TOKEN` - Check token 3. `SHOW DATABASES` - List namespaces 4. `SHOW TABLES IN namespace` - List tables 5. `DESCRIBE namespace.table` - Check schema 6. `SELECT COUNT(*) FROM namespace.table` - Verify data 7. `SELECT * FROM namespace.table LIMIT 10` - Test simple query 8. Add filters incrementally ## See Also - [api.md](api.md) - SQL syntax - [patterns.md](patterns.md) - Query optimization - [configuration.md](configuration.md) - Setup - [Cloudflare R2 SQL Docs](https://developers.cloudflare.com/r2-sql/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/r2-sql/patterns.md ================================================ # R2 SQL Patterns Common patterns, use cases, and integration examples for R2 SQL. ## Wrangler CLI Query ```bash # Basic query npx wrangler r2 sql query "my-bucket" "SELECT * FROM default.logs LIMIT 10" # Multi-line query npx wrangler r2 sql query "my-bucket" " SELECT status, COUNT(*), AVG(response_time) FROM logs.http_requests WHERE timestamp >= '2025-01-01T00:00:00Z' GROUP BY status ORDER BY COUNT(*) DESC LIMIT 100 " # Use environment variable export R2_SQL_WAREHOUSE="my-bucket" npx wrangler r2 sql query "$R2_SQL_WAREHOUSE" "SELECT * FROM default.logs" ``` ## HTTP API Query For programmatic access from external systems (not Workers - see gotchas.md). ```bash curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/r2/sql/query \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "warehouse": "my-bucket", "query": "SELECT * FROM default.my_table WHERE status = 200 LIMIT 100" }' ``` Response: ```json { "success": true, "result": [{"user_id": "user_123", "timestamp": "2025-01-15T10:30:00Z", "status": 200}], "errors": [] } ``` ## Pipelines Integration Stream data to Iceberg tables via Pipelines, then query with R2 SQL. ```bash # Setup pipeline (select Data Catalog Table destination) npx wrangler pipelines setup # Key settings: # - Destination: Data Catalog Table # - Compression: zstd (recommended) # - Roll file time: 300+ sec (production), 10 sec (dev) # Send data to pipeline curl -X POST https://{stream-id}.ingest.cloudflare.com \ -H "Content-Type: application/json" \ -d '[{"user_id": "user_123", "event_type": "purchase", "timestamp": "2025-01-15T10:30:00Z", "amount": 29.99}]' # Query ingested data (wait for roll interval) npx wrangler r2 sql query "my-bucket" " SELECT event_type, COUNT(*), SUM(amount) FROM default.events WHERE timestamp >= '2025-01-15T00:00:00Z' GROUP BY event_type " ``` See [pipelines/patterns.md](../pipelines/patterns.md) for detailed setup. ## PyIceberg Integration Create and populate Iceberg tables with PyIceberg, then query with R2 SQL. ```python from pyiceberg.catalog.rest import RestCatalog import pyarrow as pa import pandas as pd # Setup catalog catalog = RestCatalog( name="my_catalog", warehouse="my-bucket", uri="https://.r2.cloudflarestorage.com/iceberg/my-bucket", token="", ) catalog.create_namespace_if_not_exists("analytics") # Create table schema = pa.schema([ pa.field("user_id", pa.string(), nullable=False), pa.field("event_time", pa.timestamp("us", tz="UTC"), nullable=False), pa.field("page_views", pa.int64(), nullable=False), ]) table = catalog.create_table(("analytics", "user_metrics"), schema=schema) # Append data df = pd.DataFrame({ "user_id": ["user_1", "user_2"], "event_time": pd.to_datetime(["2025-01-15 10:00:00", "2025-01-15 11:00:00"], utc=True), "page_views": [10, 25], }) table.append(pa.Table.from_pandas(df, schema=schema)) ``` Query with R2 SQL: ```bash npx wrangler r2 sql query "my-bucket" " SELECT user_id, SUM(page_views) FROM analytics.user_metrics WHERE event_time >= '2025-01-15T00:00:00Z' GROUP BY user_id " ``` See [r2-data-catalog/patterns.md](../r2-data-catalog/patterns.md) for advanced PyIceberg patterns. ## Use Cases ### Log Analytics ```sql -- Error rate by endpoint SELECT path, COUNT(*), SUM(CASE WHEN status >= 400 THEN 1 ELSE 0 END) as errors FROM logs.http_requests WHERE timestamp BETWEEN '2025-01-01T00:00:00Z' AND '2025-01-31T23:59:59Z' GROUP BY path ORDER BY errors DESC LIMIT 20; -- Response time stats SELECT method, MIN(response_time_ms), AVG(response_time_ms), MAX(response_time_ms) FROM logs.http_requests WHERE timestamp >= '2025-01-15T00:00:00Z' GROUP BY method; -- Traffic by status SELECT status, COUNT(*) FROM logs.http_requests WHERE timestamp >= '2025-01-15T00:00:00Z' AND method = 'GET' GROUP BY status ORDER BY COUNT(*) DESC; ``` ### Fraud Detection ```sql -- High-value transactions SELECT location, COUNT(*), SUM(amount), AVG(amount) FROM fraud.transactions WHERE transaction_timestamp >= '2025-01-01T00:00:00Z' AND amount > 1000.0 GROUP BY location ORDER BY SUM(amount) DESC LIMIT 20; -- Flagged transactions SELECT merchant_category, COUNT(*), AVG(amount) FROM fraud.transactions WHERE is_fraud_flag = true AND transaction_timestamp >= '2025-01-01T00:00:00Z' GROUP BY merchant_category HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC; ``` ### Business Intelligence ```sql -- Sales by department SELECT department, SUM(revenue), AVG(revenue), COUNT(*) FROM sales.transactions WHERE sale_date >= '2024-01-01' GROUP BY department ORDER BY SUM(revenue) DESC LIMIT 10; -- Product performance SELECT category, COUNT(DISTINCT product_id), SUM(units_sold), SUM(revenue) FROM sales.product_sales WHERE sale_date BETWEEN '2024-10-01' AND '2024-12-31' GROUP BY category ORDER BY SUM(revenue) DESC; ``` ## Connecting External Engines R2 Data Catalog exposes Iceberg REST API. Connect Spark, Snowflake, Trino, DuckDB, etc. ```scala // Apache Spark example val spark = SparkSession.builder() .config("spark.sql.catalog.my_catalog", "org.apache.iceberg.spark.SparkCatalog") .config("spark.sql.catalog.my_catalog.catalog-impl", "org.apache.iceberg.rest.RESTCatalog") .config("spark.sql.catalog.my_catalog.uri", "https://.r2.cloudflarestorage.com/iceberg/my-bucket") .config("spark.sql.catalog.my_catalog.token", "") .getOrCreate() spark.sql("SELECT * FROM my_catalog.default.my_table LIMIT 10").show() ``` See [r2-data-catalog/patterns.md](../r2-data-catalog/patterns.md) for more engines. ## Performance Optimization ### Partitioning - **Time-series:** day/hour on timestamp - **Geographic:** region/country - **Avoid:** High-cardinality keys (user_id) ```python from pyiceberg.partitioning import PartitionSpec, PartitionField from pyiceberg.transforms import DayTransform PartitionSpec(PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name="day")) ``` ### Query Optimization - **Always use LIMIT** for early termination - **Filter on partition keys first** - **Multiple filters** for better pruning ```sql -- Better: Multiple filters on partition key SELECT * FROM logs.requests WHERE timestamp >= '2025-01-15T00:00:00Z' AND status = 404 AND method = 'GET' LIMIT 100; ``` ### File Organization - **Pipelines roll:** Dev 10-30s, Prod 300+s - **Target Parquet:** 100-500MB compressed ## See Also - [api.md](api.md) - SQL syntax reference - [gotchas.md](gotchas.md) - Limitations and troubleshooting - [r2-data-catalog/patterns.md](../r2-data-catalog/patterns.md) - PyIceberg advanced patterns - [pipelines/patterns.md](../pipelines/patterns.md) - Streaming ingestion patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtime-sfu/README.md ================================================ # Cloudflare Realtime SFU Reference Expert guidance for building real-time audio/video/data applications using Cloudflare Realtime SFU (Selective Forwarding Unit). ## Reading Order | Task | Files | ~Tokens | |------|-------|---------| | New project | README → configuration | ~1200 | | Implement publish/subscribe | README → api | ~1600 | | Add PartyTracks | patterns (PartyTracks section) | ~800 | | Build presence system | patterns (DO section) | ~800 | | Debug connection issues | gotchas | ~700 | | Scale to millions | patterns (Cascading section) | ~600 | | Add simulcast | patterns (Advanced section) | ~500 | | Configure TURN | configuration (TURN section) | ~400 | ## 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 ## Quick Start Cloudflare Realtime SFU: WebRTC infrastructure on global network (310+ cities). Anycast routing, no regional constraints, pub/sub model. **Core concepts:** - **Sessions:** WebRTC PeerConnection to Cloudflare edge - **Tracks:** Audio/video/data channels you publish or subscribe to - **No rooms:** Build presence layer yourself via track sharing (see patterns.md) **Mental model:** Your client establishes one WebRTC session, publishes tracks (audio/video), shares track IDs via your backend, others subscribe to your tracks using track IDs + your session ID. ## Choose Your Approach | Approach | When to Use | Complexity | |----------|-------------|------------| | **PartyTracks** | Production apps with device switching, React | Low - Observable-based, handles reconnections | | **Raw API** | Custom requirements, non-browser, learning | Medium - Full control, manual WebRTC lifecycle | | **RealtimeKit** | End-to-end SDK with UI components | Lowest - Managed state, React hooks | **Recommendation:** Start with PartyTracks for most production applications. See patterns.md for PartyTracks examples. ## SFU vs RealtimeKit - **Realtime SFU:** WebRTC infrastructure (this reference). Build your own signaling, presence, UI. - **RealtimeKit:** SDK layer on top of SFU. Includes React hooks, state management, UI components. Part of Cloudflare AI platform. Use SFU directly when you need custom signaling or non-React framework. Use RealtimeKit for faster development with React. ## Setup Dashboard: https://dash.cloudflare.com/?to=/:account/calls Get `CALLS_APP_ID` and `CALLS_APP_SECRET` from dashboard, then see configuration.md for deployment. ## See Also - [Orange Meets Demo](https://demo.orange.cloudflare.dev/) - [Orange Source](https://github.com/cloudflare/orange) - [Calls Examples](https://github.com/cloudflare/calls-examples) - [API Reference](https://developers.cloudflare.com/api/resources/calls/) - [RealtimeKit Docs](https://developers.cloudflare.com/workers-ai/realtimekit/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtime-sfu/api.md ================================================ # API Reference ## Authentication ```bash curl -X POST 'https://rtc.live/v1/apps/${CALLS_APP_ID}/sessions/new' \ -H "Authorization: Bearer ${CALLS_APP_SECRET}" ``` ## Core Concepts **Sessions:** PeerConnection to Cloudflare edge **Tracks:** Media/data channels (audio/video/datachannel) **No rooms:** Build presence via track sharing ## Client Libraries **PartyTracks (Recommended):** Observable-based client library for production use. Handles device changes, network switches, ICE restarts automatically. Push/pull API with React hooks. See patterns.md for full examples. ```bash npm install partytracks @cloudflare/calls ``` **Raw API:** Direct HTTP + WebRTC for custom requirements (documented below). ## Endpoints ### Create Session ```http POST /v1/apps/{appId}/sessions/new → {sessionId, sessionDescription} ``` ### Add Track (Publish) ```http POST /v1/apps/{appId}/sessions/{sessionId}/tracks/new Body: { sessionDescription: {sdp, type: "offer"}, tracks: [{location: "local", trackName: "my-video"}] } → {sessionDescription, tracks: [{trackName}]} ``` ### Add Track (Subscribe) ```http POST /v1/apps/{appId}/sessions/{sessionId}/tracks/new Body: { tracks: [{ location: "remote", trackName: "remote-track-id", sessionId: "other-session-id" }] } → {sessionDescription} (server offer) ``` ### Renegotiate ```http PUT /v1/apps/{appId}/sessions/{sessionId}/renegotiate Body: {sessionDescription: {sdp, type: "answer"}} ``` ### Close Tracks ```http PUT /v1/apps/{appId}/sessions/{sessionId}/tracks/close Body: {tracks: [{trackName}]} → {requiresImmediateRenegotiation: boolean} ``` ### Get Session ```http GET /v1/apps/{appId}/sessions/{sessionId} → {sessionId, tracks: TrackMetadata[]} ``` ## TypeScript Types ```typescript interface TrackMetadata { trackName: string; location: "local" | "remote"; sessionId?: string; // For remote tracks mid?: string; // WebRTC mid } ``` ## WebRTC Flow ```typescript // 1. Create PeerConnection const pc = new RTCPeerConnection({ iceServers: [{urls: 'stun:stun.cloudflare.com:3478'}] }); // 2. Add tracks const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true}); stream.getTracks().forEach(track => pc.addTrack(track, stream)); // 3. Create offer const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // 4. Send to backend → Cloudflare API const response = await fetch('/api/new-session', { method: 'POST', body: JSON.stringify({sdp: offer.sdp}) }); // 5. Set remote answer const {sessionDescription} = await response.json(); await pc.setRemoteDescription(sessionDescription); ``` ## Publishing ```typescript const offer = await pc.createOffer(); await pc.setLocalDescription(offer); const res = await fetch(`/api/sessions/${sessionId}/tracks`, { method: 'POST', body: JSON.stringify({ sdp: offer.sdp, tracks: [{location: 'local', trackName: 'my-video'}] }) }); const {sessionDescription, tracks} = await res.json(); await pc.setRemoteDescription(sessionDescription); const publishedTrackId = tracks[0].trackName; // Share with others ``` ## Subscribing ```typescript const res = await fetch(`/api/sessions/${sessionId}/tracks`, { method: 'POST', body: JSON.stringify({ tracks: [{location: 'remote', trackName: remoteTrackId, sessionId: remoteSessionId}] }) }); const {sessionDescription} = await res.json(); await pc.setRemoteDescription(sessionDescription); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); await fetch(`/api/sessions/${sessionId}/renegotiate`, { method: 'PUT', body: JSON.stringify({sdp: answer.sdp}) }); pc.ontrack = (event) => { const [remoteStream] = event.streams; videoElement.srcObject = remoteStream; }; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtime-sfu/configuration.md ================================================ # Configuration & Deployment ## Dashboard Setup 1. Navigate to https://dash.cloudflare.com/?to=/:account/calls 2. Click "Create Application" (or use existing app) 3. Copy `CALLS_APP_ID` from dashboard 4. Generate and copy `CALLS_APP_SECRET` (treat as sensitive credential) 5. Use credentials in Wrangler config or environment variables below ## Dependencies **Backend (Workers):** Built-in fetch API, no additional packages required **Client (PartyTracks):** ```bash npm install partytracks @cloudflare/calls ``` **Client (React + PartyTracks):** ```bash npm install partytracks @cloudflare/calls observable-hooks # Observable hooks: useObservableAsValue, useValueAsObservable ``` **Client (Raw API):** Native browser WebRTC API only ## Wrangler Setup ```jsonc { "name": "my-calls-app", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date for new projects "vars": { "CALLS_APP_ID": "your-app-id", "MAX_WEBCAM_BITRATE": "1200000", "MAX_WEBCAM_FRAMERATE": "24", "MAX_WEBCAM_QUALITY_LEVEL": "1080" }, // Set secret: wrangler secret put CALLS_APP_SECRET "durable_objects": { "bindings": [ { "name": "ROOM", "class_name": "Room" } ] } } ``` ## Deploy ```bash wrangler login wrangler secret put CALLS_APP_SECRET wrangler deploy ``` ## Environment Variables **Required:** - `CALLS_APP_ID`: From dashboard - `CALLS_APP_SECRET`: From dashboard (secret) **Optional:** - `MAX_WEBCAM_BITRATE` (default: 1200000) - `MAX_WEBCAM_FRAMERATE` (default: 24) - `MAX_WEBCAM_QUALITY_LEVEL` (default: 1080) - `TURN_SERVICE_ID`: TURN service - `TURN_SERVICE_TOKEN`: TURN auth (secret) ## TURN Configuration ```javascript const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: [ 'turn:turn.cloudflare.com:3478?transport=udp', 'turn:turn.cloudflare.com:3478?transport=tcp', 'turns:turn.cloudflare.com:5349?transport=tcp' ], username: turnUsername, credential: turnCredential } ], bundlePolicy: 'max-bundle', // Recommended: reduces overhead iceTransportPolicy: 'all' // Use 'relay' to force TURN (testing only) }); ``` **Ports:** 3478 (UDP/TCP), 53 (UDP), 80 (TCP), 443 (TLS), 5349 (TLS) **When to use TURN:** Required for restrictive corporate firewalls/networks that block UDP. ~5-10% of connections fallback to TURN. STUN works for most users. **ICE candidate filtering:** Cloudflare handles candidate filtering automatically. No need to manually filter candidates. ## Durable Object Boilerplate Minimal presence system: ```typescript export class Room { private sessions = new Map(); async fetch(req: Request) { const {pathname} = new URL(req.url); const body = await req.json(); if (pathname === '/join') { this.sessions.set(body.sessionId, {userId: body.userId, tracks: []}); return Response.json({participants: this.sessions.size}); } if (pathname === '/publish') { this.sessions.get(body.sessionId)?.tracks.push(...body.tracks); // Broadcast to others via WebSocket (not shown) return new Response('OK'); } return new Response('Not found', {status: 404}); } } ``` ## Environment Validation Check credentials before first API call: ```typescript if (!env.CALLS_APP_ID || !env.CALLS_APP_SECRET) { throw new Error('CALLS_APP_ID and CALLS_APP_SECRET required'); } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtime-sfu/gotchas.md ================================================ # Gotchas & Troubleshooting ## Common Errors ### "Slow initial connect (~1.8s)" **Cause:** First STUN delayed during consensus forming (normal behavior) **Solution:** Subsequent connections are faster. CF detects DTLS ClientHello early to compensate. ### "No media flow" **Cause:** SDP exchange incomplete, connection not established, tracks not added before offer, browser permissions missing **Solution:** 1. Verify SDP exchange complete 2. Check `pc.connectionState === 'connected'` 3. Ensure tracks added before creating offer 4. Confirm browser permissions granted 5. Use `chrome://webrtc-internals` for debugging ### "Track not receiving" **Cause:** Track not published, track ID not shared, session IDs mismatch, `pc.ontrack` not set, renegotiation needed **Solution:** 1. Verify track published successfully 2. Confirm track ID shared between peers 3. Check session IDs match 4. Set `pc.ontrack` handler before answer 5. Trigger renegotiation if needed ### "ICE connection failed" **Cause:** Network changed, firewall blocked UDP, TURN needed, transient network issue **Solution:** ```typescript pc.oniceconnectionstatechange = async () => { if (pc.iceConnectionState === 'failed') { console.warn('ICE failed, attempting restart'); await pc.restartIce(); // Triggers new ICE gathering // Create new offer with ICE restart flag const offer = await pc.createOffer({iceRestart: true}); await pc.setLocalDescription(offer); // Send to backend → Cloudflare API await fetch(`/api/sessions/${sessionId}/renegotiate`, { method: 'PUT', body: JSON.stringify({sdp: offer.sdp}) }); } }; ``` ### "Track stuck/frozen" **Cause:** Sender paused track, network congestion, codec mismatch, mobile browser backgrounded **Solution:** 1. Check `track.enabled` and `track.readyState === 'live'` 2. Verify sender active: `pc.getSenders().find(s => s.track === track)` 3. Check stats for packet loss/jitter (see patterns.md) 4. On mobile: Re-acquire tracks when app foregrounded 5. Test with different codecs if persistent ### "Network change disconnects call" **Cause:** Mobile switching WiFi↔cellular, laptop changing networks **Solution:** ```typescript // Listen for network changes if ('connection' in navigator) { (navigator as any).connection.addEventListener('change', async () => { console.log('Network changed'); await pc.restartIce(); // Use ICE restart pattern above }); } // Or use PartyTracks (handles automatically) ``` ## Retry with Exponential Backoff ```typescript async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const res = await fetch(url, options); if (res.ok) return res; if (res.status >= 500) throw new Error('Server error'); return res; // Client error, don't retry } catch (err) { if (i === maxRetries - 1) throw err; const delay = Math.min(1000 * 2 ** i, 10000); // Cap at 10s await new Promise(resolve => setTimeout(resolve, delay)); } } } ``` ## Debugging with chrome://webrtc-internals 1. Open `chrome://webrtc-internals` in Chrome/Edge 2. Find your PeerConnection in the list 3. Check **Stats graphs** for packet loss, jitter, bandwidth 4. Check **ICE candidate pairs**: Look for `succeeded` state, relay vs host candidates 5. Check **getStats**: Raw metrics for inbound/outbound RTP 6. Look for errors in **Event log**: `iceConnectionState`, `connectionState` changes 7. Export data with "Download the PeerConnection updates and stats data" button 8. Common issues visible here: ICE failures, high packet loss, bitrate drops ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | Egress (Free) | 1TB/month | Per account | | Egress (Paid) | $0.05/GB | After free tier | | Inbound traffic | Free | All plans | | TURN service | Free | Included with SFU | | Participants | No hard limit | Client bandwidth/CPU bound (typically 10-50 tracks) | | Tracks per session | No hard limit | Client resources limited | | Session duration | No hard limit | Production calls run for hours | | WebRTC ports | UDP 1024-65535 | Outbound only, required for media | | API rate limit | 600 req/min | Per app, burst allowed | ## Security Checklist - ✅ **Never expose** `CALLS_APP_SECRET` to client - ✅ **Validate user identity** in backend before creating sessions - ✅ **Implement auth tokens** for session access (JWT in custom header) - ✅ **Rate limit** session creation endpoints - ✅ **Expire sessions** server-side after inactivity - ✅ **Validate track IDs** before subscribing (prevent unauthorized access) - ✅ **Use HTTPS** for all signaling (API calls) - ✅ **Enable DTLS-SRTP** (automatic with Cloudflare, encrypts media) - ⚠️ **Consider E2EE** for sensitive content (implement client-side with Insertable Streams API) ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtime-sfu/patterns.md ================================================ # Patterns & Use Cases ## Architecture ``` Client (WebRTC) <---> CF Edge <---> Backend (HTTP) | CF Backbone (310+ DCs) | Other Edges <---> Other Clients ``` Anycast: Last-mile <50ms (95%), no region select, NACK shield, distributed consensus Cascading trees auto-scale to millions: ``` Publisher -> Edge A -> Edge B -> Sub1 \-> Edge C -> Sub2,3 ``` ## Use Cases **1:1:** A creates session+publishes, B creates+subscribes to A+publishes, A subscribes to B **N:N:** All create session+publish, backend broadcasts track IDs, all subscribe to others **1:N:** Publisher creates+publishes, viewers each create+subscribe (no fan-out limit) **Breakout:** Same PeerConnection! Backend closes/adds tracks, no recreation ## PartyTracks (Recommended) Observable-based client with automatic device/network handling: ```typescript import {PartyTracks} from 'partytracks'; // Create client const pt = new PartyTracks({ apiUrl: '/api/calls', sessionId: 'my-session', onTrack: (track, peer) => { const video = document.getElementById(`video-${peer.id}`) as HTMLVideoElement; video.srcObject = new MediaStream([track]); } }); // Publish camera (push API) const camera = await pt.getCamera(); // Auto-requests permissions, handles device changes await pt.publishTrack(camera, {trackName: 'my-camera'}); // Subscribe to remote track (pull API) await pt.subscribeToTrack({trackName: 'remote-camera', sessionId: 'other-session'}); // React hook example import {useObservableAsValue} from 'observable-hooks'; function VideoCall() { const localTracks = useObservableAsValue(pt.localTracks$); const remoteTracks = useObservableAsValue(pt.remoteTracks$); return
{/* Render tracks */}
; } // Screenshare const screen = await pt.getScreenshare(); await pt.publishTrack(screen, {trackName: 'my-screen'}); // Handle device changes (automatic) // PartyTracks detects device changes (e.g., Bluetooth headset) and renegotiates ``` ## Backend Express: ```js app.post('/api/new-session', async (req, res) => { const r = await fetch(`${CALLS_API}/apps/${process.env.CALLS_APP_ID}/sessions/new`, {method: 'POST', headers: {'Authorization': `Bearer ${process.env.CALLS_APP_SECRET}`}}); res.json(await r.json()); }); ``` Workers: Same pattern, use `env.CALLS_APP_ID` and `env.CALLS_APP_SECRET` DO Presence: See configuration.md for boilerplate ## Audio Level Detection ```typescript // Attach analyzer to audio track function attachAudioLevelDetector(track: MediaStreamTrack) { const ctx = new AudioContext(); const analyzer = ctx.createAnalyser(); const src = ctx.createMediaStreamSource(new MediaStream([track])); src.connect(analyzer); const data = new Uint8Array(analyzer.frequencyBinCount); const checkLevel = () => { analyzer.getByteFrequencyData(data); const level = data.reduce((a, b) => a + b) / data.length; if (level > 30) console.log('Speaking:', level); // Trigger UI update requestAnimationFrame(checkLevel); }; checkLevel(); } ``` ## Connection Quality Monitoring ```typescript pc.getStats().then(stats => { stats.forEach(report => { if (report.type === 'inbound-rtp' && report.kind === 'video') { const {packetsLost, packetsReceived, jitter} = report; const lossRate = packetsLost / (packetsLost + packetsReceived); if (lossRate > 0.05) console.warn('High packet loss:', lossRate); if (jitter > 100) console.warn('High jitter:', jitter); } }); }); ``` ## Stage Management (Limit Visible Participants) ```typescript // Subscribe to top 6 active speakers only let activeSubscriptions = new Set(); function updateStage(topSpeakers: string[]) { const toAdd = topSpeakers.filter(id => !activeSubscriptions.has(id)).slice(0, 6); const toRemove = [...activeSubscriptions].filter(id => !topSpeakers.includes(id)); toRemove.forEach(id => { pc.getSenders().find(s => s.track?.id === id)?.track?.stop(); activeSubscriptions.delete(id); }); toAdd.forEach(async id => { await fetch(`/api/subscribe`, {method: 'POST', body: JSON.stringify({trackId: id})}); activeSubscriptions.add(id); }); } ``` ## Advanced Bandwidth mgmt: ```ts const s = pc.getSenders().find(s => s.track?.kind === 'video'); const p = s.getParameters(); if (!p.encodings) p.encodings = [{}]; p.encodings[0].maxBitrate = 1200000; p.encodings[0].maxFramerate = 24; await s.setParameters(p); ``` Simulcast (CF auto-forwards best layer): ```ts pc.addTransceiver('video', {direction: 'sendonly', sendEncodings: [ {rid: 'high', maxBitrate: 1200000}, {rid: 'med', maxBitrate: 600000, scaleResolutionDownBy: 2}, {rid: 'low', maxBitrate: 200000, scaleResolutionDownBy: 4} ]}); ``` DataChannel: ```ts const dc = pc.createDataChannel('chat', {ordered: true, maxRetransmits: 3}); dc.onopen = () => dc.send(JSON.stringify({type: 'chat', text: 'Hi'})); dc.onmessage = (e) => console.log('RX:', JSON.parse(e.data)); ``` **WHIP/WHEP:** For streaming interop (OBS → SFU, SFU → video players), use WHIP (ingest) and WHEP (egress) protocols. See Cloudflare Stream integration docs. Integrations: R2 for recording `env.R2_BUCKET.put(...)`, Queues for analytics Perf: 100-250ms connect, ~50ms latency (95%), 200-400ms glass-to-glass, no participant limit (client: 10-50 tracks) ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtimekit/README.md ================================================ # Cloudflare RealtimeKit Expert guidance for building real-time video and audio applications using **Cloudflare RealtimeKit** - a comprehensive SDK suite for adding customizable live video and voice to web or mobile applications. ## Overview RealtimeKit is Cloudflare's SDK suite built on Realtime SFU, abstracting WebRTC complexity with fast integration, pre-built UI components, global performance (300+ cities), and production features (recording, transcription, chat, polls). **Use cases**: Team meetings, webinars, social video, audio calls, interactive plugins ## Core Concepts - **App**: Workspace grouping meetings, participants, presets, recordings. Use separate Apps for staging/production - **Meeting**: Re-usable virtual room. Each join creates new **Session** - **Session**: Live meeting instance. Created on first join, ends after last leave - **Participant**: User added via REST API. Returns `authToken` for client SDK. **Do not reuse tokens** - **Preset**: Reusable permission/UI template (permissions, meeting type, theme). Applied at participant creation - **Peer ID** (`id`): Unique per session, changes on rejoin - **Participant ID** (`userId`): Persistent across sessions ## Quick Start ### 1. Create App & Meeting (Backend) ```bash # Create app curl -X POST 'https://api.cloudflare.com/client/v4/accounts//realtime/kit/apps' \ -H 'Authorization: Bearer ' \ -d '{"name": "My RealtimeKit App"}' # Create meeting curl -X POST 'https://api.cloudflare.com/client/v4/accounts//realtime/kit//meetings' \ -H 'Authorization: Bearer ' \ -d '{"title": "Team Standup"}' # Add participant curl -X POST 'https://api.cloudflare.com/client/v4/accounts//realtime/kit//meetings//participants' \ -H 'Authorization: Bearer ' \ -d '{"name": "Alice", "preset_name": "host"}' # Returns: { authToken } ``` ### 2. Client Integration **React**: ```tsx import { RtkMeeting } from '@cloudflare/realtimekit-react-ui'; function App() { return {}} />; } ``` **Core SDK**: ```typescript import RealtimeKitClient from '@cloudflare/realtimekit'; const meeting = new RealtimeKitClient({ authToken: '', video: true, audio: true }); await meeting.join(); ``` ## Reading Order | Task | Files | |------|-------| | Quick integration | README only | | Custom UI | README → patterns → api | | Backend setup | README → configuration | | Debug issues | gotchas | | Advanced features | patterns → api | ## RealtimeKit vs Realtime SFU | Choose | When | |--------|------| | **RealtimeKit** | Need pre-built UI, fast integration, React/Angular/HTML | | **Realtime SFU** | Building from scratch, custom WebRTC, full control | RealtimeKit is built on Realtime SFU but abstracts WebRTC complexity with UI components and SDKs. ## Which Package? Need pre-built meeting UI? - React → `@cloudflare/realtimekit-react-ui` (``) - Angular → `@cloudflare/realtimekit-angular-ui` - HTML/Vanilla → `@cloudflare/realtimekit-ui` Need custom UI? - Core SDK → `@cloudflare/realtimekit` (RealtimeKitClient) - full control Need raw WebRTC control? - See `realtime-sfu/` reference ## In This Reference - [Configuration](./configuration.md) - Setup, installation, wrangler config - [API](./api.md) - Meeting object, REST API, SDK methods - [Patterns](./patterns.md) - Common workflows, code examples - [Gotchas](./gotchas.md) - Common issues, troubleshooting ## See Also - [Workers](../workers/) - Backend integration - [D1](../d1/) - Meeting metadata storage - [R2](../r2/) - Recording storage - [KV](../kv/) - Session management ## Reference Links - **Official Docs**: https://developers.cloudflare.com/realtime/realtimekit/ - **API Reference**: https://developers.cloudflare.com/api/resources/realtime_kit/ - **Examples**: https://github.com/cloudflare/realtimekit-web-examples - **Dashboard**: https://dash.cloudflare.com/?to=/:account/realtime/kit ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtimekit/api.md ================================================ # RealtimeKit API Reference Complete API reference for Meeting object, REST endpoints, and SDK methods. ## Meeting Object API ### `meeting.self` - Local Participant ```typescript // Properties: id, userId, name, audioEnabled, videoEnabled, screenShareEnabled, audioTrack, videoTrack, screenShareTracks, roomJoined, roomState // Methods await meeting.self.enableAudio() / disableAudio() / enableVideo() / disableVideo() / enableScreenShare() / disableScreenShare() await meeting.self.setName("Name") // Before join only await meeting.self.setDevice(device) const devices = await meeting.self.getAllDevices() / getAudioDevices() / getVideoDevices() / getSpeakerDevices() // Events: 'roomJoined', 'audioUpdate', 'videoUpdate', 'screenShareUpdate', 'deviceUpdate', 'deviceListUpdate' meeting.self.on('roomJoined', () => {}) meeting.self.on('audioUpdate', ({ audioEnabled, audioTrack }) => {}) ``` ### `meeting.participants` - Remote Participants **Collections**: ```typescript meeting.participants.joined / active / waitlisted / pinned // Maps const participants = meeting.participants.joined.toArray() const count = meeting.participants.joined.size() const p = meeting.participants.joined.get('peer-id') ``` **Participant Properties**: ```typescript participant.id / userId / name participant.audioEnabled / videoEnabled / screenShareEnabled participant.audioTrack / videoTrack / screenShareTracks ``` **Events**: ```typescript meeting.participants.joined.on('participantJoined', (participant) => {}) meeting.participants.joined.on('participantLeft', (participant) => {}) ``` ### `meeting.meta` - Metadata ```typescript meeting.meta.meetingId / meetingTitle / meetingStartedTimestamp ``` ### `meeting.chat` - Chat ```typescript meeting.chat.messages // Array await meeting.chat.sendTextMessage("Hello") / sendImageMessage(file) meeting.chat.on('chatUpdate', ({ message, messages }) => {}) ``` ### `meeting.polls` - Polling ```typescript meeting.polls.items // Array await meeting.polls.create(question, options, anonymous, hideVotes) await meeting.polls.vote(pollId, optionIndex) ``` ### `meeting.plugins` - Collaborative Apps ```typescript meeting.plugins.all // Array await meeting.plugins.activate(pluginId) / deactivate() ``` ### `meeting.ai` - AI Features ```typescript meeting.ai.transcripts // Live transcriptions (when enabled in Preset) ``` ### Core Methods ```typescript await meeting.join() // Emits 'roomJoined' on meeting.self await meeting.leave() ``` ## TypeScript Types ```typescript import type { RealtimeKitClient, States, UIConfig, Participant } from '@cloudflare/realtimekit'; // Main interface interface RealtimeKitClient { self: SelfState; // Local participant (id, userId, name, audioEnabled, videoEnabled, roomJoined, roomState) participants: { joined, active, waitlisted, pinned }; // Reactive Maps chat: ChatNamespace; // messages[], sendTextMessage(), sendImageMessage() polls: PollsNamespace; // items[], create(), vote() plugins: PluginsNamespace; // all[], activate(), deactivate() ai: AINamespace; // transcripts[] meta: MetaState; // meetingId, meetingTitle, meetingStartedTimestamp join(): Promise; leave(): Promise; } // Participant (self & remote share same shape) interface Participant { id: string; // Peer ID (changes on rejoin) userId: string; // Persistent participant ID name: string; audioEnabled: boolean; videoEnabled: boolean; screenShareEnabled: boolean; audioTrack: MediaStreamTrack | null; videoTrack: MediaStreamTrack | null; screenShareTracks: MediaStreamTrack[]; } ``` ## Store Architecture RealtimeKit uses reactive store (event-driven updates, live Maps): ```typescript // Subscribe to state changes meeting.self.on('audioUpdate', ({ audioEnabled, audioTrack }) => {}); meeting.participants.joined.on('participantJoined', (p) => {}); // Access current state synchronously const isAudioOn = meeting.self.audioEnabled; const count = meeting.participants.joined.size(); ``` **Key principles:** State updates emit events after changes. Use `.toArray()` sparingly. Collections are live Maps. ## REST API Base: `https://api.cloudflare.com/client/v4/accounts/{account_id}/realtime/kit/{app_id}` ### Meetings ```bash GET /meetings # List all GET /meetings/{meeting_id} # Get details POST /meetings # Create: {"title": "..."} PATCH /meetings/{meeting_id} # Update: {"title": "...", "record_on_start": true} ``` ### Participants ```bash GET /meetings/{meeting_id}/participants # List all GET /meetings/{meeting_id}/participants/{participant_id} # Get details POST /meetings/{meeting_id}/participants # Add: {"name": "...", "preset_name": "...", "custom_participant_id": "..."} PATCH /meetings/{meeting_id}/participants/{participant_id} # Update: {"name": "...", "preset_name": "..."} DELETE /meetings/{meeting_id}/participants/{participant_id} # Delete POST /meetings/{meeting_id}/participants/{participant_id}/token # Refresh token ``` ### Active Session ```bash GET /meetings/{meeting_id}/active-session # Get active session POST /meetings/{meeting_id}/active-session/kick # Kick users: {"user_ids": ["id1", "id2"]} POST /meetings/{meeting_id}/active-session/kick-all # Kick all POST /meetings/{meeting_id}/active-session/poll # Create poll: {"question": "...", "options": [...], "anonymous": false} ``` ### Recording ```bash GET /recordings?meeting_id={meeting_id} # List recordings GET /recordings/active-recording/{meeting_id} # Get active recording POST /recordings # Start: {"meeting_id": "...", "type": "composite"} (or "track") PUT /recordings/{recording_id} # Control: {"action": "pause"} (or "resume", "stop") POST /recordings/track # Track recording: {"meeting_id": "...", "layers": [...]} ``` ### Livestreaming ```bash GET /livestreams?exclude_meetings=false # List all GET /livestreams/{livestream_id} # Get details POST /meetings/{meeting_id}/livestreams # Start for meeting POST /meetings/{meeting_id}/active-livestream/stop # Stop POST /livestreams # Create independent: returns {ingest_server, stream_key, playback_url} ``` ### Sessions & Analytics ```bash GET /sessions # List all GET /sessions/{session_id} # Get details GET /sessions/{session_id}/participants # List participants GET /sessions/{session_id}/participants/{participant_id} # Call stats GET /sessions/{session_id}/chat # Download chat CSV GET /sessions/{session_id}/transcript # Download transcript CSV GET /sessions/{session_id}/summary # Get summary POST /sessions/{session_id}/summary # Generate summary GET /analytics/daywise?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD # Day-wise analytics GET /analytics/livestreams/overall # Livestream analytics ``` ### Webhooks ```bash GET /webhooks # List all POST /webhooks # Create: {"url": "https://...", "events": ["session.started", "session.ended"]} PATCH /webhooks/{webhook_id} # Update DELETE /webhooks/{webhook_id} # Delete ``` ## Session Lifecycle ``` Initialization → Join Intent → [Waitlist?] → Meeting Screen (Stage) → Ended ↓ Approved [Rejected → Ended] ``` UI Kit handles state transitions automatically. ## See Also - [Configuration](./configuration.md) - Setup and installation - [Patterns](./patterns.md) - Usage examples - [README](./README.md) - Overview and quick start ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtimekit/configuration.md ================================================ # RealtimeKit Configuration Configuration guide for RealtimeKit setup, client SDKs, and wrangler integration. ## Installation ### React ```bash npm install @cloudflare/realtimekit @cloudflare/realtimekit-react-ui ``` ### Angular ```bash npm install @cloudflare/realtimekit @cloudflare/realtimekit-angular-ui ``` ### Web Components/HTML ```bash npm install @cloudflare/realtimekit @cloudflare/realtimekit-ui ``` ## Client SDK Configuration ### React UI Kit ```tsx import { RtkMeeting } from '@cloudflare/realtimekit-react-ui'; {}} /> ``` ### Angular UI Kit ```typescript @Component({ template: `` }) export class AppComponent { authToken = ''; onLeave() {} } ``` ### Web Components ```html ``` ### Core SDK Configuration ```typescript import RealtimeKitClient from '@cloudflare/realtimekit'; const meeting = new RealtimeKitClient({ authToken: '', video: true, audio: true, autoSwitchAudioDevice: true, mediaConfiguration: { video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, screenshare: { width: { max: 1920 }, height: { max: 1080 }, frameRate: { ideal: 15 } } } }); await meeting.join(); ``` ## Backend Setup ### Create App & Credentials **Dashboard**: https://dash.cloudflare.com/?to=/:account/realtime/kit **API**: ```bash curl -X POST 'https://api.cloudflare.com/client/v4/accounts//realtime/kit/apps' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{"name": "My RealtimeKit App"}' ``` **Required Permissions**: API token with **Realtime / Realtime Admin** permissions ### Create Presets ```bash curl -X POST 'https://api.cloudflare.com/client/v4/accounts//realtime/kit//presets' \ -H 'Authorization: Bearer ' \ -d '{ "name": "host", "permissions": { "canShareAudio": true, "canShareVideo": true, "canRecord": true, "canLivestream": true, "canStartStopRecording": true } }' ``` ## Wrangler Configuration ### Basic Configuration ```jsonc // wrangler.jsonc { "name": "realtimekit-app", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date "vars": { "CLOUDFLARE_ACCOUNT_ID": "abc123", "REALTIMEKIT_APP_ID": "xyz789" } // Secrets: wrangler secret put CLOUDFLARE_API_TOKEN } ``` ### With Database & Storage ```jsonc { "d1_databases": [{ "binding": "DB", "database_name": "meetings", "database_id": "d1-id" }], "r2_buckets": [{ "binding": "RECORDINGS", "bucket_name": "recordings" }], "kv_namespaces": [{ "binding": "SESSIONS", "id": "kv-id" }] } ``` ### Multi-Environment ```bash # Deploy to environments wrangler deploy --env staging wrangler deploy --env production ``` ## TURN Service Configuration RealtimeKit can use Cloudflare's TURN service for connectivity through restrictive networks: ```jsonc // wrangler.jsonc { "vars": { "TURN_SERVICE_ID": "your_turn_service_id" } // Set secret: wrangler secret put TURN_SERVICE_TOKEN } ``` TURN automatically configured when enabled in account - no client-side changes needed. ## Theming & Design Tokens ```typescript import type { UIConfig } from '@cloudflare/realtimekit'; const uiConfig: UIConfig = { designTokens: { colors: { brand: { 500: '#0066ff', 600: '#0052cc' }, background: { 1000: '#1A1A1A', 900: '#2D2D2D' }, text: { 1000: '#FFFFFF', 900: '#E0E0E0' } }, borderRadius: 'extra-rounded', // 'rounded' | 'extra-rounded' | 'sharp' theme: 'dark' // 'light' | 'dark' }, logo: { url: 'https://example.com/logo.png', altText: 'Company' } }; // Apply to React {}} /> // Or use CSS variables // :root { --rtk-color-brand-500: #0066ff; --rtk-border-radius: 12px; } ``` ## Internationalization (i18n) ### Custom Language Strings ```typescript import { useLanguage } from '@cloudflare/realtimekit-ui'; const customLanguage = { 'join': 'Entrar', 'leave': 'Salir', 'mute': 'Silenciar', 'unmute': 'Activar audio', 'turn_on_camera': 'Encender cámara', 'turn_off_camera': 'Apagar cámara', 'share_screen': 'Compartir pantalla', 'stop_sharing': 'Dejar de compartir' }; const t = useLanguage(customLanguage); // React usage {}} /> ``` ### Supported Locales Default locales available: `en`, `es`, `fr`, `de`, `pt`, `ja`, `zh` ```typescript import { setLocale } from '@cloudflare/realtimekit-ui'; setLocale('es'); // Switch to Spanish ``` ## See Also - [API](./api.md) - Meeting APIs, REST endpoints - [Patterns](./patterns.md) - Backend integration examples - [README](./README.md) - Overview and quick start ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtimekit/gotchas.md ================================================ # RealtimeKit Gotchas & Troubleshooting ## Common Errors ### "Cannot connect to meeting" **Cause:** Auth token invalid/expired, API credentials lack permissions, or network blocks WebRTC **Solution:** Verify token validity, check API token has **Realtime / Realtime Admin** permissions, enable TURN service for restrictive networks ### "No video/audio tracks" **Cause:** Browser permissions not granted, video/audio not enabled, device in use, or device unavailable **Solution:** Request browser permissions explicitly, verify initialization config, use `meeting.self.getAllDevices()` to debug, close other apps using device ### "Participant count mismatched" **Cause:** `meeting.participants` doesn't include `meeting.self` **Solution:** Total count = `meeting.participants.joined.size() + 1` ### "Events not firing" **Cause:** Listeners registered after actions, incorrect event name, or wrong namespace **Solution:** Register listeners before calling `meeting.join()`, check event names against docs, verify correct namespace ### "CORS errors in API calls" **Cause:** Making REST API calls from client-side **Solution:** All REST API calls **must** be server-side (Workers, backend). Never expose API tokens to clients. ### "Preset not applying" **Cause:** Preset doesn't exist, name mismatch (case-sensitive), or participant created before preset **Solution:** Verify preset exists via Dashboard or API, check exact spelling and case, create preset before adding participants ### "Token reuse error" **Cause:** Reusing participant tokens across sessions **Solution:** Generate fresh token per session. Use refresh endpoint if token expires during session. ### "Video quality poor" **Cause:** Insufficient bandwidth, resolution/bitrate too high, or CPU overload **Solution:** Lower `mediaConfiguration.video` resolution/frameRate, monitor network conditions, reduce participant count or grid size ### "Echo or audio feedback" **Cause:** Multiple devices picking up same audio source **Solution:** - Lower `mediaConfiguration.video` resolution/frameRate - Monitor network conditions - Reduce participant count or grid size ### Issue: Echo or audio feedback **Cause**: Multiple devices picking up same audio source **Solutions**: Enable `echoCancellation: true` in `mediaConfiguration.audio`, use headphones, mute when not speaking ### "Screen share not working" **Cause:** Browser doesn't support screen sharing API, permission denied, or wrong `displaySurface` config **Solution:** Use Chrome/Edge/Firefox (Safari limited support), check browser permissions, try different `displaySurface` values ('window', 'monitor', 'browser') ### "How do I schedule meetings?" **Cause:** RealtimeKit has no built-in scheduling system **Solution:** Store meeting IDs in your database with timestamps. Generate participant tokens only when user should join. Example: ```typescript // Store in DB { meetingId: 'abc123', scheduledFor: '2026-02-15T10:00:00Z', userId: 'user456' } // Generate token when user clicks "Join" near scheduled time const response = await fetch('/api/join-meeting', { method: 'POST', body: JSON.stringify({ meetingId: 'abc123' }) }); const { authToken } = await response.json(); ``` ### "Recording not starting" **Cause:** Preset lacks recording permissions, no active session, or API call from client **Solution:** Verify preset has `canRecord: true` and `canStartStopRecording: true`, ensure session is active (at least one participant), make recording API calls server-side only ## Limits | Resource | Limit | |----------|-------| | Max participants per session | 100 | | Max concurrent sessions per App | 1000 | | Max recording duration | 6 hours | | Max meeting duration | 24 hours | | Max chat message length | 4000 characters | | Max preset name length | 64 characters | | Max meeting title length | 256 characters | | Max participant name length | 256 characters | | Token expiration | 24 hours (default) | | WebRTC ports required | UDP 1024-65535 | ## Network Requirements ### Firewall Rules Allow outbound UDP/TCP to: - `*.cloudflare.com` ports 443, 80 - UDP ports 1024-65535 (WebRTC media) ### TURN Service Enable for users behind restrictive firewalls/proxies: ```jsonc // wrangler.jsonc { "vars": { "TURN_SERVICE_ID": "your_turn_service_id" } // Set secret: wrangler secret put TURN_SERVICE_TOKEN } ``` TURN automatically configured in SDK when enabled in account. ## Debugging Tips ```typescript // Check devices const devices = await meeting.self.getAllDevices(); meeting.self.on('deviceListUpdate', ({ added, removed, devices }) => console.log('Devices:', { added, removed, devices })); // Monitor participants meeting.participants.joined.on('participantJoined', (p) => console.log(`${p.name} joined:`, { id: p.id, userId: p.userId, audioEnabled: p.audioEnabled, videoEnabled: p.videoEnabled })); // Check room state meeting.self.on('roomJoined', () => console.log('Room:', { meetingId: meeting.meta.meetingId, meetingTitle: meeting.meta.meetingTitle, participantCount: meeting.participants.joined.size() + 1, audioEnabled: meeting.self.audioEnabled, videoEnabled: meeting.self.videoEnabled })); // Log all events ['roomJoined', 'audioUpdate', 'videoUpdate', 'screenShareUpdate', 'deviceUpdate', 'deviceListUpdate'].forEach(event => meeting.self.on(event, (data) => console.log(`[self] ${event}:`, data))); ['participantJoined', 'participantLeft'].forEach(event => meeting.participants.joined.on(event, (data) => console.log(`[participants] ${event}:`, data))); meeting.chat.on('chatUpdate', (data) => console.log('[chat] chatUpdate:', data)); ``` ## Security & Performance ### Security: Do NOT - Expose `CLOUDFLARE_API_TOKEN` in client code, hardcode credentials in frontend - Reuse participant tokens, store tokens in localStorage without encryption - Allow client-side meeting creation ### Security: DO - Generate tokens server-side only, use HTTPS, implement rate limiting - Validate user auth before generating tokens, use `custom_participant_id` to map to your user system - Set appropriate preset permissions per user role, rotate API tokens regularly ### Performance - **CPU**: Lower video resolution/frameRate, disable video for audio-only, use `meeting.participants.active` for large meetings, implement virtual scrolling - **Bandwidth**: Set max resolution in `mediaConfiguration`, disable screenshare audio if unneeded, use audio-only mode, implement adaptive bitrate - **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 ================================================ FILE: skills/.curated/cloudflare-deploy/references/realtimekit/patterns.md ================================================ # RealtimeKit Patterns ## UI Kit (Minimal Code) ```tsx // React import { RtkMeeting } from '@cloudflare/realtimekit-react-ui'; console.log('Left')} /> // Angular @Component({ template: `` }) export class AppComponent { authToken = ''; onLeave(event: unknown) {} } // HTML/Web Components ``` ## UI Components RealtimeKit provides 133+ pre-built Stencil.js Web Components with framework wrappers: ### Layout Components - `` - Full meeting UI (all-in-one) - ``, ``, `` - Layout sections - `` - Chat/participants sidebar - `` - Adaptive video grid ### Control Components - ``, `` - Media controls - `` - Screen sharing - `` - Leave meeting - `` - Device settings ### Grid Variants - `` - Active speaker focus - `` - Audio-only mode - `` - Paginated layout **See full catalog**: https://docs.realtime.cloudflare.com/ui-kit ## Core SDK Patterns ### Basic Setup ```typescript import RealtimeKitClient from '@cloudflare/realtimekit'; const meeting = new RealtimeKitClient({ authToken, video: true, audio: true }); meeting.self.on('roomJoined', () => console.log('Joined:', meeting.meta.meetingTitle)); meeting.participants.joined.on('participantJoined', (p) => console.log(`${p.name} joined`)); await meeting.join(); ``` ### Video Grid & Device Selection ```typescript // Video grid function VideoGrid({ meeting }) { const [participants, setParticipants] = useState([]); useEffect(() => { const update = () => setParticipants(meeting.participants.joined.toArray()); meeting.participants.joined.on('participantJoined', update); meeting.participants.joined.on('participantLeft', update); update(); return () => { meeting.participants.joined.off('participantJoined', update); meeting.participants.joined.off('participantLeft', update); }; }, [meeting]); return
{participants.map(p => )}
; } function VideoTile({ participant }) { const videoRef = useRef(null); useEffect(() => { if (videoRef.current && participant.videoTrack) videoRef.current.srcObject = new MediaStream([participant.videoTrack]); }, [participant.videoTrack]); return
; } // Device selection const devices = await meeting.self.getAllDevices(); const switchCamera = (deviceId: string) => { const device = devices.find(d => d.deviceId === deviceId); if (device) await meeting.self.setDevice(device); }; ``` ## React Hooks (Official) ```typescript import { useRealtimeKitClient, useRealtimeKitSelector } from '@cloudflare/realtimekit-react-ui'; function MyComponent() { const [meeting, initMeeting] = useRealtimeKitClient(); const audioEnabled = useRealtimeKitSelector(m => m.self.audioEnabled); const participantCount = useRealtimeKitSelector(m => m.participants.joined.size()); useEffect(() => { initMeeting({ authToken: '' }); }, []); return
{participantCount} participants
; } ``` **Benefits:** Automatic re-renders, memoized selectors, type-safe ## Waitlist Handling ```typescript // Monitor waitlist meeting.participants.waitlisted.on('participantJoined', (participant) => { console.log(`${participant.name} is waiting`); // Show admin UI to approve/reject }); // Approve from waitlist (backend only) await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/realtime/kit/${appId}/meetings/${meetingId}/active-session/waitlist/approve`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}` }, body: JSON.stringify({ user_ids: [participant.userId] }) } ); // Client receives automatic transition when approved meeting.self.on('roomJoined', () => console.log('Approved and joined')); ``` ## Audio-Only Mode ```typescript const meeting = new RealtimeKitClient({ authToken: '', video: false, // Disable video audio: true, mediaConfiguration: { audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } } }); // Use audio grid component import { RtkAudioGrid } from '@cloudflare/realtimekit-react-ui'; ``` ## Addon System ```typescript // List available addons meeting.plugins.all.forEach(plugin => { console.log(plugin.id, plugin.name, plugin.active); }); // Activate collaborative app await meeting.plugins.activate('whiteboard-addon-id'); // Listen for activations meeting.plugins.on('pluginActivated', ({ plugin }) => { console.log(`${plugin.name} activated`); }); // Deactivate await meeting.plugins.deactivate(); ``` ## Backend Integration ### Token Generation (Workers) ```typescript export interface Env { CLOUDFLARE_API_TOKEN: string; CLOUDFLARE_ACCOUNT_ID: string; REALTIMEKIT_APP_ID: string; } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname === '/api/join-meeting') { const { meetingId, userName, presetName } = await request.json(); const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/realtime/kit/${env.REALTIMEKIT_APP_ID}/meetings/${meetingId}/participants`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${env.CLOUDFLARE_API_TOKEN}` }, body: JSON.stringify({ name: userName, preset_name: presetName }) } ); const data = await response.json(); return Response.json({ authToken: data.result.authToken }); } return new Response('Not found', { status: 404 }); } }; ``` ## Best Practices ### Security 1. **Never expose API tokens client-side** - Generate participant tokens server-side only 2. **Don't reuse participant tokens** - Generate fresh token per session, use refresh endpoint if expired 3. **Use custom participant IDs** - Map to your user system for cross-session tracking ### Performance 1. **Event-driven updates** - Listen to events, don't poll. Use `toArray()` only when needed 2. **Media quality constraints** - Set appropriate resolution/bitrate limits based on network conditions 3. **Device management** - Enable `autoSwitchAudioDevice` for better UX, handle device list updates ### Architecture 1. **Separate Apps for environments** - staging vs production to prevent data mixing 2. **Preset strategy** - Create presets at App level, reuse across meetings 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 ================================================ FILE: skills/.curated/cloudflare-deploy/references/sandbox/README.md ================================================ # Cloudflare Sandbox SDK Secure isolated code execution in containers on Cloudflare's edge. Run untrusted code, manage files, expose services, integrate with AI agents. **Use cases**: AI code execution, interactive dev environments, data analysis, CI/CD, code interpreters, multi-tenant execution. ## Architecture - Each sandbox = Durable Object + Container - Persistent across requests (same ID = same sandbox) - Isolated filesystem/processes/network - Configurable sleep/wake for cost optimization ## Quick Start ```typescript import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; export { Sandbox } from '@cloudflare/sandbox'; type Env = { Sandbox: DurableObjectNamespace; }; export default { async fetch(request: Request, env: Env): Promise { // CRITICAL: proxyToSandbox MUST be called first for preview URLs const proxyResponse = await proxyToSandbox(request, env); if (proxyResponse) return proxyResponse; const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); const result = await sandbox.exec('python3 -c "print(2 + 2)"'); return Response.json({ output: result.stdout }); } }; ``` **wrangler.jsonc**: ```jsonc { "name": "my-sandbox-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date for new projects "containers": [{ "class_name": "Sandbox", "image": "./Dockerfile", "instance_type": "lite", // lite | standard | heavy "max_instances": 5 }], "durable_objects": { "bindings": [{ "class_name": "Sandbox", "name": "Sandbox" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Sandbox"] }] } ``` **Dockerfile**: ```dockerfile FROM docker.io/cloudflare/sandbox:latest RUN pip3 install --no-cache-dir pandas numpy matplotlib EXPOSE 8080 3000 # Required for wrangler dev ``` ## Core APIs - `getSandbox(namespace, id, options?)` → Get/create sandbox - `sandbox.exec(command, options?)` → Execute command - `sandbox.readFile(path)` / `writeFile(path, content)` → File ops - `sandbox.startProcess(command, options)` → Background process - `sandbox.exposePort(port, options)` → Get preview URL - `sandbox.createSession(options)` → Isolated session - `sandbox.wsConnect(request, port)` → WebSocket proxy - `sandbox.destroy()` → Terminate container - `sandbox.mountBucket(bucket, path, options)` → Mount S3 storage ## Critical Rules - ALWAYS call `proxyToSandbox()` first - Same ID = reuse sandbox - Use `/workspace` for persistent files - `normalizeId: true` for preview URLs - Retry on `CONTAINER_NOT_READY` ## In This Reference - [configuration.md](./configuration.md) - Config, CLI, environment setup - [api.md](./api.md) - Programmatic API, testing patterns - [patterns.md](./patterns.md) - Common workflows, CI/CD integration - [gotchas.md](./gotchas.md) - Issues, limits, best practices ## See Also - [durable-objects](../durable-objects/) - Sandbox runs on DO infrastructure - [containers](../containers/) - Container runtime fundamentals - [workers](../workers/) - Entry point for sandbox requests ================================================ FILE: skills/.curated/cloudflare-deploy/references/sandbox/api.md ================================================ # API Reference ## Command Execution ```typescript // Basic const result = await sandbox.exec('python3 script.py'); // Returns: { stdout, stderr, exitCode, success, duration } // With options await sandbox.exec('python3 test.py', { cwd: '/workspace/project', env: { API_KEY: 'secret' }, stream: true, onOutput: (stream, data) => console.log(data) }); ``` ## File Operations ```typescript // Read/Write const { content } = await sandbox.readFile('/workspace/data.txt'); await sandbox.writeFile('/workspace/file.txt', 'content'); // Auto-creates dirs // List/Delete const files = await sandbox.listFiles('/workspace'); await sandbox.deleteFile('/workspace/temp.txt'); await sandbox.deleteFile('/workspace/dir', { recursive: true }); // Utils await sandbox.mkdir('/workspace/dir', { recursive: true }); await sandbox.pathExists('/workspace/file.txt'); ``` ## Background Processes ```typescript // Start const process = await sandbox.startProcess('python3 -m http.server 8080', { processId: 'web-server', cwd: '/workspace/public', env: { PORT: '8080' } }); // Returns: { id, pid, command } // Wait for readiness await process.waitForPort(8080); // Wait for port to listen await process.waitForLog(/Server running/); // Wait for log pattern await process.waitForExit(); // Wait for completion // Management const processes = await sandbox.listProcesses(); const info = await sandbox.getProcess('web-server'); await sandbox.stopProcess('web-server'); const logs = await sandbox.getProcessLogs('web-server'); ``` ## Port Exposure ```typescript // Expose port const { url } = await sandbox.exposePort(8080, { name: 'web-app', hostname: request.hostname }); // Management await sandbox.isPortExposed(8080); await sandbox.getExposedPorts(request.hostname); await sandbox.unexposePort(8080); ``` ## Sessions (Isolated Contexts) Each session maintains own shell state, env vars, cwd, process namespace. ```typescript // Create with context const session = await sandbox.createSession({ id: 'user-123', cwd: '/workspace/user123', env: { USER_ID: '123' } }); // Use (full sandbox API) await session.exec('echo $USER_ID'); await session.writeFile('config.txt', 'data'); // Manage await sandbox.getSession('user-123'); await sandbox.deleteSession('user-123'); ``` ## Code Interpreter ```typescript // Create context with variables const ctx = await sandbox.createCodeContext({ language: 'python', variables: { data: [1, 2, 3, 4, 5], config: { verbose: true } } }); // Execute code with rich outputs const result = await ctx.runCode(` import matplotlib.pyplot as plt plt.plot(data, [x**2 for x in data]) plt.savefig('plot.png') print(f"Processed {len(data)} points") `); // Returns: { outputs: [{ type: 'text'|'image'|'html', content }], error } // Context persists variables across runs const result2 = await ctx.runCode('print(data[0])'); // Still has 'data' ``` ## WebSocket Connections ```typescript // Proxy WebSocket to sandbox service export default { async fetch(request: Request, env: Env): Promise { const proxyResponse = await proxyToSandbox(request, env); if (proxyResponse) return proxyResponse; if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') { const sandbox = getSandbox(env.Sandbox, 'realtime'); return await sandbox.wsConnect(request, 8080); } return new Response('Not a WebSocket request', { status: 400 }); } }; ``` ## Bucket Mounting (S3 Storage) ```typescript // Mount R2 bucket (production only, not wrangler dev) await sandbox.mountBucket(env.DATA_BUCKET, '/data', { readOnly: false }); // Access files in mounted bucket await sandbox.exec('ls /data'); await sandbox.writeFile('/data/output.txt', 'result'); // Unmount await sandbox.unmountBucket('/data'); ``` **Note**: Bucket mounting only works in production. Mounted buckets are sandbox-scoped (visible to all sessions in that sandbox). ## Lifecycle Management ```typescript // Terminate container immediately await sandbox.destroy(); // REQUIRED when using keepAlive: true const sandbox = getSandbox(env.Sandbox, 'temp', { keepAlive: true }); try { await sandbox.writeFile('/tmp/code.py', code); const result = await sandbox.exec('python /tmp/code.py'); return result.stdout; } finally { await sandbox.destroy(); // Free resources } ``` Deletes: files, processes, sessions, network connections, exposed ports. ## Error Handling ```typescript // Command errors const result = await sandbox.exec('python3 invalid.py'); if (!result.success) { console.error('Exit code:', result.exitCode); console.error('Stderr:', result.stderr); } // SDK errors try { await sandbox.readFile('/nonexistent'); } catch (error) { if (error.code === 'FILE_NOT_FOUND') { /* ... */ } else if (error.code === 'CONTAINER_NOT_READY') { /* retry */ } else if (error.code === 'TIMEOUT') { /* ... */ } } // Retry pattern (see gotchas.md for full implementation) ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/sandbox/configuration.md ================================================ # Configuration ## getSandbox Options ```typescript const sandbox = getSandbox(env.Sandbox, 'sandbox-id', { normalizeId: true, // lowercase ID (required for preview URLs) sleepAfter: '10m', // sleep after inactivity: '5m', '1h', '2d' (default: '10m') keepAlive: false, // false = auto-timeout, true = never sleep containerTimeouts: { instanceGetTimeoutMS: 30000, // 30s for provisioning (default: 30000) portReadyTimeoutMS: 90000 // 90s for container startup (default: 90000) } }); ``` **Sleep Config**: - `sleepAfter`: Duration string (e.g., '5m', '10m', '1h') - default: '10m' - `keepAlive: false`: Auto-sleep (default, cost-optimized) - `keepAlive: true`: Never sleep (higher cost, requires explicit `destroy()`) - Sleeping sandboxes wake automatically (cold start) ## Instance Types wrangler.jsonc `instance_type`: - `lite`: 256MB RAM, 0.5 vCPU (default) - `standard`: 512MB RAM, 1 vCPU - `heavy`: 1GB RAM, 2 vCPU ## Dockerfile Patterns **Basic**: ```dockerfile FROM docker.io/cloudflare/sandbox:latest RUN pip3 install --no-cache-dir pandas numpy EXPOSE 8080 # Required for wrangler dev ``` **Scientific**: ```dockerfile FROM docker.io/cloudflare/sandbox:latest RUN pip3 install --no-cache-dir \ jupyter-server ipykernel matplotlib \ pandas seaborn plotly scipy scikit-learn ``` **Node.js**: ```dockerfile FROM docker.io/cloudflare/sandbox:latest RUN npm install -g typescript ts-node ``` **CRITICAL**: `EXPOSE` required for `wrangler dev` port access. Production auto-exposes all ports. ## CLI Commands ```bash # Dev wrangler dev # Start local dev server wrangler deploy # Deploy to production wrangler tail # Monitor logs wrangler containers list # Check container status wrangler secret put KEY # Set secret ``` ## Environment & Secrets **wrangler.jsonc**: ```jsonc { "vars": { "ENVIRONMENT": "production", "API_URL": "https://api.example.com" }, "r2_buckets": [{ "binding": "DATA_BUCKET", "bucket_name": "my-data-bucket" }] } ``` **Usage**: ```typescript const token = env.GITHUB_TOKEN; // From wrangler secret await sandbox.exec('git clone ...', { env: { GIT_TOKEN: token } }); ``` ## Preview URL Setup **Prerequisites**: - Custom domain with wildcard DNS: `*.yourdomain.com → worker.yourdomain.com` - `.workers.dev` domains NOT supported - `normalizeId: true` in getSandbox - `proxyToSandbox()` called first in fetch handler ## Cron Triggers (Pre-warming) ```jsonc { "triggers": { "crons": ["*/5 * * * *"] // Every 5 minutes } } ``` ```typescript export default { async scheduled(event: ScheduledEvent, env: Env) { const sandbox = getSandbox(env.Sandbox, 'main'); await sandbox.exec('echo "keepalive"'); // Wake sandbox } }; ``` ## Logging Configuration **wrangler.jsonc**: ```jsonc { "vars": { "SANDBOX_LOG_LEVEL": "debug", // debug | info | warn | error (default: info) "SANDBOX_LOG_FORMAT": "pretty" // json | pretty (default: json) } } ``` **Dev**: `debug` + `pretty`. **Production**: `info`/`warn` + `json`. ## Timeout Environment Overrides Override default timeouts via environment variables: ```jsonc { "vars": { "SANDBOX_INSTANCE_TIMEOUT_MS": "60000", // Override instanceGetTimeoutMS "SANDBOX_PORT_TIMEOUT_MS": "120000" // Override portReadyTimeoutMS } } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/sandbox/gotchas.md ================================================ # Gotchas & Best Practices ## Common Errors ### "Container running indefinitely" **Cause:** `keepAlive: true` without calling `destroy()` **Solution:** Always call `destroy()` when done with keepAlive containers ```typescript const sandbox = getSandbox(env.Sandbox, 'temp', { keepAlive: true }); try { const result = await sandbox.exec('python script.py'); return result.stdout; } finally { await sandbox.destroy(); // REQUIRED to free resources } ``` ### "CONTAINER_NOT_READY" **Cause:** Container still provisioning (first request or after sleep) **Solution:** Retry after 2-3s ```typescript async function execWithRetry(sandbox, cmd) { for (let i = 0; i < 3; i++) { try { return await sandbox.exec(cmd); } catch (e) { if (e.code === 'CONTAINER_NOT_READY') { await new Promise(r => setTimeout(r, 2000)); continue; } throw e; } } } ``` ### "Connection refused: container port not found" **Cause:** Missing `EXPOSE` directive in Dockerfile **Solution:** Add `EXPOSE ` to Dockerfile (only needed for `wrangler dev`, production auto-exposes) ### "Preview URLs not working" **Cause:** Custom domain not configured, wildcard DNS missing, `normalizeId` not set, or `proxyToSandbox()` not called **Solution:** Check: 1. Custom domain configured? (not `.workers.dev`) 2. Wildcard DNS set up? (`*.domain.com → worker.domain.com`) 3. `normalizeId: true` in getSandbox? 4. `proxyToSandbox()` called first in fetch? ### "Slow first request" **Cause:** Cold start (container provisioning) **Solution:** - Use `sleepAfter` instead of creating new sandboxes - Pre-warm with cron triggers - Set `keepAlive: true` for critical sandboxes ### "File not persisting" **Cause:** Files in `/tmp` or other ephemeral paths **Solution:** Use `/workspace` for persistent files ### "Bucket mounting doesn't work locally" **Cause:** Bucket mounting requires FUSE, not available in `wrangler dev` **Solution:** Test bucket mounting in production only. Use mock data locally. ### "Different normalizeId = different sandbox" **Cause:** Changing `normalizeId` option changes Durable Object ID **Solution:** Set `normalizeId` consistently. `normalizeId: true` lowercases the ID. ```typescript // These create DIFFERENT sandboxes: getSandbox(env.Sandbox, 'MyApp'); // DO ID: hash('MyApp') getSandbox(env.Sandbox, 'MyApp', { normalizeId: true }); // DO ID: hash('myapp') ``` ### "Code context variables disappeared" **Cause:** Container restart clears code context state **Solution:** Code contexts are ephemeral. Recreate context after container sleep/wake. ## Performance Optimization ### Sandbox ID Strategy ```typescript // ❌ BAD: New sandbox every time (slow) const sandbox = getSandbox(env.Sandbox, `user-${Date.now()}`); // ✅ GOOD: Reuse per user const sandbox = getSandbox(env.Sandbox, `user-${userId}`); ``` ### Sleep & Traffic Config ```typescript // Cost-optimized getSandbox(env.Sandbox, 'id', { sleepAfter: '30m', keepAlive: false }); // Always-on (requires destroy()) getSandbox(env.Sandbox, 'id', { keepAlive: true }); ``` ```jsonc // High traffic: increase max_instances { "containers": [{ "class_name": "Sandbox", "max_instances": 50 }] } ``` ## Security Best Practices ### Sandbox Isolation - Each sandbox = isolated container (filesystem, network, processes) - Use unique sandbox IDs per tenant for multi-tenant apps - Sandboxes cannot communicate directly ### Input Validation ```typescript // ❌ DANGEROUS: Command injection const result = await sandbox.exec(`python3 -c "${userCode}"`); // ✅ SAFE: Write to file, execute file await sandbox.writeFile('/workspace/user_code.py', userCode); const result = await sandbox.exec('python3 /workspace/user_code.py'); ``` ### Resource Limits ```typescript // Timeout long-running commands const result = await sandbox.exec('python3 script.py', { timeout: 30000 // 30 seconds }); ``` ### Secrets Management ```typescript // ❌ NEVER hardcode secrets const token = 'ghp_abc123'; // ✅ Use environment secrets const token = env.GITHUB_TOKEN; // Pass to sandbox via exec env const result = await sandbox.exec('git clone ...', { env: { GIT_TOKEN: token } }); ``` ### Preview URL Security Preview URLs include auto-generated tokens: ``` https://8080-sandbox-abc123def456.yourdomain.com ``` Token changes on each expose operation, preventing unauthorized access. ## Limits | Resource | Lite | Standard | Heavy | |----------|------|----------|-------| | RAM | 256MB | 512MB | 1GB | | vCPU | 0.5 | 1 | 2 | | Operation | Default Timeout | Override | |-----------|----------------|----------| | Container provisioning | 30s | `SANDBOX_INSTANCE_TIMEOUT_MS` | | Port readiness | 90s | `SANDBOX_PORT_TIMEOUT_MS` | | exec() | 120s | `timeout` option | | sleepAfter | 10m | `sleepAfter` option | **Performance**: - **First deploy**: 2-3 min for container build - **Cold start**: 2-3s when waking from sleep - **Bucket mounting**: Production only (FUSE not in dev) ## Production Guide See: https://developers.cloudflare.com/sandbox/guides/production-deployment/ ## Resources - [Official Docs](https://developers.cloudflare.com/sandbox/) - [API Reference](https://developers.cloudflare.com/sandbox/api/) - [Examples](https://github.com/cloudflare/sandbox-sdk/tree/main/examples) - [npm Package](https://www.npmjs.com/package/@cloudflare/sandbox) - [Discord Support](https://discord.cloudflare.com) ================================================ FILE: skills/.curated/cloudflare-deploy/references/sandbox/patterns.md ================================================ # Common Patterns ## AI Code Execution with Code Context ```typescript export default { async fetch(request: Request, env: Env): Promise { const { code, variables } = await request.json(); const sandbox = getSandbox(env.Sandbox, 'ai-agent'); // Create context with persistent variables const ctx = await sandbox.createCodeContext({ language: 'python', variables: variables || {} }); // Execute with rich outputs (text, images, HTML) const result = await ctx.runCode(code); return Response.json({ outputs: result.outputs, // [{ type: 'text'|'image'|'html', content }] error: result.error, success: !result.error }); } }; ``` ## Interactive Dev Environment ```typescript export default { async fetch(request: Request, env: Env): Promise { const proxyResponse = await proxyToSandbox(request, env); if (proxyResponse) return proxyResponse; const sandbox = getSandbox(env.Sandbox, 'ide', { normalizeId: true }); if (request.url.endsWith('/start')) { await sandbox.exec('curl -fsSL https://code-server.dev/install.sh | sh'); await sandbox.startProcess('code-server --bind-addr 0.0.0.0:8080', { processId: 'vscode' }); const exposed = await sandbox.exposePort(8080); return Response.json({ url: exposed.url }); } return new Response('Try /start'); } }; ``` ## WebSocket Real-Time Service ```typescript export default { async fetch(request: Request, env: Env): Promise { const proxyResponse = await proxyToSandbox(request, env); if (proxyResponse) return proxyResponse; if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') { const sandbox = getSandbox(env.Sandbox, 'realtime-service'); return await sandbox.wsConnect(request, 8080); } // Non-WebSocket: expose preview URL const sandbox = getSandbox(env.Sandbox, 'realtime-service'); const { url } = await sandbox.exposePort(8080, { hostname: new URL(request.url).hostname }); return Response.json({ wsUrl: url.replace('https', 'wss') }); } }; ``` **Dockerfile**: ```dockerfile FROM docker.io/cloudflare/sandbox:latest RUN npm install -g ws EXPOSE 8080 ``` ## Process Readiness Pattern ```typescript export default { async fetch(request: Request, env: Env): Promise { const sandbox = getSandbox(env.Sandbox, 'app-server'); // Start server const process = await sandbox.startProcess( 'node server.js', { processId: 'server' } ); // Wait for server to be ready await process.waitForPort(8080); // Wait for port listening // Now safe to expose const { url } = await sandbox.exposePort(8080); return Response.json({ url }); } }; ``` ## Persistent Data with Bucket Mounting ```typescript export default { async fetch(request: Request, env: Env): Promise { const sandbox = getSandbox(env.Sandbox, 'data-processor'); // Mount R2 bucket (production only) await sandbox.mountBucket(env.DATA_BUCKET, '/data', { readOnly: false }); // Process files in bucket const result = await sandbox.exec('python3 /workspace/process.py', { env: { DATA_DIR: '/data/input' } }); // Results written to /data/output are persisted in R2 return Response.json({ success: result.success }); } }; ``` ## CI/CD Pipeline ```typescript export default { async fetch(request: Request, env: Env): Promise { const { repo, branch } = await request.json(); const sandbox = getSandbox(env.Sandbox, `ci-${repo}-${Date.now()}`); await sandbox.exec(`git clone -b ${branch} ${repo} /workspace/repo`); const install = await sandbox.exec('npm install', { cwd: '/workspace/repo', stream: true, onOutput: (stream, data) => console.log(data) }); if (!install.success) { return Response.json({ success: false, error: 'Install failed' }); } const test = await sandbox.exec('npm test', { cwd: '/workspace/repo' }); return Response.json({ success: test.success, output: test.stdout, exitCode: test.exitCode }); } }; ``` ## Multi-Tenant Pattern ```typescript export default { async fetch(request: Request, env: Env): Promise { const userId = request.headers.get('X-User-ID'); const sandbox = getSandbox(env.Sandbox, 'multi-tenant'); // Each user gets isolated session let session; try { session = await sandbox.getSession(userId); } catch { session = await sandbox.createSession({ id: userId, cwd: `/workspace/users/${userId}`, env: { USER_ID: userId } }); } const code = await request.text(); const result = await session.exec(`python3 -c "${code}"`); return Response.json({ output: result.stdout }); } }; ``` ## Git Operations ```typescript // Clone repo await sandbox.exec('git clone https://github.com/user/repo.git /workspace/repo'); // Authenticated (use env secrets) await sandbox.exec(`git clone https://${env.GITHUB_TOKEN}@github.com/user/repo.git`); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/secrets-store/README.md ================================================ # Cloudflare Secrets Store Account-level encrypted secret management for Workers and AI Gateway. ## Overview **Secrets Store**: Centralized, account-level secrets, reusable across Workers **Worker Secrets**: Per-Worker secrets (`wrangler secret put`) ### Architecture - **Store**: Container (1/account in beta) - **Secret**: String ≤1024 bytes - **Scopes**: Permission boundaries controlling access - `workers`: For Workers runtime access - `ai-gateway`: For AI Gateway access - Secrets must have correct scope for binding to work - **Bindings**: Connect secrets via `env` object **Regional Availability**: Global except China Network (unavailable) ### Access Control - **Super Admin**: Full access - **Admin**: Create/edit/delete secrets, view metadata - **Deployer**: View metadata + bindings - **Reporter**: View metadata only API Token permissions: `Account Secrets Store Edit/Read` ### Limits (Beta) - 100 secrets/account - 1 store/account - 1024 bytes max/secret - Production secrets count toward limit ## When to Use **Use Secrets Store when:** - Multiple Workers share same credential - Centralized management needed - Compliance requires audit trail - Team collaboration on secrets **Use Worker Secrets when:** - Secret unique to one Worker - Simple single-Worker project - No cross-Worker sharing needed ## In This Reference ### Reading Order by Task | Task | Start Here | Then Read | |------|------------|-----------| | Quick overview | README.md | - | | First-time setup | README.md → configuration.md | api.md | | Add secret to Worker | configuration.md | api.md | | Implement access pattern | api.md | patterns.md | | Debug errors | gotchas.md | api.md | | Secret rotation | patterns.md | configuration.md | | Best practices | gotchas.md | patterns.md | ### Files - [configuration.md](./configuration.md) - Wrangler commands, binding config - [api.md](./api.md) - Binding API, get/put/delete operations - [patterns.md](./patterns.md) - Rotation, encryption, access control - [gotchas.md](./gotchas.md) - Security issues, limits, best practices ## See Also - [workers](../workers/) - Worker bindings integration - [wrangler](../wrangler/) - CLI secret management commands ================================================ FILE: skills/.curated/cloudflare-deploy/references/secrets-store/api.md ================================================ # API Reference ## Binding API ### Basic Access **CRITICAL**: Async `.get()` required - secrets NOT directly available. **`.get()` throws on error** - does NOT return null. Always use try/catch. ```typescript interface Env { API_KEY: { get(): Promise }; } export default { async fetch(request: Request, env: Env): Promise { const apiKey = await env.API_KEY.get(); return fetch("https://api.example.com", { headers: { "Authorization": `Bearer ${apiKey}` } }); } } ``` ### Error Handling ```typescript export default { async fetch(request: Request, env: Env): Promise { try { const apiKey = await env.API_KEY.get(); return fetch("https://api.example.com", { headers: { "Authorization": `Bearer ${apiKey}` } }); } catch (error) { console.error("Secret access failed:", error); return new Response("Configuration error", { status: 500 }); } } } ``` ### Multiple Secrets & Patterns ```typescript // Parallel fetch const [stripeKey, sendgridKey] = await Promise.all([ env.STRIPE_KEY.get(), env.SENDGRID_KEY.get() ]); // ❌ Missing .get() const key = env.API_KEY; // ❌ Module-level cache const CACHED_KEY = await env.API_KEY.get(); // Fails // ✅ Request-scope cache const key = await env.API_KEY.get(); // OK - reuse within request ``` ## REST API Base: `https://api.cloudflare.com/client/v4` ### Auth ```bash curl -H "Authorization: Bearer $CF_TOKEN" \ https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/secrets_store/stores ``` ### Store Operations ```bash # List GET /accounts/{account_id}/secrets_store/stores # Create POST /accounts/{account_id}/secrets_store/stores {"name": "my-store"} # Delete DELETE /accounts/{account_id}/secrets_store/stores/{store_id} ``` ### Secret Operations ```bash # List GET /accounts/{account_id}/secrets_store/stores/{store_id}/secrets # Create (single) POST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets { "name": "my_secret", "value": "secret_value", "scopes": ["workers"], "comment": "Optional" } # Create (batch) POST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets [ {"name": "secret_one", "value": "val1", "scopes": ["workers"]}, {"name": "secret_two", "value": "val2", "scopes": ["workers", "ai-gateway"]} ] # Get metadata GET /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id} # Update PATCH /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id} {"value": "new_value", "comment": "Updated"} # Delete (single) DELETE /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id} # Delete (batch) DELETE /accounts/{account_id}/secrets_store/stores/{store_id}/secrets {"secret_ids": ["id-1", "id-2"]} # Duplicate POST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}/duplicate {"name": "new_name"} # Quota GET /accounts/{account_id}/secrets_store/quota ``` ### Responses Success: ```json { "success": true, "result": { "id": "secret-id-123", "name": "my_secret", "created": "2025-01-11T12:00:00Z", "scopes": ["workers"] } } ``` Error: ```json { "success": false, "errors": [{"code": 10000, "message": "Name exists"}] } ``` ## TypeScript Helpers Official types available via `@cloudflare/workers-types`: ```typescript import type { SecretsStoreSecret } from "@cloudflare/workers-types"; interface Env { STRIPE_API_KEY: SecretsStoreSecret; DATABASE_URL: SecretsStoreSecret; WORKER_SECRET: string; // Regular Worker secret (direct access) } ``` Custom helper type: ```typescript interface SecretsStoreBinding { get(): Promise; } // Fallback helper async function getSecretWithFallback( primary: SecretsStoreBinding, fallback?: SecretsStoreBinding ): Promise { try { return await primary.get(); } catch (error) { if (fallback) return await fallback.get(); throw error; } } // Batch helper async function getAllSecrets( secrets: Record ): Promise> { const entries = await Promise.all( Object.entries(secrets).map(async ([k, v]) => [k, await v.get()]) ); return Object.fromEntries(entries); } ``` See: [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/secrets-store/configuration.md ================================================ # Configuration ## Wrangler Config ### Basic Binding **wrangler.jsonc**: ```jsonc { "secrets_store_secrets": [ { "binding": "API_KEY", "store_id": "abc123", "secret_name": "stripe_api_key" } ] } ``` **wrangler.toml** (alternative): ```toml [[secrets_store_secrets]] binding = "API_KEY" store_id = "abc123" secret_name = "stripe_api_key" ``` Fields: - `binding`: Variable name for `env` access - `store_id`: From `wrangler secrets-store store list` - `secret_name`: Identifier (no spaces) ### Environment-Specific **wrangler.jsonc**: ```jsonc { "env": { "production": { "secrets_store_secrets": [ { "binding": "API_KEY", "store_id": "prod-store", "secret_name": "prod_api_key" } ] }, "staging": { "secrets_store_secrets": [ { "binding": "API_KEY", "store_id": "staging-store", "secret_name": "staging_api_key" } ] } } } ``` **wrangler.toml** (alternative): ```toml [env.production] [[env.production.secrets_store_secrets]] binding = "API_KEY" store_id = "prod-store" secret_name = "prod_api_key" [env.staging] [[env.staging.secrets_store_secrets]] binding = "API_KEY" store_id = "staging-store" secret_name = "staging_api_key" ``` ## Wrangler Commands ### Store Management ```bash wrangler secrets-store store list wrangler secrets-store store create my-store --remote wrangler secrets-store store delete --remote ``` ### Secret Management (Production) ```bash # Create (interactive) wrangler secrets-store secret create \ --name MY_SECRET --scopes workers --remote # Create (piped) cat secret.txt | wrangler secrets-store secret create \ --name MY_SECRET --scopes workers --remote # List/get/update/delete wrangler secrets-store secret list --remote wrangler secrets-store secret get --name MY_SECRET --remote wrangler secrets-store secret update --name MY_SECRET --new-value "val" --remote wrangler secrets-store secret delete --name MY_SECRET --remote # Duplicate wrangler secrets-store secret duplicate \ --name ORIG --new-name COPY --remote ``` ### Local Development **CRITICAL**: Production secrets (`--remote`) NOT accessible in local dev. ```bash # Create local-only (no --remote) wrangler secrets-store secret create --name DEV_KEY --scopes workers wrangler dev # Uses local secrets wrangler deploy # Uses production secrets ``` Best practice: Separate names for local/prod: ```jsonc { "env": { "development": { "secrets_store_secrets": [ { "binding": "API_KEY", "store_id": "store", "secret_name": "dev_api_key" } ] }, "production": { "secrets_store_secrets": [ { "binding": "API_KEY", "store_id": "store", "secret_name": "prod_api_key" } ] } } } ``` ## Dashboard ### Creating Secrets 1. **Secrets Store** → **Create secret** 2. Fill: Name (no spaces), Value, Scope (`Workers`), Comment 3. **Save** (value hidden after) ### Adding Bindings **Method 1**: Worker → Settings → Bindings → Add → Secrets Store **Method 2**: Create secret directly from Worker settings dropdown Deploy options: - **Deploy**: Immediate 100% - **Save version**: Gradual rollout ## CI/CD ### GitHub Actions ```yaml - name: Create secret env: CLOUDFLARE_API_TOKEN: ${{ secrets.CF_TOKEN }} run: | echo "${{ secrets.API_KEY }}" | \ npx wrangler secrets-store secret create $STORE_ID \ --name API_KEY --scopes workers --remote - name: Deploy run: npx wrangler deploy ``` ### GitLab CI ```yaml script: - echo "$API_KEY_VALUE" | npx wrangler secrets-store secret create $STORE_ID --name API_KEY --scopes workers --remote - npx wrangler deploy ``` See: [api.md](./api.md), [patterns.md](./patterns.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/secrets-store/gotchas.md ================================================ # Gotchas ## Common Errors ### ".get() Throws on Error" **Cause:** Assuming `.get()` returns null on failure instead of throwing **Solution:** Always wrap `.get()` calls in try/catch blocks to handle errors gracefully ```typescript try { const key = await env.API_KEY.get(); } catch (error) { return new Response("Configuration error", { status: 500 }); } ``` ### "Logging Secret Values" **Cause:** Accidentally logging secret values in console or error messages **Solution:** Only log metadata (e.g., "Retrieved API_KEY") never the actual secret value ### "Module-Level Secret Access" **Cause:** Attempting to access secrets during module initialization before env is available **Solution:** Cache secrets in request scope only, not at module level ### "Secret not found in store" **Cause:** Secret name doesn't exist, case mismatch, missing workers scope, or incorrect store_id **Solution:** Verify secret exists with `wrangler secrets-store secret list --remote`, check name matches exactly (case-sensitive), ensure secret has `workers` scope, and verify correct store_id ### "Scope Mismatch" **Cause:** Secret exists but missing `workers` scope (only has `ai-gateway` scope) **Solution:** Update secret scopes: `wrangler secrets-store secret update --name SECRET --scopes workers --remote` or add via Dashboard ### "JSON Parsing Failure" **Cause:** Storing invalid JSON in secret, then failing to parse during runtime **Solution:** Validate JSON before storing: ```bash # Validate before storing echo '{"key":"value"}' | jq . && \ echo '{"key":"value"}' | wrangler secrets-store secret create \ --name CONFIG --scopes workers --remote ``` Runtime parsing with error handling: ```typescript try { const configStr = await env.CONFIG.get(); const config = JSON.parse(configStr); } catch (error) { console.error("Invalid config JSON:", error); return new Response("Invalid configuration", { status: 500 }); } ``` ### "Cannot access secret in local dev" **Cause:** Attempting to access production secrets in local development environment **Solution:** Create local-only secrets (without `--remote` flag) for development: `wrangler secrets-store secret create --name API_KEY --scopes workers` ### "Property 'get' does not exist" **Cause:** Missing TypeScript type definition for secret binding **Solution:** Define interface with get method: `interface Env { API_KEY: { get(): Promise }; }` ### "Binding already exists" **Cause:** Duplicate binding in dashboard or conflict between wrangler.jsonc and dashboard **Solution:** Remove duplicate from dashboard Settings → Bindings, check for conflicts, or delete old Worker secret with `wrangler secret delete API_KEY` ### "Account secret quota exceeded" **Cause:** Account has reached 100 secret limit (beta) **Solution:** Check quota with `wrangler secrets-store quota --remote`, delete unused secrets, consolidate duplicates, or contact Cloudflare for increase ## Limits | Limit | Value | Notes | |-------|-------|-------| | Max secrets per account | 100 | Beta limit | | Max stores per account | 1 | Beta limit | | Max secret size | 1024 bytes | Per secret | | Local secrets | Don't count toward limit | Only production secrets count | | Scopes available | `workers`, `ai-gateway` | Must have correct scope for access | | Scope | Account-level | Can be reused across multiple Workers | | Access method | `await env.BINDING.get()` | Async only, throws on error | | Management | Centralized | Via secrets-store commands | | Local dev | Separate local secrets | Use without `--remote` flag | | Regional availability | Global except China Network | Unavailable in China Network | See: [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/secrets-store/patterns.md ================================================ # Patterns ## Secret Rotation Zero-downtime rotation with versioned naming (`api_key_v1`, `api_key_v2`): ```typescript interface Env { PRIMARY_KEY: { get(): Promise }; FALLBACK_KEY?: { get(): Promise }; } async function fetchWithAuth(url: string, key: string) { return fetch(url, { headers: { "Authorization": `Bearer ${key}` } }); } export default { async fetch(request: Request, env: Env): Promise { let resp = await fetchWithAuth("https://api.example.com", await env.PRIMARY_KEY.get()); // Fallback during rotation if (!resp.ok && env.FALLBACK_KEY) { resp = await fetchWithAuth("https://api.example.com", await env.FALLBACK_KEY.get()); } return resp; } } ``` Workflow: Create `api_key_v2` → add fallback binding → deploy → swap primary → deploy → remove `v1` ## Encryption with KV ```typescript interface Env { CACHE: KVNamespace; ENCRYPTION_KEY: { get(): Promise }; } async function encryptValue(value: string, key: string): Promise { const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( "raw", enc.encode(key), { name: "AES-GCM" }, false, ["encrypt"] ); const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, keyMaterial, enc.encode(value) ); const combined = new Uint8Array(iv.length + encrypted.byteLength); combined.set(iv); combined.set(new Uint8Array(encrypted), iv.length); return btoa(String.fromCharCode(...combined)); } export default { async fetch(request: Request, env: Env): Promise { const key = await env.ENCRYPTION_KEY.get(); const encrypted = await encryptValue("sensitive-data", key); await env.CACHE.put("user:123:data", encrypted); return Response.json({ ok: true }); } } ``` ## HMAC Signing ```typescript interface Env { HMAC_SECRET: { get(): Promise }; } async function signRequest(data: string, secret: string): Promise { const enc = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"] ); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data)); return btoa(String.fromCharCode(...new Uint8Array(sig))); } export default { async fetch(request: Request, env: Env): Promise { const secret = await env.HMAC_SECRET.get(); const payload = await request.text(); const signature = await signRequest(payload, secret); return Response.json({ signature }); } } ``` ## Audit & Monitoring ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const startTime = Date.now(); try { const apiKey = await env.API_KEY.get(); const resp = await fetch("https://api.example.com", { headers: { "Authorization": `Bearer ${apiKey}` } }); ctx.waitUntil( fetch("https://log.example.com/log", { method: "POST", body: JSON.stringify({ event: "secret_used", secret_name: "API_KEY", timestamp: new Date().toISOString(), duration_ms: Date.now() - startTime, success: resp.ok }) }) ); return resp; } catch (error) { ctx.waitUntil( fetch("https://log.example.com/log", { method: "POST", body: JSON.stringify({ event: "secret_access_failed", secret_name: "API_KEY", error: error instanceof Error ? error.message : "Unknown" }) }) ); return new Response("Error", { status: 500 }); } } } ``` ## Migration from Worker Secrets Change `env.SECRET` (direct) to `await env.SECRET.get()` (async). Steps: 1. Create in Secrets Store: `wrangler secrets-store secret create --name API_KEY --scopes workers --remote` 2. Add binding to `wrangler.jsonc`: `{"binding": "API_KEY", "store_id": "abc123", "secret_name": "api_key"}` 3. Update code: `const key = await env.API_KEY.get();` 4. Test staging, deploy 5. Remove old: `wrangler secret delete API_KEY` ## Sharing Across Workers Same secret, different binding names: ```jsonc // worker-1: binding="SHARED_DB", secret_name="postgres_url" // worker-2: binding="DB_CONN", secret_name="postgres_url" ``` ## JSON Secret Parsing Store structured config as JSON secrets: ```typescript interface Env { DB_CONFIG: { get(): Promise }; } interface DbConfig { host: string; port: number; username: string; password: string; } export default { async fetch(request: Request, env: Env): Promise { try { const configStr = await env.DB_CONFIG.get(); const config: DbConfig = JSON.parse(configStr); // Use parsed config const dbUrl = `postgres://${config.username}:${config.password}@${config.host}:${config.port}`; return Response.json({ connected: true }); } catch (error) { if (error instanceof SyntaxError) { return new Response("Invalid config JSON", { status: 500 }); } throw error; } } } ``` Store JSON secret: ```bash echo '{"host":"db.example.com","port":5432,"username":"app","password":"secret"}' | \ wrangler secrets-store secret create \ --name DB_CONFIG --scopes workers --remote ``` ## Integration ### Service Bindings Auth Worker signs JWT with Secrets Store; API Worker verifies via service binding. See: [workers](../workers/) for service binding patterns. See: [api.md](./api.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/smart-placement/README.md ================================================ # Cloudflare Workers Smart Placement Automatic workload placement optimization to minimize latency by running Workers closer to backend infrastructure rather than end users. ## Core Concept Smart Placement automatically analyzes Worker request duration across Cloudflare's global network and intelligently routes requests to optimal data center locations. Instead of defaulting to the location closest to the end user, Smart Placement can forward requests to locations closer to backend infrastructure when this reduces overall request duration. ### When to Use **Enable Smart Placement when:** - Worker makes multiple round trips to backend services/databases - Backend infrastructure is geographically concentrated - Request duration dominated by backend latency rather than network latency from user - Running backend logic in Workers (APIs, data aggregation, SSR with DB calls) - Worker uses `fetch` handler (not RPC methods) **Do NOT enable for:** - Workers serving only static content or cached responses - Workers without significant backend communication - Pure edge logic (auth checks, redirects, simple transformations) - Workers without fetch event handlers - Workers with RPC methods or named entrypoints (only `fetch` handlers are affected) - Pages/Assets Workers with `run_worker_first = true` (degrades asset serving) ### Decision Tree ``` Does your Worker have a fetch handler? ├─ No → Smart Placement won't work (skip) └─ Yes │ Does it make multiple backend calls (DB/API)? ├─ No → Don't enable (won't help) └─ Yes │ Is backend geographically concentrated? ├─ No (globally distributed) → Probably won't help └─ Yes or uncertain │ Does it serve static assets with run_worker_first=true? ├─ Yes → Don't enable (will hurt performance) └─ No → Enable Smart Placement │ After 15min, check placement_status ├─ SUCCESS → Monitor metrics ├─ INSUFFICIENT_INVOCATIONS → Need more traffic └─ UNSUPPORTED_APPLICATION → Disable (hurting performance) ``` ### Key Architecture Pattern **Recommended:** Split full-stack applications into separate Workers: ``` User → Frontend Worker (at edge, close to user) ↓ Service Binding Backend Worker (Smart Placement enabled, close to DB/API) ↓ Database/Backend Service ``` This maintains fast, reactive frontends while optimizing backend latency. ## Quick Start ```jsonc // wrangler.jsonc { "placement": { "mode": "smart" // or "off" to explicitly disable } } ``` Deploy and wait 15 minutes for analysis. Check status via API or dashboard metrics. **To disable:** Set `"mode": "off"` or remove `placement` field entirely (both equivalent). ## Requirements - Wrangler 2.20.0+ - Analysis time: Up to 15 minutes after enabling - Traffic requirements: Consistent traffic from multiple global locations - Available on all Workers plans (Free, Paid, Enterprise) ## Placement Status Values ```typescript type PlacementStatus = | undefined // Not yet analyzed | 'SUCCESS' // Successfully optimized | 'INSUFFICIENT_INVOCATIONS' // Not enough traffic | 'UNSUPPORTED_APPLICATION'; // Made Worker slower (reverted) ``` ## CLI Commands ```bash # Deploy with Smart Placement wrangler deploy # Check placement status curl -H "Authorization: Bearer $TOKEN" \ https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/services/$WORKER_NAME \ | jq .result.placement_status # Monitor wrangler tail your-worker-name --header cf-placement ``` ## Reading Order **First time?** Start here: 1. This README - understand core concepts and when to use Smart Placement 2. [configuration.md](./configuration.md) - set up wrangler.jsonc and understand limitations 3. [patterns.md](./patterns.md) - see practical examples for your use case 4. [api.md](./api.md) - monitor and verify Smart Placement is working 5. [gotchas.md](./gotchas.md) - troubleshoot common issues **Quick lookup:** - "Should I enable Smart Placement?" → See "When to Use" above - "How do I configure it?" → [configuration.md](./configuration.md) - "How do I split frontend/backend?" → [patterns.md](./patterns.md) - "Why isn't it working?" → [gotchas.md](./gotchas.md) ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc setup, mode values, validation rules - [api.md](./api.md) - Placement Status API, cf-placement header, monitoring - [patterns.md](./patterns.md) - Frontend/backend split, database workers, SSR patterns - [gotchas.md](./gotchas.md) - Troubleshooting INSUFFICIENT_INVOCATIONS, performance issues ## See Also - [workers](../workers/) - Worker runtime and fetch handlers - [d1](../d1/) - D1 database that benefits from Smart Placement - [durable-objects](../durable-objects/) - Durable Objects with backend logic - [bindings](../bindings/) - Service bindings for frontend/backend split ================================================ FILE: skills/.curated/cloudflare-deploy/references/smart-placement/api.md ================================================ # Smart Placement API ## Placement Status API Query Worker placement status via Cloudflare API: ```bash curl -X GET "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/services/{WORKER_NAME}" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` Response includes `placement_status` field: ```typescript type PlacementStatus = | undefined // Not yet analyzed | 'SUCCESS' // Successfully optimized | 'INSUFFICIENT_INVOCATIONS' // Not enough traffic | 'UNSUPPORTED_APPLICATION'; // Made Worker slower (reverted) ``` ## Status Meanings **`undefined` (not present)** - Worker not yet analyzed - Always runs at default edge location closest to user **`SUCCESS`** - Analysis complete, Smart Placement active - Worker runs in optimal location (may be edge or remote) **`INSUFFICIENT_INVOCATIONS`** - Not enough requests to make placement decision - Requires consistent multi-region traffic - Always runs at default edge location **`UNSUPPORTED_APPLICATION`** (rare, <1% of Workers) - Smart Placement made Worker slower - Placement decision reverted - Always runs at edge location - Won't be re-analyzed until redeployed ## cf-placement Header (Beta) Smart Placement adds response header indicating routing decision: ```typescript // Remote placement (Smart Placement routed request) "cf-placement: remote-LHR" // Routed to London // Local placement (default edge routing) "cf-placement: local-EWR" // Stayed at Newark edge ``` Format: `{placement-type}-{IATA-code}` - `remote-*` = Smart Placement routed to remote location - `local-*` = Stayed at default edge location - IATA code = nearest airport to data center **Warning:** Beta feature, may be removed before GA. ## Detecting Smart Placement in Code **Note:** `cf-placement` header is a beta feature and may change or be removed. ```typescript export default { async fetch(request: Request, env: Env): Promise { const placementHeader = request.headers.get('cf-placement'); if (placementHeader?.startsWith('remote-')) { const location = placementHeader.split('-')[1]; console.log(`Smart Placement routed to ${location}`); } else if (placementHeader?.startsWith('local-')) { const location = placementHeader.split('-')[1]; console.log(`Running at edge location ${location}`); } return new Response('OK'); } } satisfies ExportedHandler; ``` ## Request Duration Metrics Available in Cloudflare dashboard when Smart Placement enabled: **Workers & Pages → [Your Worker] → Metrics → Request Duration** Shows histogram comparing: - Request duration WITH Smart Placement (99% of traffic) - Request duration WITHOUT Smart Placement (1% baseline) **Request Duration vs Execution Duration:** - **Request duration:** Total time from request arrival to response delivery (includes network latency) - **Execution duration:** Time Worker code actively executing (excludes network waits) Use request duration to measure Smart Placement impact. ### Interpreting Metrics | Metric Comparison | Interpretation | Action | |-------------------|----------------|--------| | WITH < WITHOUT | Smart Placement helping | Keep enabled | | WITH ≈ WITHOUT | Neutral impact | Consider disabling to free resources | | WITH > WITHOUT | Smart Placement hurting | Disable with `mode: "off"` | **Why Smart Placement might hurt performance:** - Worker primarily serves static assets or cached content - Backend services are globally distributed (no single optimal location) - Worker has minimal backend communication - Using Pages with `assets.run_worker_first = true` **Typical improvements when Smart Placement helps:** - 20-50% reduction in request duration for database-heavy Workers - 30-60% reduction for Workers making multiple backend API calls - Larger improvements when backend is geographically concentrated ## Monitoring Commands ```bash # Tail Worker logs wrangler tail your-worker-name # Tail with filters wrangler tail your-worker-name --status error wrangler tail your-worker-name --header cf-placement # Check placement status via API curl -H "Authorization: Bearer $TOKEN" \ https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/services/$WORKER_NAME \ | jq .result.placement_status ``` ## TypeScript Types ```typescript // Placement status returned by API (field may be absent) type PlacementStatus = | 'SUCCESS' | 'INSUFFICIENT_INVOCATIONS' | 'UNSUPPORTED_APPLICATION' | undefined; // Placement configuration in wrangler.jsonc type PlacementMode = 'smart' | 'off'; interface PlacementConfig { mode: PlacementMode; // Legacy fields (deprecated/removed): // hint?: string; // REMOVED - no longer supported } // Explicit placement (separate feature from Smart Placement) interface ExplicitPlacementConfig { region?: string; host?: string; hostname?: string; // Cannot combine with mode field } // Worker metadata from API response interface WorkerMetadata { placement?: PlacementConfig | ExplicitPlacementConfig; placement_status?: PlacementStatus; } // Service Binding for backend Worker interface Env { BACKEND_SERVICE: Fetcher; // Service Binding to backend Worker DATABASE: D1Database; } // Example Worker with Service Binding export default { async fetch(request: Request, env: Env): Promise { // Forward to backend Worker with Smart Placement enabled const response = await env.BACKEND_SERVICE.fetch(request); return response; } } satisfies ExportedHandler; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/smart-placement/configuration.md ================================================ # Smart Placement Configuration ## wrangler.jsonc Setup ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "placement": { "mode": "smart" } } ``` ## Placement Mode Values | Mode | Behavior | |------|----------| | `"smart"` | Enable Smart Placement - automatic optimization based on traffic analysis | | `"off"` | Explicitly disable Smart Placement - always run at edge closest to user | | Not specified | Default behavior - run at edge closest to user (same as `"off"`) | **Note:** Smart Placement vs Explicit Placement are separate features. Smart Placement (`mode: "smart"`) uses automatic analysis. For manual placement control, see explicit placement options (`region`, `host`, `hostname` fields - not covered in this reference). ## Frontend + Backend Split Configuration ### Frontend Worker (No Smart Placement) ```jsonc // frontend-worker/wrangler.jsonc { "name": "frontend", "main": "frontend-worker.ts", // No "placement" - runs at edge "services": [ { "binding": "BACKEND", "service": "backend-api" } ] } ``` ### Backend Worker (Smart Placement Enabled) ```jsonc // backend-api/wrangler.jsonc { "name": "backend-api", "main": "backend-worker.ts", "placement": { "mode": "smart" }, "d1_databases": [ { "binding": "DATABASE", "database_id": "xxx" } ] } ``` ## Requirements & Limitations ### Requirements - **Wrangler version:** 2.20.0+ - **Analysis time:** Up to 15 minutes - **Traffic requirements:** Consistent multi-location traffic - **Workers plan:** All plans (Free, Paid, Enterprise) ### What Smart Placement Affects **CRITICAL LIMITATION - Smart Placement ONLY Affects `fetch` Handlers:** Smart Placement is fundamentally limited to Workers with default `fetch` handlers. This is a key architectural constraint. - ✅ **Affects:** `fetch` event handlers ONLY (the default export's fetch method) - ❌ **Does NOT affect:** - RPC methods (Service Bindings with `WorkerEntrypoint` - see example below) - Named entrypoints (exports other than `default`) - Workers without `fetch` handlers - Queue consumers, scheduled handlers, or other event types **Example - Smart Placement ONLY affects `fetch`:** ```typescript // ✅ Smart Placement affects this: export default { async fetch(request: Request, env: Env): Promise { // This runs close to backend when Smart Placement enabled const data = await env.DATABASE.prepare('SELECT * FROM users').all(); return Response.json(data); } } // ❌ Smart Placement DOES NOT affect these: export class MyRPC extends WorkerEntrypoint { async myMethod() { // This ALWAYS runs at edge, Smart Placement has NO EFFECT const data = await this.env.DATABASE.prepare('SELECT * FROM users').all(); return data; } } export async function scheduled(event: ScheduledEvent, env: Env) { // NOT affected by Smart Placement } ``` **Consequence:** If your backend logic uses RPC methods (`WorkerEntrypoint`), Smart Placement cannot optimize those calls. You must use fetch-based patterns for Smart Placement to work. **Solution:** Convert RPC methods to fetch endpoints, or use a wrapper Worker with `fetch` handler that calls your backend RPC (though this adds latency). ### Baseline Traffic Smart Placement automatically routes 1% of requests WITHOUT optimization as baseline for performance comparison. ### Validation Rules **Mutually exclusive fields:** - `mode` cannot be used with explicit placement fields (`region`, `host`, `hostname`) - Choose either Smart Placement OR explicit placement, not both ```jsonc // ✅ Valid - Smart Placement { "placement": { "mode": "smart" } } // ✅ Valid - Explicit Placement (different feature) { "placement": { "region": "us-east1" } } // ❌ Invalid - Cannot combine { "placement": { "mode": "smart", "region": "us-east1" } } ``` ## Dashboard Configuration **Workers & Pages** → Select Worker → **Settings** → **General** → **Placement: Smart** → Wait 15min → Check **Metrics** ## TypeScript Types ```typescript interface Env { BACKEND: Fetcher; DATABASE: D1Database; } export default { async fetch(request: Request, env: Env): Promise { const data = await env.DATABASE.prepare('SELECT * FROM table').all(); return Response.json(data); } } satisfies ExportedHandler; ``` ## Cloudflare Pages/Assets Warning **CRITICAL PERFORMANCE ISSUE:** Enabling Smart Placement with `assets.run_worker_first = true` in Pages projects **severely degrades asset serving performance**. This is one of the most common misconfigurations. **Why this is bad:** - Smart Placement routes ALL requests (including static assets) away from edge to remote locations - Static assets (HTML, CSS, JS, images) should ALWAYS be served from edge closest to user - Result: 2-5x slower asset loading times, poor user experience **Problem:** Smart Placement routes asset requests away from edge, but static assets should always be served from edge closest to user. **Solutions (in order of preference):** 1. **Recommended:** Split into separate Workers (frontend at edge + backend with Smart Placement) 2. Set `"mode": "off"` to explicitly disable Smart Placement for Pages/Assets Workers 3. Use `assets.run_worker_first = false` (serves assets first, bypasses Worker for static content) ```jsonc // ❌ BAD - Degrades asset performance by 2-5x { "name": "pages-app", "placement": { "mode": "smart" }, "assets": { "run_worker_first": true } } // ✅ GOOD - Frontend at edge, backend optimized // frontend-worker/wrangler.jsonc { "name": "frontend", "assets": { "run_worker_first": true } // No placement - runs at edge } // backend-worker/wrangler.jsonc { "name": "backend-api", "placement": { "mode": "smart" }, "d1_databases": [{ "binding": "DB", "database_id": "xxx" }] } ``` **Key takeaway:** Never enable Smart Placement on Workers that serve static assets with `run_worker_first = true`. ## Local Development Smart Placement does NOT work in `wrangler dev` (local only). Test by deploying: `wrangler deploy --env staging` ================================================ FILE: skills/.curated/cloudflare-deploy/references/smart-placement/gotchas.md ================================================ # Smart Placement Gotchas ## Common Errors ### "INSUFFICIENT_INVOCATIONS" **Cause:** Not enough traffic for Smart Placement to analyze **Solution:** - Ensure Worker receives consistent global traffic - Wait longer (analysis takes up to 15 minutes) - Send test traffic from multiple global locations - Check Worker has fetch event handler ### "UNSUPPORTED_APPLICATION" **Cause:** Smart Placement made Worker slower rather than faster **Reasons:** - Worker doesn't make backend calls (runs faster at edge) - Backend calls are cached (network latency to user more important) - Backend service has good global distribution - Worker serves static assets or Pages content **Solutions:** - Disable Smart Placement: `{ "placement": { "mode": "off" } }` - Review whether Worker actually benefits from Smart Placement - Consider caching strategy to reduce backend calls - For Pages/Assets Workers, use separate backend Worker with Smart Placement ### "No request duration metrics" **Cause:** Smart Placement not enabled, insufficient time passed, insufficient traffic, or analysis incomplete **Solution:** - Ensure Smart Placement enabled in config - Wait 15+ minutes after deployment - Verify Worker has sufficient traffic - Check `placement_status` is `SUCCESS` ### "cf-placement header missing" **Cause:** Smart Placement not enabled, beta feature removed, or Worker not analyzed yet **Solution:** Verify Smart Placement enabled, wait for analysis (15min), check if beta feature still available ## Pages/Assets + Smart Placement Performance Degradation **Problem:** Static assets load 2-5x slower when Smart Placement enabled with `run_worker_first = true`. **Cause:** Smart Placement routes ALL requests (including static assets like HTML, CSS, JS, images) to remote locations. Static content should ALWAYS be served from edge closest to user. **Solution:** Split into separate Workers OR disable Smart Placement: ```jsonc // ❌ BAD - Assets routed away from user { "name": "pages-app", "placement": { "mode": "smart" }, "assets": { "run_worker_first": true } } // ✅ GOOD - Assets at edge, API optimized // frontend/wrangler.jsonc { "name": "frontend", "assets": { "run_worker_first": true } // No placement field - stays at edge } // backend/wrangler.jsonc { "name": "backend-api", "placement": { "mode": "smart" } } ``` This is one of the most common and impactful Smart Placement misconfigurations. ## Monolithic Full-Stack Worker **Problem:** Frontend and backend logic in single Worker with Smart Placement enabled. **Cause:** Smart Placement optimizes for backend latency but increases user-facing response time. **Solution:** Split into two Workers: ```jsonc // frontend/wrangler.jsonc { "name": "frontend", "placement": { "mode": "off" }, // Explicit: stay at edge "services": [{ "binding": "BACKEND", "service": "backend-api" }] } // backend/wrangler.jsonc { "name": "backend-api", "placement": { "mode": "smart" }, "d1_databases": [{ "binding": "DB", "database_id": "xxx" }] } ``` ## Local Development Confusion **Issue:** Smart Placement doesn't work in `wrangler dev`. **Explanation:** Smart Placement only activates in production deployments, not local development. **Solution:** Test Smart Placement in staging environment: `wrangler deploy --env staging` ## Baseline Traffic & Analysis Time **Note:** Smart Placement routes 1% of requests WITHOUT optimization for comparison (expected). **Analysis time:** Up to 15 minutes. During analysis, Worker runs at edge. Monitor `placement_status`. ## RPC Methods Not Affected (Critical Limitation) **Problem:** Enabled Smart Placement on backend but RPC calls still slow. **Cause:** Smart Placement ONLY affects `fetch` handlers. RPC methods (Service Bindings with `WorkerEntrypoint`) are NEVER affected. **Why:** RPC bypasses `fetch` handler - Smart Placement can only route `fetch` requests. **Solution:** Convert to fetch-based Service Bindings: ```typescript // ❌ RPC - Smart Placement has NO EFFECT export class BackendRPC extends WorkerEntrypoint { async getData() { // ALWAYS runs at edge return await this.env.DATABASE.prepare('SELECT * FROM table').all(); } } // ✅ Fetch - Smart Placement WORKS export default { async fetch(request: Request, env: Env): Promise { // Runs close to DATABASE when Smart Placement enabled const data = await env.DATABASE.prepare('SELECT * FROM table').all(); return Response.json(data); } } ``` ## Requirements - **Wrangler 2.20.0+** required - **Consistent multi-region traffic** needed for analysis - **Only affects fetch handlers** - RPC methods and named entrypoints not affected ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | Analysis time | Up to 15 minutes | After enabling | | Baseline traffic | 1% | Routed without optimization | | Min Wrangler version | 2.20.0+ | Required | | Traffic requirement | Multi-region | Consistent needed | ## Disabling Smart Placement ```jsonc { "placement": { "mode": "off" } } // Explicit disable // OR remove "placement" field entirely (same effect) ``` Both behaviors identical - Worker runs at edge closest to user. ## When NOT to Use Smart Placement - Workers serving only static content or cached responses - Workers without significant backend communication - Pure edge logic (auth checks, redirects, simple transformations) - Workers without fetch event handlers - Pages/Assets Workers with `run_worker_first = true` - Workers using RPC methods instead of fetch handlers These scenarios won't benefit and may perform worse with Smart Placement. ================================================ FILE: skills/.curated/cloudflare-deploy/references/smart-placement/patterns.md ================================================ # Smart Placement Patterns ## Backend Worker with Database Access ```typescript export default { async fetch(request: Request, env: Env): Promise { const user = await env.DATABASE.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); const orders = await env.DATABASE.prepare('SELECT * FROM orders WHERE user_id = ?').bind(userId).all(); return Response.json({ user, orders }); } }; ``` ```jsonc { "placement": { "mode": "smart" }, "d1_databases": [{ "binding": "DATABASE", "database_id": "xxx" }] } ``` ## Frontend + Backend Split (Service Bindings) **Frontend:** Runs at edge for fast user response **Backend:** Smart Placement runs close to database ```typescript // Frontend Worker - routes requests to backend interface Env { BACKEND: Fetcher; // Service Binding to backend Worker } export default { async fetch(request: Request, env: Env): Promise { if (new URL(request.url).pathname.startsWith('/api/')) { return env.BACKEND.fetch(request); // Forward to backend } return new Response('Frontend content'); } }; // Backend Worker - database operations interface BackendEnv { DATABASE: D1Database; } export default { async fetch(request: Request, env: BackendEnv): Promise { const data = await env.DATABASE.prepare('SELECT * FROM table').all(); return Response.json(data); } }; ``` **CRITICAL:** Use fetch-based Service Bindings (shown above). If using RPC with `WorkerEntrypoint`, Smart Placement will NOT optimize those method calls - only `fetch` handlers are affected. **RPC vs Fetch - CRITICAL:** Smart Placement ONLY works with fetch-based bindings, NOT RPC. ```typescript // ❌ RPC - Smart Placement has NO EFFECT on backend RPC methods export class BackendRPC extends WorkerEntrypoint { async getData() { // ALWAYS runs at edge, Smart Placement ignored return await this.env.DATABASE.prepare('SELECT * FROM table').all(); } } // ✅ Fetch - Smart Placement WORKS export default { async fetch(request: Request, env: Env): Promise { // Runs close to DATABASE when Smart Placement enabled const data = await env.DATABASE.prepare('SELECT * FROM table').all(); return Response.json(data); } }; ``` ## External API Integration ```typescript export default { async fetch(request: Request, env: Env): Promise { const apiUrl = 'https://api.partner.com'; const headers = { 'Authorization': `Bearer ${env.API_KEY}` }; const [profile, transactions] = await Promise.all([ fetch(`${apiUrl}/profile`, { headers }), fetch(`${apiUrl}/transactions`, { headers }) ]); return Response.json({ profile: await profile.json(), transactions: await transactions.json() }); } }; ``` ## SSR / API Gateway Pattern ```typescript // Frontend (edge) - auth/routing close to user export default { async fetch(request: Request, env: Env) { if (!request.headers.get('Authorization')) { return new Response('Unauthorized', { status: 401 }); } const data = await env.BACKEND.fetch(request); return new Response(renderPage(await data.json()), { headers: { 'Content-Type': 'text/html' } }); } }; // Backend (Smart Placement) - DB operations close to data export default { async fetch(request: Request, env: Env) { const data = await env.DATABASE.prepare('SELECT * FROM pages WHERE id = ?').bind(pageId).first(); return Response.json(data); } }; ``` ## Durable Objects with Smart Placement **Key principle:** Smart Placement does NOT control WHERE Durable Objects run. DOs always run in their designated region (based on jurisdiction or smart location hints). **What Smart Placement DOES affect:** The location of the coordinator Worker's `fetch` handler that makes calls to multiple DOs. **Pattern:** Enable Smart Placement on coordinator Worker that aggregates data from multiple DOs: ```typescript // Worker with Smart Placement - aggregates data from multiple DOs export default { async fetch(request: Request, env: Env): Promise { const userId = new URL(request.url).searchParams.get('user'); // Get DO stubs const userDO = env.USER_DO.get(env.USER_DO.idFromName(userId)); const analyticsID = env.ANALYTICS_DO.idFromName(`analytics-${userId}`); const analyticsDO = env.ANALYTICS_DO.get(analyticsID); // Fetch from multiple DOs const [userData, analyticsData] = await Promise.all([ userDO.fetch(new Request('https://do/profile')), analyticsDO.fetch(new Request('https://do/stats')) ]); return Response.json({ user: await userData.json(), analytics: await analyticsData.json() }); } }; ``` ```jsonc // wrangler.jsonc { "placement": { "mode": "smart" }, "durable_objects": { "bindings": [ { "name": "USER_DO", "class_name": "UserDO" }, { "name": "ANALYTICS_DO", "class_name": "AnalyticsDO" } ] } } ``` **When this helps:** - Worker's `fetch` handler runs closer to DO regions, reducing network latency for multiple DO calls - Most beneficial when DOs are geographically concentrated or in specific jurisdictions - Helps when coordinator makes many sequential or parallel DO calls **When this DOESN'T help:** - DOs are globally distributed (no single optimal Worker location) - Worker only calls a single DO - DO calls are infrequent or cached ## Best Practices - Split full-stack apps: frontend at edge, backend with Smart Placement - Use fetch-based Service Bindings (not RPC) - Enable for backend logic: APIs, data aggregation, DB operations - Don't enable for: static content, edge logic, RPC methods, Pages with `run_worker_first` - Wait 15+ min for analysis, verify `placement_status = SUCCESS` ================================================ FILE: skills/.curated/cloudflare-deploy/references/snippets/README.md ================================================ # Cloudflare Snippets Skill Reference ## Description Expert guidance for **Cloudflare Snippets ONLY** - a lightweight JavaScript-based edge logic platform for modifying HTTP requests and responses. Snippets run as part of the Ruleset Engine and are included at no additional cost on paid plans (Pro, Business, Enterprise). ## What Are Snippets? Snippets are JavaScript functions executed at the edge as part of Cloudflare's Ruleset Engine. Key characteristics: - **Execution time**: 5ms CPU limit per request - **Size limit**: 32KB per snippet - **Runtime**: V8 isolate (subset of Workers APIs) - **Subrequests**: 2-5 fetch calls depending on plan - **Cost**: Included with Pro/Business/Enterprise plans ## Snippets vs Workers Decision Matrix | Factor | Choose Snippets If... | Choose Workers If... | |--------|----------------------|---------------------| | **Complexity** | Simple request/response modifications | Complex business logic, routing, middleware | | **Execution time** | <5ms sufficient | Need >5ms or variable time | | **Subrequests** | 2-5 fetch calls sufficient | Need >5 subrequests or complex orchestration | | **Code size** | <32KB sufficient | Need >32KB or npm dependencies | | **Cost** | Want zero additional cost | Can afford $5/mo + usage | | **APIs** | Need basic fetch, headers, URL | Need KV, D1, R2, Durable Objects, cron triggers | | **Deployment** | Need rule-based triggers | Want custom routing logic | **Rule of thumb**: Use Snippets for modifications, Workers for applications. ## Execution Model 1. Request arrives at Cloudflare edge 2. Ruleset Engine evaluates snippet rules (filter expressions) 3. If rule matches, snippet executes within 5ms limit 4. Modified request/response continues through pipeline 5. Response returned to client Snippets execute synchronously in the request path - performance is critical. ## Reading Order 1. **[configuration.md](configuration.md)** - Start here: setup, deployment methods (Dashboard/API/Terraform) 2. **[api.md](api.md)** - Core APIs: Request, Response, headers, `request.cf` properties 3. **[patterns.md](patterns.md)** - Real-world examples: geo-routing, A/B tests, security headers 4. **[gotchas.md](gotchas.md)** - Troubleshooting: common errors, performance tips, API limitations ## 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 ## Quick Start ```javascript // Snippet: Add security headers export default { async fetch(request) { const response = await fetch(request); const newResponse = new Response(response.body, response); newResponse.headers.set("X-Frame-Options", "DENY"); newResponse.headers.set("X-Content-Type-Options", "nosniff"); return newResponse; } } ``` Deploy via Dashboard (Rules → Snippets) or API/Terraform. See configuration.md for details. ## See Also - [Cloudflare Docs](https://developers.cloudflare.com/rules/snippets/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/snippets/api.md ================================================ # Snippets API Reference ## Request Object ### HTTP Properties ```javascript request.method // GET, POST, PUT, DELETE, etc. request.url // Full URL string request.headers // Headers object request.body // ReadableStream (for POST/PUT) request.cf // Cloudflare properties (see below) ``` ### URL Operations ```javascript const url = new URL(request.url); url.hostname // "example.com" url.pathname // "/path/to/page" url.search // "?query=value" url.searchParams.get("q") // "value" url.searchParams.set("q", "new") url.searchParams.delete("q") ``` ### Header Operations ```javascript // Read headers request.headers.get("User-Agent") request.headers.has("Authorization") request.headers.getSetCookie() // Get all Set-Cookie headers // Modify headers (create new request) const modifiedRequest = new Request(request); modifiedRequest.headers.set("X-Custom", "value") modifiedRequest.headers.delete("X-Remove") ``` ### Cloudflare Properties (`request.cf`) Access Cloudflare-specific metadata about the request: ```javascript // Geolocation request.cf.city // "San Francisco" request.cf.continent // "NA" request.cf.country // "US" request.cf.region // "California" or "CA" request.cf.regionCode // "CA" request.cf.postalCode // "94102" request.cf.latitude // "37.7749" request.cf.longitude // "-122.4194" request.cf.timezone // "America/Los_Angeles" request.cf.metroCode // "807" (DMA code) // Network request.cf.colo // "SFO" (airport code of datacenter) request.cf.asn // 13335 (ASN number) request.cf.asOrganization // "Cloudflare, Inc." // Bot Management (if enabled) request.cf.botManagement.score // 1-99 (1=bot, 99=human) request.cf.botManagement.verified_bot // true/false request.cf.botManagement.static_resource // true/false // TLS/HTTP version request.cf.tlsVersion // "TLSv1.3" request.cf.tlsCipher // "AEAD-AES128-GCM-SHA256" request.cf.httpProtocol // "HTTP/2" // Request metadata request.cf.requestPriority // "weight=192;exclusive=0" ``` **Use cases**: Geo-routing, bot detection, security decisions, analytics. ## Response Object ### Response Constructors ```javascript // Plain text new Response("Hello", { status: 200 }) // JSON Response.json({ key: "value" }, { status: 200 }) // HTML new Response("

Hi

", { status: 200, headers: { "Content-Type": "text/html" } }) // Redirect Response.redirect("https://example.com", 301) // or 302 // Stream (pass through) new Response(response.body, response) ``` ### Response Headers ```javascript // Create modified response const newResponse = new Response(response.body, response); // Set/modify headers newResponse.headers.set("X-Custom", "value") newResponse.headers.append("Set-Cookie", "session=abc; Path=/") newResponse.headers.delete("Server") // Common headers newResponse.headers.set("Cache-Control", "public, max-age=3600") newResponse.headers.set("Content-Type", "application/json") ``` ### Response Properties ```javascript response.status // 200, 404, 500, etc. response.statusText // "OK", "Not Found", etc. response.headers // Headers object response.body // ReadableStream response.ok // true if status 200-299 response.redirected // true if redirected ``` ## REST API Operations ### List Snippets ```bash GET /zones/{zone_id}/snippets ``` ### Get Snippet ```bash GET /zones/{zone_id}/snippets/{snippet_name} ``` ### Create/Update Snippet ```bash PUT /zones/{zone_id}/snippets/{snippet_name} Content-Type: multipart/form-data files=@snippet.js metadata={"main_module":"snippet.js"} ``` ### Delete Snippet ```bash DELETE /zones/{zone_id}/snippets/{snippet_name} ``` ### List Snippet Rules ```bash GET /zones/{zone_id}/rulesets/phases/http_request_snippets/entrypoint ``` ### Update Snippet Rules ```bash PUT /zones/{zone_id}/snippets/snippet_rules Content-Type: application/json { "rules": [{ "description": "Apply snippet", "enabled": true, "expression": "http.host eq \"example.com\"", "snippet_name": "my_snippet" }] } ``` ## Available APIs in Snippets ### ✅ Supported - `fetch()` - HTTP requests (2-5 subrequests per plan) - `Request` / `Response` - Standard Web APIs - `URL` / `URLSearchParams` - URL manipulation - `Headers` - Header manipulation - `TextEncoder` / `TextDecoder` - Text encoding - `crypto.subtle` - Web Crypto API (hashing, signing) - `crypto.randomUUID()` - UUID generation ### ❌ Not Supported in Snippets - `caches` API - Not available (use Workers) - `KV`, `D1`, `R2` - Storage APIs (use Workers) - `Durable Objects` - Stateful objects (use Workers) - `WebSocket` - WebSocket upgrades (use Workers) - `HTMLRewriter` - HTML parsing (use Workers) - `import` statements - No module imports - `addEventListener` - Use `export default { async fetch() {}` pattern ## Snippet Structure ```javascript export default { async fetch(request) { // Your logic here const response = await fetch(request); return response; // or modified response } } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/snippets/configuration.md ================================================ # Snippets Configuration Guide ## Configuration Methods ### 1. Dashboard (GUI) **Best for**: Quick tests, single snippets, visual rule building ``` 1. Go to zone → Rules → Snippets 2. Click "Create Snippet" or select template 3. Enter snippet name (a-z, 0-9, _ only, cannot change later) 4. Write JavaScript code (32KB max) 5. Configure snippet rule: - Expression Builder (visual) or Expression Editor (text) - Use Ruleset Engine filter expressions 6. Test with Preview/HTTP tabs 7. Deploy or Save as Draft ``` ### 2. REST API **Best for**: CI/CD, automation, programmatic management ```bash # Create/update snippet curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets/$SNIPPET_NAME" \ --request PUT \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ --form "files=@example.js" \ --form "metadata={\"main_module\": \"example.js\"}" # Create snippet rule curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets/snippet_rules" \ --request PUT \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "rules": [ { "description": "Trigger snippet on /api paths", "enabled": true, "expression": "starts_with(http.request.uri.path, \"/api/\")", "snippet_name": "api_snippet" } ] }' # List snippets curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets" \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" # Delete snippet curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets/$SNIPPET_NAME" \ --request DELETE \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" ``` ### 3. Terraform **Best for**: Infrastructure-as-code, multi-zone deployments ```hcl # Configure Terraform provider terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" version = "~> 4.0" } } } provider "cloudflare" { api_token = var.cloudflare_api_token } # Create snippet resource "cloudflare_snippet" "security_headers" { zone_id = var.zone_id name = "security_headers" main_module = "security_headers.js" files { name = "security_headers.js" content = file("${path.module}/snippets/security_headers.js") } } # Create snippet rule resource "cloudflare_snippet_rules" "security_rules" { zone_id = var.zone_id rules { description = "Apply security headers to all requests" enabled = true expression = "true" snippet_name = cloudflare_snippet.security_headers.name } } ``` ### 4. Pulumi **Best for**: Multi-cloud IaC, TypeScript/Python/Go workflows ```typescript import * as cloudflare from "@pulumi/cloudflare"; import * as fs from "fs"; // Create snippet const securitySnippet = new cloudflare.Snippet("security-headers", { zoneId: zoneId, name: "security_headers", mainModule: "security_headers.js", files: [{ name: "security_headers.js", content: fs.readFileSync("./snippets/security_headers.js", "utf8"), }], }); // Create snippet rule const snippetRule = new cloudflare.SnippetRules("security-rules", { zoneId: zoneId, rules: [{ description: "Apply security headers", enabled: true, expression: "true", snippetName: securitySnippet.name, }], }); ``` ## Filter Expressions Snippets use Cloudflare's Ruleset Engine expression language to determine when to execute. ### Common Expression Patterns ```javascript // Host matching http.host eq "example.com" http.host in {"example.com" "www.example.com"} http.host contains "example" // Path matching http.request.uri.path eq "/api/users" starts_with(http.request.uri.path, "/api/") ends_with(http.request.uri.path, ".json") matches(http.request.uri.path, "^/api/v[0-9]+/") // Query parameters http.request.uri.query contains "debug=true" // Headers http.headers["user-agent"] contains "Mobile" http.headers["accept-language"] eq "en-US" // Cookies http.cookie contains "session=" // Geolocation ip.geoip.country eq "US" ip.geoip.continent eq "EU" // Bot detection (requires Bot Management) cf.bot_management.score lt 30 // Method http.request.method eq "POST" http.request.method in {"POST" "PUT" "PATCH"} // Combine with logical operators http.host eq "example.com" and starts_with(http.request.uri.path, "/api/") ip.geoip.country eq "US" or ip.geoip.country eq "CA" not http.headers["user-agent"] contains "bot" ``` ### Expression Functions | Function | Example | Description | |----------|---------|-------------| | `starts_with()` | `starts_with(http.request.uri.path, "/api/")` | Check prefix | | `ends_with()` | `ends_with(http.request.uri.path, ".json")` | Check suffix | | `contains()` | `contains(http.headers["user-agent"], "Mobile")` | Check substring | | `matches()` | `matches(http.request.uri.path, "^/api/")` | Regex match | | `lower()` | `lower(http.host) eq "example.com"` | Convert to lowercase | | `upper()` | `upper(http.headers["x-api-key"])` | Convert to uppercase | | `len()` | `len(http.request.uri.path) gt 100` | String length | ## Deployment Workflow ### Development 1. Write snippet code locally 2. Test syntax with `node snippet.js` or TypeScript compiler 3. Deploy to Dashboard or use API with `Save as Draft` 4. Test with Preview/HTTP tabs in Dashboard 5. Enable rule when ready ### Production 1. Store snippet code in version control 2. Use Terraform/Pulumi for reproducible deployments 3. Deploy to staging zone first 4. Test with real traffic (use low-traffic subdomain) 5. Apply to production zone 6. Monitor with Analytics/Logpush ## Limits & Requirements | Resource | Limit | Notes | |----------|-------|-------| | Snippet size | 32 KB | Per snippet, compressed | | Snippet name | 64 chars | `a-z`, `0-9`, `_` only, immutable | | Snippets per zone | 20 | Soft limit, contact support for more | | Rules per zone | 20 | One rule per snippet typical | | Expression length | 4096 chars | Per rule expression | ## Authentication ### API Token (Recommended) ```bash # Create token at: https://dash.cloudflare.com/profile/api-tokens # Required permissions: Zone.Snippets:Edit, Zone.Rules:Edit export CLOUDFLARE_API_TOKEN="your_token_here" ``` ### API Key (Legacy) ```bash export CLOUDFLARE_EMAIL="your@email.com" export CLOUDFLARE_API_KEY="your_global_api_key" ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/snippets/gotchas.md ================================================ # Gotchas & Best Practices ## Common Errors ### 1000: "Snippet execution failed" Runtime error or syntax error. Wrap code in try/catch: ```javascript try { return await fetch(request); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } ``` ### 1100: "Exceeded execution limit" Code takes >5ms CPU. Simplify logic or move to Workers. ### 1201: "Multiple origin fetches" Call `fetch(request)` exactly once: ```javascript // ❌ Multiple origin fetches const r1 = await fetch(request); const r2 = await fetch(request); // ✅ Single fetch, reuse response const response = await fetch(request); ``` ### 1202: "Subrequest limit exceeded" Pro: 2 subrequests, Business/Enterprise: 5. Reduce fetch calls. ### "Cannot set property on immutable object" Clone before modifying: ```javascript const modifiedRequest = new Request(request); modifiedRequest.headers.set("X-Custom", "value"); ``` ### "caches is not defined" Cache API NOT available in Snippets. Use Workers. ### "Module not found" Snippets don't support `import`. Use inline code or Workers. ## Best Practices ### Performance - Keep code <10KB (32KB limit) - Optimize for 5ms CPU - Clone only when modifying - Minimize subrequests ### Security - Validate all inputs - Use Web Crypto API for hashing - Sanitize headers before origin - Don't log secrets ### Debugging ```javascript newResponse.headers.set("X-Debug-Country", request.cf.country); ``` ```bash curl -H "X-Test: true" https://example.com -v ``` ## Available APIs **✅ Available:** `fetch()`, `Request`, `Response`, `Headers`, `URL`, `crypto.subtle`, `crypto.randomUUID()`, `atob()`/`btoa()`, `JSON` **❌ NOT Available:** `caches`, `KV`, `D1`, `R2`, `Durable Objects`, `WebSocket`, `HTMLRewriter`, `import`, Node.js APIs ## Limits | Resource | Limit | |----------|-------| | Snippet size | 32KB | | Execution time | 5ms CPU | | Subrequests (Pro/Biz) | 2/5 | | Snippets/zone | 20 | ## Performance Benchmarks | Operation | Time | |-----------|------| | Header set | <0.1ms | | URL parsing | <0.2ms | | fetch() | 1-3ms | | SHA-256 | 0.5-1ms | **Migrate to Workers when:** >5ms needed, >5 subrequests, need storage (KV/D1/R2), need npm packages, >32KB code ================================================ FILE: skills/.curated/cloudflare-deploy/references/snippets/patterns.md ================================================ # Snippets Patterns ## Security Headers ```javascript export default { async fetch(request) { const response = await fetch(request); const newResponse = new Response(response.body, response); newResponse.headers.set("X-Frame-Options", "DENY"); newResponse.headers.set("X-Content-Type-Options", "nosniff"); newResponse.headers.delete("X-Powered-By"); return newResponse; } } ``` **Rule:** `true` (all requests) ## Geo-Based Routing ```javascript export default { async fetch(request) { const country = request.cf.country; if (["GB", "DE", "FR"].includes(country)) { const url = new URL(request.url); url.hostname = url.hostname.replace(".com", ".eu"); return Response.redirect(url.toString(), 302); } return fetch(request); } } ``` ## A/B Testing ```javascript export default { async fetch(request) { const cookies = request.headers.get("Cookie") || ""; let variant = cookies.match(/ab_test=([AB])/)?.[1] || (Math.random() < 0.5 ? "A" : "B"); const req = new Request(request); req.headers.set("X-Variant", variant); const response = await fetch(req); if (!cookies.includes("ab_test=")) { const newResponse = new Response(response.body, response); newResponse.headers.append("Set-Cookie", `ab_test=${variant}; Path=/; Secure`); return newResponse; } return response; } } ``` ## Bot Detection ```javascript export default { async fetch(request) { const botScore = request.cf.botManagement?.score; if (botScore && botScore < 30) return new Response("Denied", { status: 403 }); return fetch(request); } } ``` **Requires:** Bot Management plan ## API Auth Header Injection ```javascript export default { async fetch(request) { if (new URL(request.url).pathname.startsWith("/api/")) { const req = new Request(request); req.headers.set("X-Internal-Auth", "secret_token"); req.headers.delete("Authorization"); return fetch(req); } return fetch(request); } } ``` ## CORS Headers ```javascript export default { async fetch(request) { if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", "Access-Control-Allow-Headers": "Content-Type, Authorization" } }); } const response = await fetch(request); const newResponse = new Response(response.body, response); newResponse.headers.set("Access-Control-Allow-Origin", "*"); return newResponse; } } ``` ## Maintenance Mode ```javascript export default { async fetch(request) { if (request.headers.get("X-Bypass-Token") === "admin") return fetch(request); return new Response("

Maintenance

", { status: 503, headers: { "Content-Type": "text/html", "Retry-After": "3600" } }); } } ``` ## Pattern Selection | Pattern | Complexity | Use Case | |---------|-----------|----------| | Security Headers | Low | All sites | | Geo-Routing | Low | Regional content | | A/B Testing | Medium | Experiments | | Bot Detection | Medium | Requires Bot Management | | API Auth | Low | Backend protection | | CORS | Low | API endpoints | | Maintenance | Low | Deployments | ================================================ FILE: skills/.curated/cloudflare-deploy/references/spectrum/README.md ================================================ # Cloudflare Spectrum Skill Reference ## Overview Cloudflare Spectrum provides security and acceleration for ANY TCP or UDP-based application. It's a global Layer 4 (L4) reverse proxy running on Cloudflare's edge nodes that routes MQTT, email, file transfer, version control, games, and more through Cloudflare to mask origins and protect from DDoS attacks. **When to Use Spectrum**: When your protocol isn't HTTP/HTTPS (use Cloudflare proxy for HTTP). Spectrum handles everything else: SSH, gaming, databases, MQTT, SMTP, RDP, custom protocols. ## Plan Capabilities | Capability | Pro/Business | Enterprise | |------------|--------------|------------| | TCP protocols | Selected ports only | All ports (1-65535) | | UDP protocols | Selected ports only | All ports (1-65535) | | Port ranges | ❌ | ✅ | | Argo Smart Routing | ✅ | ✅ | | IP Firewall | ✅ | ✅ | | Load balancer origins | ✅ | ✅ | ## Decision Tree **What are you trying to do?** 1. **Create/manage Spectrum app** - Via Dashboard → See [Cloudflare Dashboard](https://dash.cloudflare.com) - Via API → See [api.md](api.md) - REST endpoints - Via SDK → See [api.md](api.md) - TypeScript/Python/Go examples - Via IaC → See [configuration.md](configuration.md) - Terraform/Pulumi 2. **Protect specific protocol** - SSH → See [patterns.md](patterns.md#1-ssh-server-protection) - Gaming (Minecraft, etc) → See [patterns.md](patterns.md#2-game-server) - MQTT/IoT → See [patterns.md](patterns.md#3-mqtt-broker) - SMTP/Email → See [patterns.md](patterns.md#4-smtp-relay) - Database → See [patterns.md](patterns.md#5-database-proxy) - RDP → See [patterns.md](patterns.md#6-rdp-remote-desktop) 3. **Choose origin type** - Direct IP (single server) → See [configuration.md](configuration.md#direct-ip-origin) - CNAME (hostname) → See [configuration.md](configuration.md#cname-origin) - Load balancer (HA/failover) → See [configuration.md](configuration.md#load-balancer-origin) ## Reading Order 1. Start with [patterns.md](patterns.md) for your specific protocol 2. Then [configuration.md](configuration.md) for your origin type 3. Check [gotchas.md](gotchas.md) before going to production 4. Use [api.md](api.md) for programmatic access ## See Also - [Cloudflare Docs](https://developers.cloudflare.com/spectrum/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/spectrum/api.md ================================================ ## REST API Endpoints ``` GET /zones/{zone_id}/spectrum/apps # List apps POST /zones/{zone_id}/spectrum/apps # Create app GET /zones/{zone_id}/spectrum/apps/{app_id} # Get app PUT /zones/{zone_id}/spectrum/apps/{app_id} # Update app DELETE /zones/{zone_id}/spectrum/apps/{app_id} # Delete app GET /zones/{zone_id}/spectrum/analytics/aggregate/current GET /zones/{zone_id}/spectrum/analytics/events/bytime GET /zones/{zone_id}/spectrum/analytics/events/summary ``` ## Request/Response Schemas ### CreateSpectrumAppRequest ```typescript interface CreateSpectrumAppRequest { protocol: string; // "tcp/22", "udp/53" dns: { type: "CNAME" | "ADDRESS"; name: string; // "ssh.example.com" }; origin_direct?: string[]; // ["tcp://192.0.2.1:22"] origin_dns?: { name: string }; // {"name": "origin.example.com"} origin_port?: number | { start: number; end: number }; proxy_protocol?: "off" | "v1" | "v2" | "simple"; ip_firewall?: boolean; tls?: "off" | "flexible" | "full" | "strict"; edge_ips?: { type: "dynamic" | "static"; connectivity: "all" | "ipv4" | "ipv6"; }; traffic_type?: "direct" | "http" | "https"; argo_smart_routing?: boolean; } ``` ### SpectrumApp Response ```typescript interface SpectrumApp { id: string; protocol: string; dns: { type: string; name: string }; origin_direct?: string[]; origin_dns?: { name: string }; origin_port?: number | { start: number; end: number }; proxy_protocol: string; ip_firewall: boolean; tls: string; edge_ips: { type: string; connectivity: string; ips?: string[] }; argo_smart_routing: boolean; created_on: string; modified_on: string; } ``` ## TypeScript SDK ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN }); // Create const app = await client.spectrum.apps.create({ zone_id: 'your-zone-id', protocol: 'tcp/22', dns: { type: 'CNAME', name: 'ssh.example.com' }, origin_direct: ['tcp://192.0.2.1:22'], ip_firewall: true, tls: 'off', }); // List const apps = await client.spectrum.apps.list({ zone_id: 'your-zone-id' }); // Get const appDetails = await client.spectrum.apps.get({ zone_id: 'your-zone-id', app_id: app.id }); // Update await client.spectrum.apps.update({ zone_id: 'your-zone-id', app_id: app.id, tls: 'full' }); // Delete await client.spectrum.apps.delete({ zone_id: 'your-zone-id', app_id: app.id }); // Analytics const analytics = await client.spectrum.analytics.aggregate({ zone_id: 'your-zone-id', metrics: ['bytesIngress', 'bytesEgress'], since: new Date(Date.now() - 3600000).toISOString(), }); ``` ## Python SDK ```python from cloudflare import Cloudflare client = Cloudflare(api_token="your-api-token") # Create app = client.spectrum.apps.create( zone_id="your-zone-id", protocol="tcp/22", dns={"type": "CNAME", "name": "ssh.example.com"}, origin_direct=["tcp://192.0.2.1:22"], ip_firewall=True, tls="off", ) # List apps = client.spectrum.apps.list(zone_id="your-zone-id") # Get app_details = client.spectrum.apps.get(zone_id="your-zone-id", app_id=app.id) # Update client.spectrum.apps.update(zone_id="your-zone-id", app_id=app.id, tls="full") # Delete client.spectrum.apps.delete(zone_id="your-zone-id", app_id=app.id) # Analytics analytics = client.spectrum.analytics.aggregate( zone_id="your-zone-id", metrics=["bytesIngress", "bytesEgress"], since=datetime.now() - timedelta(hours=1), ) ``` ## Go SDK ```go import "github.com/cloudflare/cloudflare-go" api, _ := cloudflare.NewWithAPIToken("your-api-token") // Create app, _ := api.CreateSpectrumApplication(ctx, "zone-id", cloudflare.SpectrumApplication{ Protocol: "tcp/22", DNS: cloudflare.SpectrumApplicationDNS{Type: "CNAME", Name: "ssh.example.com"}, OriginDirect: []string{"tcp://192.0.2.1:22"}, IPFirewall: true, ArgoSmartRouting: true, }) // List apps, _ := api.SpectrumApplications(ctx, "zone-id") // Delete _ = api.DeleteSpectrumApplication(ctx, "zone-id", app.ID) ``` ## Analytics API **Metrics:** - `bytesIngress` - Bytes received from clients - `bytesEgress` - Bytes sent to clients - `count` - Number of connections - `duration` - Connection duration (seconds) **Dimensions:** - `event` - Connection event type - `appID` - Spectrum application ID - `coloName` - Datacenter name - `ipVersion` - IPv4 or IPv6 **Example:** ```bash curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/spectrum/analytics/aggregate/current?metrics=bytesIngress,bytesEgress,count&dimensions=appID" \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" ``` ## See Also - [configuration.md](configuration.md) - Terraform/Pulumi - [patterns.md](patterns.md) - Protocol examples ================================================ FILE: skills/.curated/cloudflare-deploy/references/spectrum/configuration.md ================================================ ## Origin Types ### Direct IP Origin Use when origin is a single server with static IP. **TypeScript SDK:** ```typescript const app = await client.spectrum.apps.create({ zone_id: 'your-zone-id', protocol: 'tcp/22', dns: { type: 'CNAME', name: 'ssh.example.com' }, origin_direct: ['tcp://192.0.2.1:22'], ip_firewall: true, tls: 'off', }); ``` **Terraform:** ```hcl resource "cloudflare_spectrum_application" "ssh" { zone_id = var.zone_id protocol = "tcp/22" dns { type = "CNAME" name = "ssh.example.com" } origin_direct = ["tcp://192.0.2.1:22"] ip_firewall = true tls = "off" argo_smart_routing = true } ``` ### CNAME Origin Use when origin is a hostname (not static IP). Spectrum resolves DNS dynamically. **TypeScript SDK:** ```typescript const app = await client.spectrum.apps.create({ zone_id: 'your-zone-id', protocol: 'tcp/3306', dns: { type: 'CNAME', name: 'db.example.com' }, origin_dns: { name: 'db-primary.internal.example.com' }, origin_port: 3306, tls: 'full', }); ``` **Terraform:** ```hcl resource "cloudflare_spectrum_application" "database" { zone_id = var.zone_id protocol = "tcp/3306" dns { type = "CNAME" name = "db.example.com" } origin_dns { name = "db-primary.internal.example.com" } origin_port = 3306 tls = "full" argo_smart_routing = true } ``` ### Load Balancer Origin Use for high availability and failover. **Terraform:** ```hcl resource "cloudflare_load_balancer" "game_lb" { zone_id = var.zone_id name = "game-lb.example.com" default_pool_ids = [cloudflare_load_balancer_pool.game_pool.id] } resource "cloudflare_load_balancer_pool" "game_pool" { name = "game-primary" origins { name = "game-1"; address = "192.0.2.1" } monitor = cloudflare_load_balancer_monitor.tcp_monitor.id } resource "cloudflare_load_balancer_monitor" "tcp_monitor" { type = "tcp"; port = 25565; interval = 60; timeout = 5 } resource "cloudflare_spectrum_application" "game" { zone_id = var.zone_id protocol = "tcp/25565" dns { type = "CNAME"; name = "game.example.com" } origin_dns { name = cloudflare_load_balancer.game_lb.name } origin_port = 25565 } ``` ## TLS Configuration | Mode | Description | Use Case | Origin Cert | |------|-------------|----------|-------------| | `off` | No TLS | Non-encrypted (SSH, gaming) | No | | `flexible` | TLS client→CF, plain CF→origin | Testing | No | | `full` | TLS end-to-end, self-signed OK | Production | Yes (any) | | `strict` | Full + valid cert verification | Max security | Yes (CA) | **Example:** ```typescript const app = await client.spectrum.apps.create({ zone_id: 'your-zone-id', protocol: 'tcp/3306', dns: { type: 'CNAME', name: 'db.example.com' }, origin_direct: ['tcp://192.0.2.1:3306'], tls: 'strict', // Validates origin certificate }); ``` ## Proxy Protocol Forwards real client IP to origin. Origin must support parsing. | Version | Protocol | Use Case | |---------|----------|----------| | `off` | - | Origin doesn't need client IP | | `v1` | TCP | Most TCP apps (SSH, databases) | | `v2` | TCP | High-performance TCP | | `simple` | UDP | UDP applications | **Compatibility:** - **v1**: HAProxy, nginx, SSH, most databases - **v2**: HAProxy 1.5+, nginx 1.11+ - **simple**: Cloudflare-specific UDP format **Enable:** ```typescript const app = await client.spectrum.apps.create({ // ... proxy_protocol: 'v1', // Origin must parse PROXY header }); ``` **Origin Config (nginx):** ```nginx stream { server { listen 22 proxy_protocol; proxy_pass backend:22; } } ``` ## IP Access Rules Enable `ip_firewall: true` then configure zone-level firewall rules. ```typescript const app = await client.spectrum.apps.create({ // ... ip_firewall: true, // Applies zone firewall rules }); ``` ## Port Ranges (Enterprise Only) ```hcl resource "cloudflare_spectrum_application" "game_cluster" { zone_id = var.zone_id protocol = "tcp/25565-25575" dns { type = "CNAME" name = "games.example.com" } origin_direct = ["tcp://192.0.2.1"] origin_port { start = 25565 end = 25575 } } ``` ## See Also - [patterns.md](patterns.md) - Protocol-specific examples - [api.md](api.md) - REST/SDK reference ================================================ FILE: skills/.curated/cloudflare-deploy/references/spectrum/gotchas.md ================================================ ## Common Issues ### Connection Timeouts **Problem:** Connections fail or timeout **Cause:** Origin firewall blocking Cloudflare IPs, origin service not running, incorrect DNS **Solution:** 1. Verify origin firewall allows Cloudflare IP ranges 2. Check origin service running on correct port 3. Ensure DNS record is CNAME (not A/AAAA) 4. Verify origin IP/hostname is correct ```bash # Test connectivity nc -zv app.example.com 22 dig app.example.com ``` ### Client IP Showing Cloudflare IP **Problem:** Origin logs show Cloudflare IPs not real client IPs **Cause:** Proxy Protocol not enabled or origin not configured **Solution:** ```typescript // Enable in Spectrum app const app = await client.spectrum.apps.create({ // ... proxy_protocol: 'v1', // TCP: v1/v2; UDP: simple }); ``` **Origin config:** - **nginx**: `listen 22 proxy_protocol;` - **HAProxy**: `bind :22 accept-proxy` ### TLS Errors **Problem:** TLS handshake failures, 525 errors **Cause:** TLS mode mismatch | Error | TLS Mode | Problem | Solution | |-------|----------|---------|----------| | Connection refused | `full`/`strict` | Origin not TLS | Use `tls: "off"` or enable TLS | | 525 cert invalid | `strict` | Self-signed cert | Use `tls: "full"` or valid cert | | Handshake timeout | `flexible` | Origin expects TLS | Use `tls: "full"` | **Debug:** ```bash openssl s_client -connect app.example.com:443 -showcerts ``` ### SMTP Reverse DNS **Problem:** Email servers reject SMTP via Spectrum **Cause:** Spectrum IPs lack PTR (reverse DNS) records **Impact:** Many mail servers require valid rDNS for anti-spam **Solution:** - Outbound SMTP: NOT recommended through Spectrum - Inbound SMTP: Use Cloudflare Email Routing - Internal relay: Whitelist Spectrum IPs on destination ### Proxy Protocol Compatibility **Problem:** Connection works but app behaves incorrectly **Cause:** Origin doesn't support Proxy Protocol **Solution:** 1. Verify origin supports version (v1: widely supported, v2: HAProxy 1.5+/nginx 1.11+) 2. Test with `proxy_protocol: 'off'` first 3. Configure origin to parse headers **nginx TCP:** ```nginx stream { server { listen 22 proxy_protocol; proxy_pass backend:22; } } ``` **HAProxy:** ``` frontend ft_ssh bind :22 accept-proxy ``` ### Analytics Data Retention **Problem:** Historical data not available **Cause:** Retention varies by plan | Plan | Real-time | Historical | |------|-----------|------------| | Pro | Last hour | ❌ | | Business | Last hour | Limited | | Enterprise | Last hour | 90+ days | **Solution:** Query within retention window or export to external system ### Enterprise-Only Features **Problem:** Feature unavailable/errors **Cause:** Requires Enterprise plan **Enterprise-only:** - Port ranges (`tcp/25565-25575`) - All TCP/UDP ports (Pro/Business: selected only) - Extended analytics retention - Advanced load balancing ### IPv6 Considerations **Problem:** IPv6 clients can't connect or origin doesn't support IPv6 **Solution:** Configure `edge_ips.connectivity` ```typescript const app = await client.spectrum.apps.create({ // ... edge_ips: { type: 'dynamic', connectivity: 'ipv4', // Options: 'all', 'ipv4', 'ipv6' }, }); ``` **Options:** - `all`: Dual-stack (default, requires origin support both) - `ipv4`: IPv4 only (use if origin lacks IPv6) - `ipv6`: IPv6 only (rare) ## Limits | Resource | Pro/Business | Enterprise | |----------|--------------|------------| | Max apps | ~10-15 | 100+ | | Protocols | Selected | All TCP/UDP | | Port ranges | ❌ | ✅ | | Analytics | ~1 hour | 90+ days | ## See Also - [patterns.md](patterns.md) - Protocol examples - [configuration.md](configuration.md) - TLS/Proxy setup ================================================ FILE: skills/.curated/cloudflare-deploy/references/spectrum/patterns.md ================================================ ## Common Use Cases ### 1. SSH Server Protection **Terraform:** ```hcl resource "cloudflare_spectrum_application" "ssh" { zone_id = var.zone_id protocol = "tcp/22" dns { type = "CNAME" name = "ssh.example.com" } origin_direct = ["tcp://10.0.1.5:22"] ip_firewall = true argo_smart_routing = true } ``` **Benefits:** Hide origin IP, DDoS protection, IP firewall, Argo reduces latency ### 2. Game Server **TypeScript (Minecraft):** ```typescript const app = await client.spectrum.apps.create({ zone_id: 'your-zone-id', protocol: 'tcp/25565', dns: { type: 'CNAME', name: 'mc.example.com' }, origin_direct: ['tcp://192.168.1.10:25565'], proxy_protocol: 'v1', // Preserves player IPs argo_smart_routing: true, }); ``` **Benefits:** DDoS protection, hide origin IP, Proxy Protocol for player IPs/bans, Argo reduces latency ### 3. MQTT Broker IoT device communication. **TypeScript:** ```typescript const mqttApp = await client.spectrum.apps.create({ zone_id: 'your-zone-id', protocol: 'tcp/8883', // Use 1883 for plain MQTT dns: { type: 'CNAME', name: 'mqtt.example.com' }, origin_direct: ['tcp://mqtt-broker.internal:8883'], tls: 'full', // Use 'off' for plain MQTT }); ``` **Benefits:** DDoS protection, hide broker IP, TLS termination at edge ### 4. SMTP Relay Email submission (port 587). **WARNING**: See [gotchas.md](gotchas.md#smtp-reverse-dns) **Terraform:** ```hcl resource "cloudflare_spectrum_application" "smtp" { zone_id = var.zone_id protocol = "tcp/587" dns { type = "CNAME" name = "smtp.example.com" } origin_direct = ["tcp://mail-server.internal:587"] tls = "full" # STARTTLS support } ``` **Limitations:** - Spectrum IPs lack reverse DNS (PTR records) - Many mail servers reject without valid rDNS - Best for internal/trusted relay only ### 5. Database Proxy MySQL/PostgreSQL. **Use with caution** - security critical. **PostgreSQL:** ```typescript const postgresApp = await client.spectrum.apps.create({ zone_id: 'your-zone-id', protocol: 'tcp/5432', dns: { type: 'CNAME', name: 'postgres.example.com' }, origin_dns: { name: 'db-primary.internal.example.com' }, origin_port: 5432, tls: 'strict', // REQUIRED ip_firewall: true, // REQUIRED }); ``` **MySQL:** ```hcl resource "cloudflare_spectrum_application" "mysql" { zone_id = var.zone_id protocol = "tcp/3306" dns { type = "CNAME" name = "mysql.example.com" } origin_dns { name = "mysql-primary.internal.example.com" } origin_port = 3306 tls = "strict" ip_firewall = true } ``` **Security:** - ALWAYS use `tls: "strict"` - ALWAYS use `ip_firewall: true` - Restrict to known IPs via zone firewall - Use strong DB authentication - Consider VPN or Cloudflare Access instead ### 6. RDP (Remote Desktop) **Requires IP firewall.** **Terraform:** ```hcl resource "cloudflare_spectrum_application" "rdp" { zone_id = var.zone_id protocol = "tcp/3389" dns { type = "CNAME" name = "rdp.example.com" } origin_direct = ["tcp://windows-server.internal:3389"] tls = "off" # RDP has own encryption ip_firewall = true # REQUIRED } ``` **Security:** ALWAYS `ip_firewall: true`, whitelist admin IPs, RDP is DDoS/brute-force target ### 7. Multi-Origin Failover High availability with load balancer. **Terraform:** ```hcl resource "cloudflare_load_balancer" "database_lb" { zone_id = var.zone_id name = "db-lb.example.com" default_pool_ids = [cloudflare_load_balancer_pool.db_primary.id] fallback_pool_id = cloudflare_load_balancer_pool.db_secondary.id } resource "cloudflare_load_balancer_pool" "db_primary" { name = "db-primary-pool" origins { name = "db-1"; address = "192.0.2.1" } monitor = cloudflare_load_balancer_monitor.postgres_monitor.id } resource "cloudflare_load_balancer_pool" "db_secondary" { name = "db-secondary-pool" origins { name = "db-2"; address = "192.0.2.2" } monitor = cloudflare_load_balancer_monitor.postgres_monitor.id } resource "cloudflare_load_balancer_monitor" "postgres_monitor" { type = "tcp"; port = 5432; interval = 30; timeout = 5 } resource "cloudflare_spectrum_application" "postgres_ha" { zone_id = var.zone_id protocol = "tcp/5432" dns { type = "CNAME"; name = "postgres.example.com" } origin_dns { name = cloudflare_load_balancer.database_lb.name } origin_port = 5432 tls = "strict" ip_firewall = true } ``` **Benefits:** Automatic failover, health monitoring, traffic distribution, zero-downtime deployments ## See Also - [configuration.md](configuration.md) - Origin type setup - [gotchas.md](gotchas.md) - Protocol limitations - [api.md](api.md) - SDK reference ================================================ FILE: skills/.curated/cloudflare-deploy/references/static-assets/README.md ================================================ # Cloudflare Static Assets Skill Reference Expert guidance for deploying and configuring static assets with Cloudflare Workers. This skill covers configuration patterns, routing architectures, asset binding usage, and best practices for SPAs, SSG sites, and full-stack applications. ## Quick Start ```jsonc // wrangler.jsonc { "name": "my-app", "main": "src/index.ts", "compatibility_date": "2025-01-01", "assets": { "directory": "./dist" } } ``` ```typescript // src/index.ts export default { async fetch(request: Request, env: Env): Promise { return env.ASSETS.fetch(request); } }; ``` Deploy: `wrangler deploy` ## When to Use Workers Static Assets vs Pages | Factor | Workers Static Assets | Cloudflare Pages | |--------|----------------------|------------------| | **Use case** | Hybrid apps (static + dynamic API) | Static sites, SSG | | **Worker control** | Full control over routing | Limited (Functions) | | **Configuration** | Code-first, flexible | Git-based, opinionated | | **Dynamic routing** | Worker-first patterns | Functions (_functions/) | | **Best for** | Full-stack apps, SPAs with APIs | Jamstack, static docs | **Decision tree:** - Need custom routing logic? → Workers Static Assets - Pure static site or SSG? → Pages - API routes + SPA? → Workers Static Assets - Framework (Next, Nuxt, Remix)? → Pages ## Reading Order 1. **configuration.md** - Setup, wrangler.jsonc options, routing patterns 2. **api.md** - ASSETS binding API, request/response handling 3. **patterns.md** - Common patterns (SPA, API routes, auth, A/B testing) 4. **gotchas.md** - Limits, errors, performance tips ## 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 ## See Also - [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) - [Static Assets Docs](https://developers.cloudflare.com/workers/static-assets/) - [Cloudflare Pages](https://developers.cloudflare.com/pages/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/static-assets/api.md ================================================ # API Reference ## ASSETS Binding The `ASSETS` binding provides access to static assets via the `Fetcher` interface. ### Type Definition ```typescript interface Env { ASSETS: Fetcher; } interface Fetcher { fetch(input: RequestInfo | URL, init?: RequestInit): Promise; } ``` ### Method Signatures ```typescript // 1. Forward entire request await env.ASSETS.fetch(request); // 2. String path (hostname ignored, only path matters) await env.ASSETS.fetch("https://any-host/path/to/asset.png"); // 3. URL object await env.ASSETS.fetch(new URL("/index.html", request.url)); // 4. Constructed Request object await env.ASSETS.fetch(new Request(new URL("/logo.png", request.url), { method: "GET", headers: request.headers })); ``` **Key behaviors:** - Host/origin is ignored for string/URL inputs (only path is used) - Method must be GET (others return 405) - Request headers pass through (affects response) - Returns standard `Response` object ## Request Handling ### Path Resolution ```typescript // All resolve to same asset: env.ASSETS.fetch("https://example.com/logo.png") env.ASSETS.fetch("https://ignored.host/logo.png") env.ASSETS.fetch("/logo.png") ``` Assets are resolved relative to configured `assets.directory`. ### Headers Request headers that affect response: | Header | Effect | |--------|--------| | `Accept-Encoding` | Controls compression (gzip, brotli) | | `Range` | Enables partial content (206 responses) | | `If-None-Match` | Conditional request via ETag | | `If-Modified-Since` | Conditional request via modification date | Custom headers pass through but don't affect asset serving. ### Method Support | Method | Supported | Response | |--------|-----------|----------| | `GET` | ✅ Yes | Asset content | | `HEAD` | ✅ Yes | Headers only, no body | | `POST`, `PUT`, etc. | ❌ No | 405 Method Not Allowed | ## Response Behavior ### Content-Type Inference Automatically set based on file extension: | Extension | Content-Type | |-----------|--------------| | `.html` | `text/html; charset=utf-8` | | `.css` | `text/css` | | `.js` | `application/javascript` | | `.json` | `application/json` | | `.png` | `image/png` | | `.jpg`, `.jpeg` | `image/jpeg` | | `.svg` | `image/svg+xml` | | `.woff2` | `font/woff2` | ### Default Headers Responses include: ``` Content-Type: ETag: "" Cache-Control: public, max-age=3600 Content-Encoding: br (if supported and beneficial) ``` **Cache-Control defaults:** - 1 hour (`max-age=3600`) for most assets - Override via Worker response transformation (see patterns.md:27-35) ### Compression Automatic compression based on `Accept-Encoding`: - **Brotli** (`br`): Preferred, best compression - **Gzip** (`gzip`): Fallback - **None**: If client doesn't support or asset too small ### ETag Generation ETags are content-based hashes: ``` ETag: "a3b2c1d4e5f6..." ``` Used for conditional requests (`If-None-Match`). Returns `304 Not Modified` if match. ## Error Responses | Status | Condition | Behavior | |--------|-----------|----------| | `404` | Asset not found | Body depends on `not_found_handling` config | | `405` | Non-GET/HEAD method | `{ "error": "Method not allowed" }` | | `416` | Invalid Range header | Range not satisfiable | ### 404 Handling Depends on configuration (see configuration.md:45-52): ```typescript // not_found_handling: "single-page-application" // Returns /index.html with 200 status // not_found_handling: "404-page" // Returns /404.html if exists, else 404 response // not_found_handling: "none" // Returns 404 response ``` ## Advanced Usage ### Modifying Responses ```typescript const response = await env.ASSETS.fetch(request); // Clone and modify return new Response(response.body, { status: response.status, headers: { ...Object.fromEntries(response.headers), 'Cache-Control': 'public, max-age=31536000', 'X-Custom': 'value' } }); ``` See patterns.md:27-35 for full example. ### Error Handling ```typescript const response = await env.ASSETS.fetch(request); if (!response.ok) { // Asset not found or error return new Response('Custom error page', { status: 404 }); } return response; ``` ### Conditional Serving ```typescript const url = new URL(request.url); // Serve different assets based on conditions if (url.pathname === '/') { return env.ASSETS.fetch('/index.html'); } return env.ASSETS.fetch(request); ``` See patterns.md for complete patterns. ================================================ FILE: skills/.curated/cloudflare-deploy/references/static-assets/configuration.md ================================================ ## Configuration ### Basic Setup Minimal configuration requires only `assets.directory`: ```jsonc { "name": "my-worker", "compatibility_date": "2025-01-01", // Use current date for new projects "assets": { "directory": "./dist" } } ``` ### Full Configuration Options ```jsonc { "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", "assets": { "directory": "./dist", "binding": "ASSETS", "not_found_handling": "single-page-application", "html_handling": "auto-trailing-slash", "run_worker_first": ["/api/*", "!/api/docs/*"] } } ``` **Configuration keys:** - `directory` (string, required): Path to assets folder (e.g. `./dist`, `./public`, `./build`) - `binding` (string, optional): Name to access assets in Worker code (e.g. `env.ASSETS`). Default: `"ASSETS"` - `not_found_handling` (string, optional): Behavior when asset not found - `"single-page-application"`: Serve `/index.html` for non-asset paths (default for SPAs) - `"404-page"`: Serve `/404.html` if present, otherwise 404 - `"none"`: Return 404 for missing assets - `html_handling` (string, optional): URL trailing slash behavior - `run_worker_first` (boolean | string[], optional): Routes that invoke Worker before checking assets ### not_found_handling Modes | Mode | Behavior | Use Case | |------|----------|----------| | `"single-page-application"` | Serve `/index.html` for non-asset requests | React, Vue, Angular SPAs | | `"404-page"` | Serve `/404.html` if exists, else 404 | Static sites with custom error page | | `"none"` | Return 404 for missing assets | API-first or custom routing | ### html_handling Modes Controls trailing slash behavior for HTML files: | Mode | `/page` | `/page/` | Use Case | |------|---------|----------|----------| | `"auto-trailing-slash"` | Redirect to `/page/` if `/page/index.html` exists | Serve `/page/index.html` | Default, SEO-friendly | | `"force-trailing-slash"` | Always redirect to `/page/` | Serve if exists | Consistent trailing slashes | | `"drop-trailing-slash"` | Serve if exists | Redirect to `/page` | Cleaner URLs | | `"none"` | No modification | No modification | Custom routing logic | **Default:** `"auto-trailing-slash"` ### run_worker_first Configuration Controls which requests invoke Worker before checking assets. **Boolean syntax:** ```jsonc { "assets": { "run_worker_first": true // ALL requests invoke Worker } } ``` **Array syntax (recommended):** ```jsonc { "assets": { "run_worker_first": [ "/api/*", // Positive pattern: match API routes "/admin/*", // Match admin routes "!/admin/assets/*" // Negative pattern: exclude admin assets ] } } ``` **Pattern rules:** - Glob patterns: `*` (any chars), `**` (any path segments) - Negative patterns: Prefix with `!` to exclude - Precedence: Negative patterns override positive patterns - Default: `false` (assets served directly) **Decision guidance:** - Use `true` for API-first apps (few static assets) - Use array patterns for hybrid apps (APIs + static assets) - Use `false` for static-first sites (minimal dynamic routes) ### .assetsignore File Exclude files from upload using `.assetsignore` (same syntax as `.gitignore`): ``` # .assetsignore _worker.js *.map *.md node_modules/ .git/ ``` **Common patterns:** - `_worker.js` - Exclude Worker code from assets - `*.map` - Exclude source maps - `*.md` - Exclude markdown files - Development artifacts ### Vite Plugin Integration For Vite-based projects, use `@cloudflare/vite-plugin`: ```typescript // vite.config.ts import { defineConfig } from 'vite'; import { cloudflare } from '@cloudflare/vite-plugin'; export default defineConfig({ plugins: [ cloudflare({ assets: { directory: './dist', binding: 'ASSETS' } }) ] }); ``` **Features:** - Automatic asset detection during dev - Hot module replacement for assets - Production build integration - Requires: Wrangler 4.0.0+, `@cloudflare/vite-plugin` 1.0.0+ ### Key Compatibility Dates | Date | Feature | Impact | |------|---------|--------| | `2025-04-01` | Navigation request optimization | SPAs skip Worker for navigation, reducing costs | Use current date for new projects. See [Compatibility Dates](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) for full list. ### Environment-Specific Configuration Use `wrangler.jsonc` environments for different configs: ```jsonc { "name": "my-worker", "assets": { "directory": "./dist" }, "env": { "staging": { "assets": { "not_found_handling": "404-page" } }, "production": { "assets": { "not_found_handling": "single-page-application" } } } } ``` Deploy with: `wrangler deploy --env staging` ================================================ FILE: skills/.curated/cloudflare-deploy/references/static-assets/gotchas.md ================================================ ## Best Practices ### 1. Use Selective Worker-First Routing Instead of `run_worker_first = true`, use array patterns: ```jsonc { "assets": { "run_worker_first": [ "/api/*", // API routes "/admin/*", // Admin area "!/admin/assets/*" // Except admin assets ] } } ``` **Benefits:** - Reduces Worker invocations - Lowers costs - Improves asset delivery performance ### 2. Leverage Navigation Request Optimization For SPAs, use `compatibility_date = "2025-04-01"` or later: ```jsonc { "compatibility_date": "2025-04-01", "assets": { "not_found_handling": "single-page-application" } } ``` Navigation requests skip Worker invocation, reducing costs. ### 3. Type Safety with Bindings Always type your environment: ```typescript interface Env { ASSETS: Fetcher; } ``` ## Common Errors ### "Asset not found" **Cause:** Asset not in assets directory, wrong path, or assets not deployed **Solution:** Verify asset exists, check path case-sensitivity, redeploy if needed ### "Worker not invoked for asset" **Cause:** Asset served directly, `run_worker_first` not configured **Solution:** Configure `run_worker_first` patterns to include asset routes (see configuration.md:66-106) ### "429 Too Many Requests on free tier" **Cause:** `run_worker_first` patterns invoke Worker for many requests, hitting free tier limits (100k req/day) **Solution:** Use more selective patterns with negative exclusions, or upgrade to paid plan ### "Smart Placement increases latency" **Cause:** `run_worker_first=true` + Smart Placement routes all requests through single smart-placed location **Solution:** Use selective patterns (array syntax) or disable Smart Placement for asset-heavy apps ### "CF-Cache-Status header unreliable" **Cause:** Header is probabilistically added for privacy reasons **Solution:** Don't rely on `CF-Cache-Status` for critical routing logic. Use other signals (ETag, age). ### "JWT expired during deployment" **Cause:** Large asset deployments exceed JWT token lifetime **Solution:** Update to Wrangler 4.34.0+ (automatic token refresh), or reduce asset count ### "Cannot use 'assets' with 'site'" **Cause:** Legacy `site` config conflicts with new `assets` config **Solution:** Migrate from `site` to `assets` (see configuration.md). Remove `site` key from wrangler.jsonc. ### "Assets not updating after deployment" **Cause:** Browser or CDN cache serving old assets **Solution:** - Hard refresh browser (Cmd+Shift+R / Ctrl+F5) - Use cache-busting (hashed filenames) - Verify deployment completed: `wrangler tail` ## Limits | Resource/Limit | Free | Paid | Notes | |----------------|------|------|-------| | Max asset size | 25 MiB | 25 MiB | Per file | | Total assets | 20,000 | **100,000** | Requires Wrangler 4.34.0+ (Sep 2025) | | Worker invocations | 100k/day | 10M/month | Optimize with `run_worker_first` patterns | | Asset storage | Unlimited | Unlimited | Included | ### Version Requirements | Feature | Minimum Wrangler Version | |---------|--------------------------| | 100k file limit (paid) | 4.34.0 | | Vite plugin | 4.0.0 + @cloudflare/vite-plugin 1.0.0 | | Navigation optimization | 4.0.0 + compatibility_date: "2025-04-01" | ## Performance Tips ### 1. Use Hashed Filenames Enable long-term caching with content-hashed filenames: ``` app.a3b2c1d4.js styles.e5f6g7h8.css ``` Most bundlers (Vite, Webpack, Parcel) do this automatically. ### 2. Minimize Worker Invocations Serve assets directly when possible: ```jsonc { "assets": { // Only invoke Worker for dynamic routes "run_worker_first": ["/api/*", "/auth/*"] } } ``` ### 3. Leverage Browser Cache Set appropriate `Cache-Control` headers: ```typescript // Versioned assets 'Cache-Control': 'public, max-age=31536000, immutable' // HTML (revalidate often) 'Cache-Control': 'public, max-age=0, must-revalidate' ``` See patterns.md:169-189 for implementation. ### 4. Use .assetsignore Reduce upload time by excluding unnecessary files: ``` *.map *.md .DS_Store node_modules/ ``` See configuration.md:107-126 for details. ================================================ FILE: skills/.curated/cloudflare-deploy/references/static-assets/patterns.md ================================================ ### Common Patterns **1. Forward request to assets:** ```typescript export default { async fetch(request: Request, env: Env): Promise { return env.ASSETS.fetch(request); } }; ``` **2. Fetch specific asset by path:** ```typescript const response = await env.ASSETS.fetch("https://assets.local/logo.png"); ``` **3. Modify request before fetching asset:** ```typescript const url = new URL(request.url); url.pathname = "/index.html"; return env.ASSETS.fetch(new Request(url, request)); ``` **4. Transform asset response:** ```typescript const response = await env.ASSETS.fetch(request); const modifiedResponse = new Response(response.body, response); modifiedResponse.headers.set("X-Custom-Header", "value"); modifiedResponse.headers.set("Cache-Control", "public, max-age=3600"); return modifiedResponse; ``` **5. Conditional asset serving:** ```typescript export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname === '/') { return env.ASSETS.fetch('/index.html'); } return env.ASSETS.fetch(request); } }; ``` **6. SPA with API routes:** Most common full-stack pattern - static SPA with backend API: ```typescript export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname.startsWith('/api/')) { return handleAPI(request, env); } return env.ASSETS.fetch(request); } }; async function handleAPI(request: Request, env: Env): Promise { return new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' } }); } ``` **Config:** Set `run_worker_first: ["/api/*"]` (see configuration.md:66-106) **7. Auth gating for protected assets:** ```typescript export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname.startsWith('/admin/')) { const session = await validateSession(request, env); if (!session) { return Response.redirect('/login', 302); } } return env.ASSETS.fetch(request); } }; ``` **Config:** Set `run_worker_first: ["/admin/*"]` **8. Custom headers for security:** ```typescript export default { async fetch(request: Request, env: Env): Promise { const response = await env.ASSETS.fetch(request); const secureResponse = new Response(response.body, response); secureResponse.headers.set('X-Frame-Options', 'DENY'); secureResponse.headers.set('X-Content-Type-Options', 'nosniff'); secureResponse.headers.set('Content-Security-Policy', "default-src 'self'"); return secureResponse; } }; ``` **9. A/B testing via cookies:** ```typescript export default { async fetch(request: Request, env: Env): Promise { const cookies = request.headers.get('Cookie') || ''; const variant = cookies.includes('variant=b') ? 'b' : 'a'; const url = new URL(request.url); if (url.pathname === '/') { return env.ASSETS.fetch(`/index-${variant}.html`); } return env.ASSETS.fetch(request); } }; ``` **10. Locale-based routing:** ```typescript export default { async fetch(request: Request, env: Env): Promise { const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en'; const url = new URL(request.url); if (url.pathname === '/') { return env.ASSETS.fetch(`/${locale}/index.html`); } if (!url.pathname.startsWith(`/${locale}/`)) { url.pathname = `/${locale}${url.pathname}`; } return env.ASSETS.fetch(url); } }; ``` **11. OAuth callback handling:** ```typescript export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname === '/auth/callback') { const code = url.searchParams.get('code'); if (code) { const session = await exchangeCode(code, env); return new Response(null, { status: 302, headers: { 'Location': '/', 'Set-Cookie': `session=${session}; HttpOnly; Secure; SameSite=Lax` } }); } } return env.ASSETS.fetch(request); } }; ``` **Config:** Set `run_worker_first: ["/auth/*"]` **12. Cache control override:** ```typescript export default { async fetch(request: Request, env: Env): Promise { const response = await env.ASSETS.fetch(request); const url = new URL(request.url); // Immutable assets (hashed filenames) if (/\.[a-f0-9]{8,}\.(js|css|png|jpg)$/.test(url.pathname)) { return new Response(response.body, { ...response, headers: { ...Object.fromEntries(response.headers), 'Cache-Control': 'public, max-age=31536000, immutable' } }); } return response; } }; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/stream/README.md ================================================ # Cloudflare Stream Serverless live and on-demand video streaming platform with one API. ## Overview Cloudflare Stream provides video upload, storage, encoding, and delivery without managing infrastructure. Runs on Cloudflare's global network. ### Key Features - **On-demand video**: Upload, encode, store, deliver - **Live streaming**: RTMPS/SRT ingestion with ABR - **Direct creator uploads**: End users upload without API keys - **Signed URLs**: Token-based access control - **Analytics**: Server-side metrics via GraphQL - **Webhooks**: Processing notifications - **Captions**: Upload or AI-generate subtitles - **Watermarks**: Apply branding to videos - **Downloads**: Enable MP4 offline viewing ## Core Concepts ### Video Upload Methods 1. **API Upload (TUS protocol)**: Direct server upload 2. **Upload from URL**: Import from external source 3. **Direct Creator Uploads**: User-generated content (recommended) ### Playback Options 1. **Stream Player (iframe)**: Built-in, optimized player 2. **Custom Player (HLS/DASH)**: Video.js, HLS.js integration 3. **Thumbnails**: Static or animated previews ### Access Control - **Public**: No restrictions - **requireSignedURLs**: Token-based access - **allowedOrigins**: Domain restrictions - **Access Rules**: Geo/IP restrictions in tokens ### Live Streaming - RTMPS/SRT ingest from OBS, FFmpeg - Automatic recording to on-demand - Simulcast to YouTube, Twitch, etc. - WebRTC support for browser streaming ## Quick Start **Upload video via API** ```bash curl -X POST \ "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/copy" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/video.mp4"}' ``` **Embed player** ```html ``` **Create live input** ```bash curl -X POST \ "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/live_inputs" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"recording": {"mode": "automatic"}}' ``` ## Limits - Max file size: 30 GB - Max frame rate: 60 fps (recommended) - Supported formats: MP4, MKV, MOV, AVI, FLV, MPEG-2 TS/PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime ## Pricing - $5/1000 min stored - $1/1000 min delivered ## Resources - Dashboard: https://dash.cloudflare.com/?to=/:account/stream - API Docs: https://developers.cloudflare.com/api/resources/stream/ - Stream Docs: https://developers.cloudflare.com/stream/ ## Reading Order | Order | File | Purpose | When to Use | |-------|------|---------|-------------| | 1 | [configuration.md](./configuration.md) | Setup SDKs, env vars, signing keys | Starting new project | | 2 | [api.md](./api.md) | On-demand video APIs | Implementing uploads/playback | | 3 | [api-live.md](./api-live.md) | Live streaming APIs | Building live streaming | | 4 | [patterns.md](./patterns.md) | Full-stack flows, TUS, JWT signing | Implementing workflows | | 5 | [gotchas.md](./gotchas.md) | Errors, limits, troubleshooting | Debugging issues | ## In This Reference - [configuration.md](./configuration.md) - Setup, environment variables, wrangler config - [api.md](./api.md) - On-demand video upload, playback, management APIs - [api-live.md](./api-live.md) - Live streaming (RTMPS/SRT/WebRTC), simulcast - [patterns.md](./patterns.md) - Full-stack flows, state management, best practices - [gotchas.md](./gotchas.md) - Error codes, troubleshooting, limits ## See Also - [workers](../workers/) - Deploy Stream APIs in Workers - [pages](../pages/) - Integrate Stream with Pages - [workers-ai](../workers-ai/) - AI-generate captions ================================================ FILE: skills/.curated/cloudflare-deploy/references/stream/api-live.md ================================================ # Stream Live Streaming API Live input creation, status checking, simulcast, and WebRTC streaming. ## Create Live Input ### Using Cloudflare SDK ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: env.CF_API_TOKEN }); const liveInput = await client.stream.liveInputs.create({ account_id: env.CF_ACCOUNT_ID, recording: { mode: 'automatic', timeoutSeconds: 30 }, deleteRecordingAfterDays: 30 }); // Returns: { uid, rtmps, srt, webRTC } ``` ### Raw fetch API ```typescript async function createLiveInput(accountId: string, apiToken: string) { const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ recording: { mode: 'automatic', timeoutSeconds: 30 }, deleteRecordingAfterDays: 30 }) } ); const { result } = await response.json(); return { uid: result.uid, rtmps: { url: result.rtmps.url, streamKey: result.rtmps.streamKey }, srt: { url: result.srt.url, streamId: result.srt.streamId, passphrase: result.srt.passphrase }, webRTC: result.webRTC }; } ``` ## Check Live Status ```typescript async function getLiveStatus(accountId: string, liveInputId: string, apiToken: string) { const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs/${liveInputId}`, { headers: { 'Authorization': `Bearer ${apiToken}` } } ); const { result } = await response.json(); return { isLive: result.status?.current?.state === 'connected', recording: result.recording, status: result.status }; } ``` ## Simulcast (Live Outputs) ### Create Output ```typescript async function createLiveOutput( accountId: string, liveInputId: string, apiToken: string, outputUrl: string, streamKey: string ) { return fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs/${liveInputId}/outputs`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: `${outputUrl}/${streamKey}`, enabled: true, streamKey // For platforms like YouTube, Twitch }) } ).then(r => r.json()); } ``` ### Example: Simulcast to YouTube + Twitch ```typescript const liveInput = await createLiveInput(accountId, apiToken); // Add YouTube output await createLiveOutput( accountId, liveInput.uid, apiToken, 'rtmp://a.rtmp.youtube.com/live2', 'your-youtube-stream-key' ); // Add Twitch output await createLiveOutput( accountId, liveInput.uid, apiToken, 'rtmp://live.twitch.tv/app', 'your-twitch-stream-key' ); ``` ## WebRTC Streaming (WHIP/WHEP) ### Browser to Stream (WHIP) ```typescript async function startWebRTCBroadcast(liveInputId: string) { const pc = new RTCPeerConnection(); // Add local media tracks const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); stream.getTracks().forEach(track => pc.addTrack(track, stream)); // Create offer const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // Send to Stream via WHIP const response = await fetch( `https://customer-.cloudflarestream.com/${liveInputId}/webRTC/publish`, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: offer.sdp } ); const answer = await response.text(); await pc.setRemoteDescription({ type: 'answer', sdp: answer }); } ``` ### Stream to Browser (WHEP) ```typescript async function playWebRTCStream(videoId: string) { const pc = new RTCPeerConnection(); pc.addTransceiver('video', { direction: 'recvonly' }); pc.addTransceiver('audio', { direction: 'recvonly' }); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); const response = await fetch( `https://customer-.cloudflarestream.com/${videoId}/webRTC/play`, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: offer.sdp } ); const answer = await response.text(); await pc.setRemoteDescription({ type: 'answer', sdp: answer }); return pc; } ``` ## Recording Settings | Mode | Behavior | |------|----------| | `automatic` | Record all live streams | | `off` | No recording | | `timeoutSeconds` | Stop recording after N seconds of inactivity | ```typescript const recordingConfig = { mode: 'automatic', timeoutSeconds: 30, // Auto-stop 30s after stream ends requireSignedURLs: true, // Require token for VOD playback allowedOrigins: ['https://yourdomain.com'] }; ``` ## In This Reference - [README.md](./README.md) - Overview and quick start - [api.md](./api.md) - On-demand video APIs - [configuration.md](./configuration.md) - Setup and config - [patterns.md](./patterns.md) - Full-stack flows, best practices - [gotchas.md](./gotchas.md) - Error codes, troubleshooting ## See Also - [workers](../workers/) - Deploy live APIs in Workers ================================================ FILE: skills/.curated/cloudflare-deploy/references/stream/api.md ================================================ # Stream API Reference Upload, playback, live streaming, and management APIs. ## Upload APIs ### Direct Creator Upload (Recommended) **Backend: Create upload URL (SDK)** ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: env.CF_API_TOKEN }); const uploadData = await client.stream.directUpload.create({ account_id: env.CF_ACCOUNT_ID, maxDurationSeconds: 3600, requireSignedURLs: true, meta: { creator: 'user-123' } }); // Returns: { uploadURL: string, uid: string } ``` **Frontend: Upload file** ```typescript async function uploadVideo(file: File, uploadURL: string) { const formData = new FormData(); formData.append('file', file); return fetch(uploadURL, { method: 'POST', body: formData }).then(r => r.json()); } ``` ### Upload from URL ```typescript const video = await client.stream.copy.create({ account_id: env.CF_ACCOUNT_ID, url: 'https://example.com/video.mp4', meta: { name: 'My Video' }, requireSignedURLs: false }); ``` ## Playback APIs ### Embed Player (iframe) ```html ``` ### HLS/DASH Manifest URLs ```typescript // HLS const hlsUrl = `https://customer-.cloudflarestream.com/${videoId}/manifest/video.m3u8`; // DASH const dashUrl = `https://customer-.cloudflarestream.com/${videoId}/manifest/video.mpd`; ``` ### Thumbnails ```typescript // At specific time (seconds) const thumb = `https://customer-.cloudflarestream.com/${videoId}/thumbnails/thumbnail.jpg?time=10s`; // By percentage const thumbPct = `https://customer-.cloudflarestream.com/${videoId}/thumbnails/thumbnail.jpg?time=50%`; // Animated GIF const gif = `https://customer-.cloudflarestream.com/${videoId}/thumbnails/thumbnail.gif`; ``` ## Signed URLs ```typescript // Low volume (<1k/day): Use API async function getSignedToken(accountId: string, videoId: string, apiToken: string) { const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/token`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 3600, accessRules: [{ type: 'ip.geoip.country', action: 'allow', country: ['US'] }] }) } ); return (await response.json()).result.token; } // High volume: Self-sign with RS256 JWT (see "Self-Sign JWT" in patterns.md) ``` ## Captions & Clips ### Upload Captions ```typescript async function uploadCaption( accountId: string, videoId: string, apiToken: string, language: string, captionFile: File ) { const formData = new FormData(); formData.append('file', captionFile); return fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/captions/${language}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${apiToken}` }, body: formData } ).then(r => r.json()); } ``` ### Generate AI Captions ```typescript // TODO: Requires Workers AI integration - see workers-ai reference async function generateAICaptions(accountId: string, videoId: string, apiToken: string) { return fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/captions/generate`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ language: 'en' }) } ).then(r => r.json()); } ``` ### Clip Video ```typescript async function clipVideo( accountId: string, videoId: string, apiToken: string, startTime: number, endTime: number ) { return fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/clip`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ clippedFromVideoUID: videoId, startTimeSeconds: startTime, endTimeSeconds: endTime }) } ).then(r => r.json()); } ``` ## Video Management ```typescript // List videos const videos = await client.stream.videos.list({ account_id: env.CF_ACCOUNT_ID, search: 'keyword' // optional }); // Get video details const video = await client.stream.videos.get(videoId, { account_id: env.CF_ACCOUNT_ID }); // Update video await client.stream.videos.update(videoId, { account_id: env.CF_ACCOUNT_ID, meta: { title: 'New Title' }, requireSignedURLs: true }); // Delete video await client.stream.videos.delete(videoId, { account_id: env.CF_ACCOUNT_ID }); ``` ## In This Reference - [README.md](./README.md) - Overview and quick start - [configuration.md](./configuration.md) - Setup and config - [api-live.md](./api-live.md) - Live streaming APIs (RTMPS/SRT/WebRTC) - [patterns.md](./patterns.md) - Full-stack flows, best practices - [gotchas.md](./gotchas.md) - Error codes, troubleshooting ## See Also - [workers](../workers/) - Deploy Stream APIs in Workers ================================================ FILE: skills/.curated/cloudflare-deploy/references/stream/configuration.md ================================================ # Stream Configuration Setup, environment variables, and wrangler configuration. ## Installation ```bash # Official Cloudflare SDK (Node.js, Workers, Pages) npm install cloudflare # React component library npm install @cloudflare/stream-react # TUS resumable uploads (large files) npm install tus-js-client ``` ## Environment Variables ```bash # Required CF_ACCOUNT_ID=your-account-id CF_API_TOKEN=your-api-token # For signed URLs (high volume) STREAM_KEY_ID=your-key-id STREAM_JWK=base64-encoded-jwk # For webhooks WEBHOOK_SECRET=your-webhook-secret # Customer subdomain (from dashboard) STREAM_CUSTOMER_CODE=your-customer-code ``` ## Wrangler Configuration ```jsonc { "name": "stream-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date for new projects "vars": { "CF_ACCOUNT_ID": "your-account-id" } // Store secrets: wrangler secret put CF_API_TOKEN // wrangler secret put STREAM_KEY_ID // wrangler secret put STREAM_JWK // wrangler secret put WEBHOOK_SECRET } ``` ## Signing Keys (High Volume) Create once for self-signing tokens (thousands of daily users). **Create key** ```bash curl -X POST \ "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/keys" \ -H "Authorization: Bearer " # Save `id` and `jwk` (base64) from response ``` **Store in secrets** ```bash wrangler secret put STREAM_KEY_ID wrangler secret put STREAM_JWK ``` ## Webhooks **Setup webhook URL** ```bash curl -X PUT \ "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/webhook" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"notificationUrl": "https://your-worker.workers.dev/webhook"}' # Save the returned `secret` for signature verification ``` **Store secret** ```bash wrangler secret put WEBHOOK_SECRET ``` ## Direct Upload / Live / Watermark Config ```typescript // Direct upload const uploadConfig = { maxDurationSeconds: 3600, expiry: new Date(Date.now() + 3600000).toISOString(), requireSignedURLs: true, allowedOrigins: ['https://yourdomain.com'], meta: { creator: 'user-123' } }; // Live input const liveConfig = { recording: { mode: 'automatic', timeoutSeconds: 30 }, deleteRecordingAfterDays: 30 }; // Watermark const watermark = { name: 'Logo', opacity: 0.7, padding: 20, position: 'lowerRight', scale: 0.15 }; ``` ## Access Rules & Player Config ```typescript // Access rules: allow US/CA, block CN/RU, or IP allowlist const geoRestrict = [ { type: 'ip.geoip.country', action: 'allow', country: ['US', 'CA'] }, { type: 'any', action: 'block' } ]; // Player params for iframe const playerParams = new URLSearchParams({ autoplay: 'true', muted: 'true', preload: 'auto', defaultTextTrack: 'en' }); ``` ## In This Reference - [README.md](./README.md) - Overview and quick start - [api.md](./api.md) - On-demand video APIs - [api-live.md](./api-live.md) - Live streaming APIs - [patterns.md](./patterns.md) - Full-stack flows, best practices - [gotchas.md](./gotchas.md) - Error codes, troubleshooting ## See Also - [wrangler](../wrangler/) - Wrangler CLI and configuration - [workers](../workers/) - Deploy Stream APIs in Workers ================================================ FILE: skills/.curated/cloudflare-deploy/references/stream/gotchas.md ================================================ # Stream Gotchas ## Common Errors ### "ERR_NON_VIDEO" **Cause:** Uploaded file is not a valid video format **Solution:** Ensure file is in supported format (MP4, MKV, MOV, AVI, FLV, MPEG-2 TS/PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime) ### "ERR_DURATION_EXCEED_CONSTRAINT" **Cause:** Video duration exceeds `maxDurationSeconds` constraint **Solution:** Increase `maxDurationSeconds` in direct upload config or trim video before upload ### "ERR_FETCH_ORIGIN_ERROR" **Cause:** Failed to download video from URL (upload from URL) **Solution:** Ensure URL is publicly accessible, uses HTTPS, and video file is available ### "ERR_MALFORMED_VIDEO" **Cause:** Video file is corrupted or improperly encoded **Solution:** Re-encode video using FFmpeg or check source file integrity ### "ERR_DURATION_TOO_SHORT" **Cause:** Video must be at least 0.1 seconds long **Solution:** Ensure video has valid duration (not a single frame) ## Troubleshooting ### Video stuck in "inprogress" state - **Cause**: Processing large/complex video - **Solution**: Wait up to 5 minutes for processing; use webhooks instead of polling ### Signed URL returns 403 - **Cause**: Token expired or invalid signature - **Solution**: Check expiration timestamp, verify JWK is correct, ensure clock sync ### Live stream not connecting - **Cause**: Invalid RTMPS URL or stream key - **Solution**: Use exact URL/key from API, ensure firewall allows outbound 443 ### Webhook signature verification fails - **Cause**: Incorrect secret or timestamp window - **Solution**: Use exact secret from webhook setup, allow 5-minute timestamp drift ### Video uploads but isn't visible - **Cause**: `requireSignedURLs` enabled without providing token - **Solution**: Generate signed token or set `requireSignedURLs: false` for public videos ### Player shows infinite loading - **Cause**: CORS issue with allowedOrigins - **Solution**: Add your domain to `allowedOrigins` array ## Limits | Resource | Limit | |----------|-------| | Max file size | 30 GB | | Max frame rate | 60 fps (recommended) | | Max duration per direct upload | Configurable via `maxDurationSeconds` | | Token generation (API endpoint) | 1,000/day recommended (use signing keys for higher) | | Live input outputs (simulcast) | 5 per live input | | Webhook retry attempts | 5 (exponential backoff) | | Webhook timeout | 30 seconds | | Caption file size | 5 MB | | Watermark image size | 2 MB | | Metadata keys per video | Unlimited | | Search results per page | Max 1,000 | ## Performance Issues ### Upload is slow - **Cause**: Large file size or network constraints - **Solution**: Use TUS resumable upload, compress video before upload, check bandwidth ### Playback buffering - **Cause**: Network congestion or low bandwidth - **Solution**: Use ABR (adaptive bitrate) with HLS/DASH, reduce max bitrate ### High processing time - **Cause**: Complex video codec, high resolution - **Solution**: Pre-encode with H.264 (most efficient), reduce resolution ## Type Safety ```typescript // Error response type interface StreamError { success: false; errors: Array<{ code: number; message: string; }>; } // Handle errors async function uploadWithErrorHandling(url: string, file: File) { const formData = new FormData(); formData.append('file', file); const response = await fetch(url, { method: 'POST', body: formData }); const result = await response.json(); if (!result.success) { throw new Error(result.errors[0]?.message || 'Upload failed'); } return result; } ``` ## Security Gotchas 1. **Never expose API token in frontend** - Use direct creator uploads 2. **Always verify webhook signatures** - Prevent spoofed notifications 3. **Set appropriate token expiration** - Short-lived for security 4. **Use requireSignedURLs for private content** - Prevent unauthorized access 5. **Whitelist allowedOrigins** - Prevent hotlinking/embedding on unauthorized sites ## In This Reference - [README.md](./README.md) - Overview and quick start - [configuration.md](./configuration.md) - Setup and config - [api.md](./api.md) - On-demand video APIs - [api-live.md](./api-live.md) - Live streaming APIs - [patterns.md](./patterns.md) - Full-stack flows, best practices ## See Also - [workers](../workers/) - Deploy Stream APIs securely ================================================ FILE: skills/.curated/cloudflare-deploy/references/stream/patterns.md ================================================ # Stream Patterns Common workflows, full-stack flows, and best practices. ## React Stream Player `npm install @cloudflare/stream-react` ```tsx import { Stream } from '@cloudflare/stream-react'; export function VideoPlayer({ videoId, token }: { videoId: string; token?: string }) { return ; } ``` ## Full-Stack Upload Flow **Backend API (Workers/Pages)** ```typescript import Cloudflare from 'cloudflare'; export default { async fetch(request: Request, env: Env): Promise { const { videoName } = await request.json(); const client = new Cloudflare({ apiToken: env.CF_API_TOKEN }); const { uploadURL, uid } = await client.stream.directUpload.create({ account_id: env.CF_ACCOUNT_ID, maxDurationSeconds: 3600, requireSignedURLs: true, meta: { name: videoName } }); return Response.json({ uploadURL, uid }); } }; ``` **Frontend component** ```tsx import { useState } from 'react'; export function VideoUploader() { const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState(0); async function handleUpload(file: File) { setUploading(true); const { uploadURL, uid } = await fetch('/api/upload-url', { method: 'POST', body: JSON.stringify({ videoName: file.name }) }).then(r => r.json()); const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => setProgress((e.loaded / e.total) * 100); xhr.onload = () => { setUploading(false); window.location.href = `/videos/${uid}`; }; xhr.open('POST', uploadURL); const formData = new FormData(); formData.append('file', file); xhr.send(formData); } return (
e.target.files?.[0] && handleUpload(e.target.files[0])} disabled={uploading} /> {uploading && }
); } ``` ## TUS Resumable Upload For large files (>500MB). `npm install tus-js-client` ```typescript import * as tus from 'tus-js-client'; async function uploadWithTUS(file: File, uploadURL: string, onProgress?: (pct: number) => void) { return new Promise((resolve, reject) => { const upload = new tus.Upload(file, { endpoint: uploadURL, retryDelays: [0, 3000, 5000, 10000, 20000], chunkSize: 50 * 1024 * 1024, metadata: { filename: file.name, filetype: file.type }, onError: reject, onProgress: (up, total) => onProgress?.((up / total) * 100), onSuccess: () => resolve(upload.url?.split('/').pop() || '') }); upload.start(); }); } ``` ## Video State Polling ```typescript async function waitForVideoReady(client: Cloudflare, accountId: string, videoId: string) { for (let i = 0; i < 60; i++) { const video = await client.stream.videos.get(videoId, { account_id: accountId }); if (video.readyToStream || video.status.state === 'error') return video; await new Promise(resolve => setTimeout(resolve, 5000)); } throw new Error('Video processing timeout'); } ``` ## Webhook Handler ```typescript export default { async fetch(request: Request, env: Env): Promise { const signature = request.headers.get('Webhook-Signature'); const body = await request.text(); if (!signature || !await verifyWebhook(signature, body, env.WEBHOOK_SECRET)) { return new Response('Unauthorized', { status: 401 }); } const payload = JSON.parse(body); if (payload.readyToStream) console.log(`Video ${payload.uid} ready`); return new Response('OK'); } }; async function verifyWebhook(sig: string, body: string, secret: string): Promise { const parts = Object.fromEntries(sig.split(',').map(p => p.split('='))); const timestamp = parseInt(parts.time || '0', 10); if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const computed = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${timestamp}.${body}`)); const hex = Array.from(new Uint8Array(computed), b => b.toString(16).padStart(2, '0')).join(''); return hex === parts.sig1; } ``` ## Self-Sign JWT (High Volume Tokens) For >1k tokens/day. Prerequisites: Create signing key (see configuration.md). ```typescript async function selfSignToken(keyId: string, jwkBase64: string, videoId: string, expiresIn = 3600) { const key = await crypto.subtle.importKey( 'jwk', JSON.parse(atob(jwkBase64)), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign'] ); const now = Math.floor(Date.now() / 1000); const header = btoa(JSON.stringify({ alg: 'RS256', kid: keyId })).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); const payload = btoa(JSON.stringify({ sub: videoId, kid: keyId, exp: now + expiresIn, nbf: now })) .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); const message = `${header}.${payload}`; const sig = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, new TextEncoder().encode(message)); const b64Sig = btoa(String.fromCharCode(...new Uint8Array(sig))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return `${message}.${b64Sig}`; } // With access rules (geo-restriction) const payloadWithRules = { sub: videoId, kid: keyId, exp: now + 3600, nbf: now, accessRules: [{ type: 'ip.geoip.country', action: 'allow', country: ['US'] }] }; ``` ## Best Practices - **Use Direct Creator Uploads** - Avoid proxying through servers - **Enable requireSignedURLs** - Control private content access - **Self-sign tokens at scale** - Use signing keys for >1k/day - **Set allowedOrigins** - Prevent hotlinking - **Use webhooks over polling** - Efficient status updates - **Set maxDurationSeconds** - Prevent abuse - **Enable live recordings** - Auto VOD after stream ## In This Reference - [README.md](./README.md) - Overview and quick start - [configuration.md](./configuration.md) - Setup and config - [api.md](./api.md) - On-demand video APIs - [api-live.md](./api-live.md) - Live streaming APIs - [gotchas.md](./gotchas.md) - Error codes, troubleshooting ## See Also - [workers](../workers/) - Deploy Stream APIs in Workers - [pages](../pages/) - Integrate Stream with Pages ================================================ FILE: skills/.curated/cloudflare-deploy/references/tail-workers/README.md ================================================ # Cloudflare Tail Workers Specialized Workers that consume execution events from producer Workers for logging, debugging, analytics, and observability. ## When to Use This Reference - Implementing observability/logging for Cloudflare Workers - Processing Worker execution events, logs, exceptions - Building custom analytics or error tracking - Configuring real-time event streaming - Working with tail handlers or tail consumers ## Core Concepts ### What Are Tail Workers? Tail Workers automatically process events from producer Workers (the Workers being monitored). They receive: - HTTP request/response info - Console logs (`console.log/error/warn/debug`) - Uncaught exceptions - Execution outcomes (`ok`, `exception`, `exceededCpu`, etc.) - Diagnostic channel events **Key characteristics:** - Invoked AFTER producer finishes executing - Capture entire request lifecycle including Service Bindings and Dynamic Dispatch sub-requests - Billed by CPU time, not request count - Available on Workers Paid and Enterprise tiers ### Alternative: OpenTelemetry Export **Before using Tail Workers, consider OpenTelemetry:** For batch exports to observability tools (Sentry, Grafana, Honeycomb): - OTEL export sends logs/traces in batches (more efficient) - Built-in integrations with popular platforms - Lower overhead than Tail Workers - **Use Tail Workers only for custom real-time processing** ## Decision Tree ``` Need observability for Workers? ├─ Batch export to known tools (Sentry/Grafana/Honeycomb)? │ └─ Use OpenTelemetry export (not Tail Workers) ├─ Custom real-time processing needed? │ ├─ Aggregated metrics? │ │ └─ Use Tail Worker + Analytics Engine │ ├─ Error tracking? │ │ └─ Use Tail Worker + external service │ ├─ Custom logging/debugging? │ │ └─ Use Tail Worker + KV/HTTP endpoint │ └─ Complex event processing? │ └─ Use Tail Worker + Durable Objects └─ Quick debugging? └─ Use `wrangler tail` (different from Tail Workers) ``` ## Reading Order 1. **[configuration.md](configuration.md)** - Set up Tail Workers 2. **[api.md](api.md)** - Handler signature, types, redaction 3. **[patterns.md](patterns.md)** - Common use cases and integrations 4. **[gotchas.md](gotchas.md)** - Pitfalls and debugging tips ## Quick Example ```typescript export default { async tail(events, env, ctx) { // Process events from producer Worker ctx.waitUntil( fetch(env.LOG_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(events), }) ); } }; ``` ## Related Skills - **observability** - General Workers observability patterns, OTEL export - **analytics-engine** - Aggregated metrics storage for tail event data - **durable-objects** - Stateful event processing, batching tail events - **logpush** - Alternative for batch log export (non-real-time) - **workers-for-platforms** - Dynamic dispatch with tail consumers ================================================ FILE: skills/.curated/cloudflare-deploy/references/tail-workers/api.md ================================================ # Tail Workers API Reference ## Handler Signature ```typescript export default { async tail( events: TraceItem[], env: Env, ctx: ExecutionContext ): Promise { // Process events } } satisfies ExportedHandler; ``` **Parameters:** - `events`: Array of `TraceItem` objects (one per producer invocation) - `env`: Bindings (KV, D1, R2, env vars, etc.) - `ctx`: Context with `waitUntil()` for async work **CRITICAL:** Tail handlers don't return values. Use `ctx.waitUntil()` for async operations. ## TraceItem Type ```typescript interface TraceItem { scriptName: string; // Producer Worker name eventTimestamp: number; // Epoch milliseconds outcome: 'ok' | 'exception' | 'exceededCpu' | 'exceededMemory' | 'canceled' | 'scriptNotFound' | 'responseStreamDisconnected' | 'unknown'; event?: { request?: { url: string; // Redacted by default method: string; headers: Record; // Sensitive headers redacted cf?: IncomingRequestCfProperties; getUnredacted(): TraceRequest; // Bypass redaction (use carefully) }; response?: { status: number; }; }; logs: Array<{ timestamp: number; // Epoch milliseconds level: 'debug' | 'info' | 'log' | 'warn' | 'error'; message: unknown[]; // Args passed to console function }>; exceptions: Array<{ timestamp: number; // Epoch milliseconds name: string; // Error type (Error, TypeError, etc.) message: string; // Error description }>; diagnosticsChannelEvents: Array<{ channel: string; message: unknown; timestamp: number; // Epoch milliseconds }>; } ``` **Note:** Official SDK uses `TraceItem`, not `TailItem`. Use `@cloudflare/workers-types` for accurate types. ## Timestamp Handling All timestamps are **epoch milliseconds**, not seconds: ```typescript // ✅ CORRECT - use directly with Date const date = new Date(event.eventTimestamp); // ❌ WRONG - don't multiply by 1000 const date = new Date(event.eventTimestamp * 1000); ``` ## Automatic Redaction By default, sensitive data is redacted from `TraceRequest`: ### Header Redaction Headers containing these substrings (case-insensitive): - `auth`, `key`, `secret`, `token`, `jwt` - `cookie`, `set-cookie` Redacted values show as `"REDACTED"`. ### URL Redaction - **Hex IDs:** 32+ hex digits → `"REDACTED"` - **Base-64 IDs:** 21+ chars with 2+ upper, 2+ lower, 2+ digits → `"REDACTED"` ## Bypassing Redaction ```typescript export default { async tail(events, env, ctx) { for (const event of events) { // ⚠️ Use with extreme caution const unredacted = event.event?.request?.getUnredacted(); // unredacted.url and unredacted.headers contain raw values } } }; ``` **Best practices:** - Only call `getUnredacted()` when absolutely necessary - Never log unredacted sensitive data - Implement additional filtering before external transmission - Use environment variables for API keys, never hardcode ## Type-Safe Handler ```typescript interface Env { LOGS_KV: KVNamespace; ANALYTICS: AnalyticsEngineDataset; LOG_ENDPOINT: string; API_TOKEN: string; } export default { async tail( events: TraceItem[], env: Env, ctx: ExecutionContext ): Promise { const payload = events.map(event => ({ script: event.scriptName, timestamp: event.eventTimestamp, outcome: event.outcome, url: event.event?.request?.url, status: event.event?.response?.status, })); ctx.waitUntil( fetch(env.LOG_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) ); } } satisfies ExportedHandler; ``` ## Outcome vs HTTP Status **IMPORTANT:** `outcome` is script execution status, NOT HTTP status. - Worker returns 500 → `outcome='ok'` if script completed successfully - Uncaught exception → `outcome='exception'` regardless of HTTP status - CPU limit exceeded → `outcome='exceededCpu'` ```typescript // ✅ Check outcome for script execution status if (event.outcome === 'exception') { // Script threw uncaught exception } // ✅ Check HTTP status separately if (event.event?.response?.status === 500) { // HTTP 500 returned (script may have handled error) } ``` ## Serialization Considerations `log.message` is `unknown[]` and may contain non-serializable objects: ```typescript // ❌ May fail with circular references or BigInt JSON.stringify(events); // ✅ Safe serialization const safePayload = events.map(event => ({ ...event, logs: event.logs.map(log => ({ ...log, message: log.message.map(m => { try { return JSON.parse(JSON.stringify(m)); } catch { return String(m); } }) })) })); ``` **Common serialization issues:** - Circular references in logged objects - `BigInt` values (not JSON-serializable) - Functions or symbols in console.log arguments - Large objects exceeding body size limits ================================================ FILE: skills/.curated/cloudflare-deploy/references/tail-workers/configuration.md ================================================ # Tail Workers Configuration ## Setup Steps ### 1. Create Tail Worker Create a Worker with a `tail()` handler: ```typescript export default { async tail(events, env, ctx) { // Process events from producer Worker ctx.waitUntil( fetch(env.LOG_ENDPOINT, { method: "POST", body: JSON.stringify(events), }) ); } }; ``` ### 2. Configure Producer Worker In producer's `wrangler.jsonc`: ```jsonc { "name": "my-producer-worker", "tail_consumers": [ { "service": "my-tail-worker" } ] } ``` ### 3. Deploy Both Workers ```bash # Deploy Tail Worker first cd tail-worker wrangler deploy # Then deploy producer Worker cd ../producer-worker wrangler deploy ``` ## Wrangler Configuration ### Single Tail Consumer ```jsonc { "name": "producer-worker", "tail_consumers": [ { "service": "logging-tail-worker" } ] } ``` ### Multiple Tail Consumers ```jsonc { "name": "producer-worker", "tail_consumers": [ { "service": "logging-tail-worker" }, { "service": "metrics-tail-worker" } ] } ``` **Note:** Each consumer receives ALL events independently. ### Remove Tail Consumer ```jsonc { "tail_consumers": [] } ``` Then redeploy producer Worker. ## Environment Variables Tail Workers use same binding syntax as regular Workers: ```jsonc { "name": "my-tail-worker", "vars": { "LOG_ENDPOINT": "https://logs.example.com/ingest" }, "kv_namespaces": [ { "binding": "LOGS_KV", "id": "abc123..." } ] } ``` ## Testing & Development ### Local Testing **Tail Workers cannot be fully tested with `wrangler dev`.** Deploy to staging environment for testing. ### Testing Strategy 1. Deploy producer Worker to staging 2. Deploy Tail Worker to staging 3. Configure `tail_consumers` in producer 4. Trigger producer Worker requests 5. Verify Tail Worker receives events (check destination logs/storage) ### Wrangler Tail Command ```bash # Stream logs to terminal (NOT Tail Workers) wrangler tail my-producer-worker ``` **This is different from Tail Workers:** - `wrangler tail` streams logs to your terminal - Tail Workers are Workers that process events programmatically ## Deployment Checklist - [ ] Tail Worker has `tail()` handler - [ ] Tail Worker deployed before producer - [ ] Producer's `wrangler.jsonc` has correct `tail_consumers` - [ ] Environment variables configured - [ ] Tested with staging environment - [ ] Monitoring configured for Tail Worker itself ## Limits | Limit | Value | Notes | |-------|-------|-------| | Max tail consumers per producer | 10 | Each receives all events independently | | Events batch size | Up to 100 events per invocation | Larger batches split across invocations | | Tail Worker CPU time | Same as regular Workers | 10ms (free), 30ms (paid), 50ms (paid bundle) | | Pricing tier | Workers Paid or Enterprise | Not available on free plan | | Request body size | 100 MB max | When sending to external endpoints | | Event retention | None | Events not retried if tail handler fails | ## Workers for Platforms For dynamic dispatch Workers, both dispatch and user Worker events sent to tail consumer: ```jsonc { "name": "dispatch-worker", "tail_consumers": [ { "service": "platform-tail-worker" } ] } ``` Tail Worker receives TWO `TraceItem` elements per request: 1. Dynamic dispatch Worker event 2. User Worker event See [patterns.md](patterns.md) for handling. ================================================ FILE: skills/.curated/cloudflare-deploy/references/tail-workers/gotchas.md ================================================ # Tail Workers Gotchas & Debugging ## Critical Pitfalls ### 1. Not Using `ctx.waitUntil()` **Problem:** Async work doesn't complete or tail Worker times out **Cause:** Handlers exit immediately; awaiting blocks processing **Solution:** ```typescript // ❌ WRONG - fire and forget export default { async tail(events) { fetch(endpoint, { body: JSON.stringify(events) }); } }; // ❌ WRONG - blocking await export default { async tail(events, env, ctx) { await fetch(endpoint, { body: JSON.stringify(events) }); } }; // ✅ CORRECT export default { async tail(events, env, ctx) { ctx.waitUntil( (async () => { await fetch(endpoint, { body: JSON.stringify(events) }); await processMore(); })() ); } }; ``` ### 2. Missing `tail()` Handler **Problem:** Producer deployment fails **Cause:** Worker in `tail_consumers` doesn't export `tail()` handler **Solution:** Ensure `export default { async tail(events, env, ctx) { ... } }` ### 3. Outcome vs HTTP Status **Problem:** Filtering by wrong status **Cause:** `outcome` is script execution status, not HTTP status ```typescript // ❌ WRONG if (event.outcome === 500) { /* never matches */ } // ✅ CORRECT if (event.outcome === 'exception') { /* script threw */ } if (event.event?.response?.status === 500) { /* HTTP 500 */ } ``` ### 4. Timestamp Units **Problem:** Dates off by 1000x **Cause:** Timestamps are epoch milliseconds, not seconds ```typescript // ❌ WRONG: const date = new Date(event.eventTimestamp * 1000); // ✅ CORRECT: const date = new Date(event.eventTimestamp); ``` ### 5. Type Name Mismatch **Problem:** Using `TailItem` type **Cause:** Old docs used `TailItem`, SDK uses `TraceItem` ```typescript import type { TraceItem } from '@cloudflare/workers-types'; export default { async tail(events: TraceItem[], env, ctx) { /* ... */ } }; ``` ### 6. Excessive Logging Volume **Problem:** Unexpected high costs **Cause:** Invoked on EVERY producer request **Solution:** Sample events ```typescript export default { async tail(events, env, ctx) { if (Math.random() > 0.1) return; // 10% sample ctx.waitUntil(sendToEndpoint(events)); } }; ``` ### 7. Serialization Issues **Problem:** `JSON.stringify()` fails **Cause:** `log.message` is `unknown[]` with non-serializable values **Solution:** ```typescript const safePayload = events.map(e => ({ ...e, logs: e.logs.map(log => ({ ...log, message: log.message.map(m => { try { return JSON.parse(JSON.stringify(m)); } catch { return String(m); } }) })) })); ``` ### 8. Missing Error Handling **Problem:** Tail Worker silently fails **Cause:** No try/catch **Solution:** ```typescript ctx.waitUntil((async () => { try { await fetch(env.ENDPOINT, { body: JSON.stringify(events) }); } catch (error) { console.error("Tail error:", error); await env.FALLBACK_KV.put(`failed:${Date.now()}`, JSON.stringify(events)); } })()); ``` ### 9. Deployment Order **Problem:** Producer deployment fails **Cause:** Tail consumer not deployed yet **Solution:** Deploy tail consumer FIRST ```bash cd tail-worker && wrangler deploy cd ../producer && wrangler deploy ``` ### 10. No Event Retry **Problem:** Events lost when handler fails **Cause:** Failed invocations NOT retried **Solution:** Implement fallback storage (see #8) ## Debugging **View logs:** `wrangler tail my-tail-worker` **Incremental testing:** 1. Verify receipt: `console.log('Events:', events.length)` 2. Inspect structure: `console.log(JSON.stringify(events[0], null, 2))` 3. Add external call with `ctx.waitUntil()` **Monitor dashboard:** Check invocation count (matches producer?), error rate, CPU time ## Testing Add test endpoint to producer: ```typescript export default { async fetch(request) { if (request.url.includes('/test')) { console.log('Test log'); throw new Error('Test error'); } return new Response('OK'); } }; ``` Trigger: `curl https://producer.example.workers.dev/test` ## Common Errors | Error | Cause | Solution | |-------|-------|----------| | "Tail consumer not found" | Not deployed | Deploy tail Worker first | | "No tail handler" | Missing `tail()` | Add to default export | | "waitUntil is not a function" | Missing `ctx` | Add `ctx` parameter | | Timeout | Blocking await | Use `ctx.waitUntil()` | ## Performance Notes - Max 100 events per invocation - Each consumer receives all events independently - CPU limits same as regular Workers - For high volume, use Durable Objects batching ================================================ FILE: skills/.curated/cloudflare-deploy/references/tail-workers/patterns.md ================================================ # Tail Workers Common Patterns ## Community Libraries While most tail Worker implementations are custom, these libraries may help: **Logging/Observability:** - **Axiom** - `axiom-cloudflare-workers` (npm) - Direct Axiom integration - **Baselime** - SDK for Baselime observability platform - **LogFlare** - Structured log aggregation **Type Definitions:** - **@cloudflare/workers-types** - Official TypeScript types (use `TraceItem`) **Note:** Most integrations require custom tail handler implementation. See integration examples below. ## Basic Patterns ### HTTP Endpoint Logging ```typescript export default { async tail(events, env, ctx) { const payload = events.map(event => ({ script: event.scriptName, timestamp: event.eventTimestamp, outcome: event.outcome, url: event.event?.request?.url, status: event.event?.response?.status, logs: event.logs, exceptions: event.exceptions, })); ctx.waitUntil( fetch(env.LOG_ENDPOINT, { method: "POST", body: JSON.stringify(payload), }) ); } }; ``` ### Error Tracking Only ```typescript export default { async tail(events, env, ctx) { const errors = events.filter(e => e.outcome === 'exception' || e.exceptions.length > 0 ); if (errors.length === 0) return; ctx.waitUntil( fetch(env.ERROR_ENDPOINT, { method: "POST", body: JSON.stringify(errors), }) ); } }; ``` ## Storage Integration ### KV Storage with TTL ```typescript export default { async tail(events, env, ctx) { ctx.waitUntil( Promise.all(events.map(event => env.LOGS_KV.put( `log:${event.scriptName}:${event.eventTimestamp}`, JSON.stringify(event), { expirationTtl: 86400 } // 24 hours ) )) ); } }; ``` ### Analytics Engine Metrics ```typescript export default { async tail(events, env, ctx) { ctx.waitUntil( Promise.all(events.map(event => env.ANALYTICS.writeDataPoint({ blobs: [event.scriptName, event.outcome], doubles: [1, event.event?.response?.status ?? 0], indexes: [event.event?.request?.cf?.colo ?? 'unknown'], }) )) ); } }; ``` ## Filtering & Routing Filter by route, outcome, or other criteria: ```typescript export default { async tail(events, env, ctx) { // Route filtering const apiEvents = events.filter(e => e.event?.request?.url?.includes('/api/') ); // Multi-destination routing const errors = events.filter(e => e.outcome === 'exception'); const success = events.filter(e => e.outcome === 'ok'); const tasks = []; if (errors.length > 0) { tasks.push(fetch(env.ERROR_ENDPOINT, { method: "POST", body: JSON.stringify(errors), })); } if (success.length > 0) { tasks.push(fetch(env.SUCCESS_ENDPOINT, { method: "POST", body: JSON.stringify(success), })); } ctx.waitUntil(Promise.all(tasks)); } }; ``` ## Sampling Reduce costs by processing only a percentage of events: ```typescript export default { async tail(events, env, ctx) { if (Math.random() > 0.1) return; // 10% sample rate ctx.waitUntil(fetch(env.LOG_ENDPOINT, { method: "POST", body: JSON.stringify(events), })); } }; ``` ## Advanced Patterns ### Batching with Durable Objects Accumulate events before sending: ```typescript export default { async tail(events, env, ctx) { const batch = env.BATCH_DO.get(env.BATCH_DO.idFromName("batch")); ctx.waitUntil(batch.fetch("https://batch/add", { method: "POST", body: JSON.stringify(events), })); } }; ``` See durable-objects skill for full implementation. ### Workers for Platforms Dynamic dispatch sends TWO events per request. Filter by `scriptName` to distinguish dispatch vs user Worker events. ### Error Handling Always wrap external calls. See gotchas.md for fallback storage pattern. ================================================ FILE: skills/.curated/cloudflare-deploy/references/terraform/README.md ================================================ # Cloudflare Terraform Provider **Expert guidance for Cloudflare Terraform Provider - infrastructure as code for Cloudflare resources.** ## Core Principles - **Provider-first**: Use Terraform provider for ALL infrastructure - never mix with wrangler.jsonc for the same resources - **State management**: Always use remote state (S3, Terraform Cloud, etc.) for team environments - **Modular architecture**: Create reusable modules for common patterns (zones, workers, pages) - **Version pinning**: Always pin provider version with `~>` for predictable upgrades - **Secret management**: Use variables + environment vars for sensitive data - never hardcode API tokens ## Provider Version | Version | Status | Notes | |---------|--------|-------| | 5.x | Current | Auto-generated from OpenAPI, breaking changes from v4 | | 4.x | Legacy | Manual maintenance, deprecated | **Critical:** v5 renamed many resources (`cloudflare_record` → `cloudflare_dns_record`, `cloudflare_worker_*` → `cloudflare_workers_*`). See [gotchas.md](./gotchas.md#v5-breaking-changes) for migration details. ## Provider Setup ### Basic Configuration ```hcl terraform { required_version = ">= 1.0" required_providers { cloudflare = { source = "cloudflare/cloudflare" version = "~> 5.15.0" } } } provider "cloudflare" { api_token = var.cloudflare_api_token # or CLOUDFLARE_API_TOKEN env var } ``` ### Authentication Methods (priority order) 1. **API Token** (RECOMMENDED): `api_token` or `CLOUDFLARE_API_TOKEN` - Create: Dashboard → My Profile → API Tokens - Scope to specific accounts/zones for security 2. **Global API Key** (LEGACY): `api_key` + `api_email` or `CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL` - Less secure, use tokens instead 3. **User Service Key**: `user_service_key` for Origin CA certificates ## Quick Reference: Common Commands ```bash terraform init # Initialize provider terraform plan # Plan changes terraform apply # Apply changes terraform destroy # Destroy resources terraform import cloudflare_zone.example # Import existing terraform state list # List resources in state terraform output # Show outputs terraform fmt -recursive # Format code terraform validate # Validate configuration ``` ## Import Existing Resources Use cf-terraforming to generate configs from existing Cloudflare resources: ```bash # Install brew install cloudflare/cloudflare/cf-terraforming # Generate HCL from existing resources cf-terraforming generate --resource-type cloudflare_dns_record --zone # Import into Terraform state cf-terraforming import --resource-type cloudflare_dns_record --zone ``` ## Reading Order 1. Start with [README.md](./README.md) for provider setup and authentication 2. Review [configuration.md](./configuration.md) for resource configurations 3. Check [api.md](./api.md) for data sources and existing resource queries 4. See [patterns.md](./patterns.md) for multi-environment and CI/CD patterns 5. Read [gotchas.md](./gotchas.md) for state drift, v5 breaking changes, and troubleshooting ## In This Reference - [configuration.md](./configuration.md) - Resources for zones, DNS, workers, KV, R2, D1, Pages, rulesets - [api.md](./api.md) - Data sources for existing resources - [patterns.md](./patterns.md) - Architecture patterns, multi-env setup, CI/CD integration - [gotchas.md](./gotchas.md) - Common issues, security, best practices ## See Also - [pulumi](../pulumi/) - Alternative IaC tool for Cloudflare - [wrangler](../wrangler/) - CLI deployment alternative - [workers](../workers/) - Worker runtime documentation ================================================ FILE: skills/.curated/cloudflare-deploy/references/terraform/api.md ================================================ # Terraform Data Sources Reference Query existing Cloudflare resources to reference in your configurations. ## v5 Data Source Names | v4 Name | v5 Name | Notes | |---------|---------|-------| | `cloudflare_record` | `cloudflare_dns_record` | | | `cloudflare_worker_script` | `cloudflare_workers_script` | Note: plural | | `cloudflare_access_*` | `cloudflare_zero_trust_*` | Access → Zero Trust | ## Zone Data Sources ```hcl # Get zone by name data "cloudflare_zone" "example" { name = "example.com" } # Use in resources resource "cloudflare_dns_record" "www" { zone_id = data.cloudflare_zone.example.id name = "www" # ... } ``` ## Account Data Sources ```hcl # List all accounts data "cloudflare_accounts" "main" { name = "My Account" } # Use account ID resource "cloudflare_worker_script" "api" { account_id = data.cloudflare_accounts.main.accounts[0].id # ... } ``` ## Worker Data Sources ```hcl # Get existing worker script (v5: cloudflare_workers_script) data "cloudflare_workers_script" "existing" { account_id = var.account_id name = "existing-worker" } # Reference in service bindings resource "cloudflare_workers_script" "consumer" { service_binding { name = "UPSTREAM" service = data.cloudflare_workers_script.existing.name } } ``` ## KV Data Sources ```hcl # Get KV namespace data "cloudflare_workers_kv_namespace" "existing" { account_id = var.account_id namespace_id = "abc123" } # Use in worker binding resource "cloudflare_workers_script" "api" { kv_namespace_binding { name = "KV" namespace_id = data.cloudflare_workers_kv_namespace.existing.id } } ``` ## Lists Data Source ```hcl # Get IP lists for WAF rules data "cloudflare_list" "blocked_ips" { account_id = var.account_id name = "blocked_ips" } ``` ## IP Ranges Data Source ```hcl # Get Cloudflare IP ranges (for firewall rules) data "cloudflare_ip_ranges" "cloudflare" {} output "ipv4_cidrs" { value = data.cloudflare_ip_ranges.cloudflare.ipv4_cidr_blocks } output "ipv6_cidrs" { value = data.cloudflare_ip_ranges.cloudflare.ipv6_cidr_blocks } # Use in security group rules (AWS example) resource "aws_security_group_rule" "allow_cloudflare" { type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = data.cloudflare_ip_ranges.cloudflare.ipv4_cidr_blocks security_group_id = aws_security_group.web.id } ``` ## Common Patterns ### Import ID Formats | Resource | Import ID Format | |----------|------------------| | `cloudflare_zone` | `` | | `cloudflare_dns_record` | `/` | | `cloudflare_workers_script` | `/` | | `cloudflare_workers_kv_namespace` | `/` | | `cloudflare_r2_bucket` | `/` | | `cloudflare_d1_database` | `/` | | `cloudflare_pages_project` | `/` | ```bash # Example: Import DNS record terraform import cloudflare_dns_record.example / ``` ### Reference Across Modules ```hcl # modules/worker/main.tf data "cloudflare_zone" "main" { name = var.domain } resource "cloudflare_worker_route" "api" { zone_id = data.cloudflare_zone.main.id pattern = "api.${var.domain}/*" script_name = cloudflare_worker_script.api.name } ``` ### Output Important Values ```hcl output "zone_id" { value = cloudflare_zone.main.id description = "Zone ID for DNS management" } output "worker_url" { value = "https://${cloudflare_worker_domain.api.hostname}" description = "Worker API endpoint" } output "kv_namespace_id" { value = cloudflare_workers_kv_namespace.app.id sensitive = false } output "name_servers" { value = cloudflare_zone.main.name_servers description = "Name servers for domain registration" } ``` ## See Also - [README](./README.md) - Provider setup - [Configuration Reference](./configuration.md) - All resource types - [Patterns](./patterns.md) - Architecture patterns - [Troubleshooting](./gotchas.md) - Common issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/terraform/configuration.md ================================================ # Terraform Configuration Reference Complete resource configurations for Cloudflare infrastructure. ## Zone & DNS ```hcl # Zone + settings resource "cloudflare_zone" "example" { account = { id = var.account_id }; name = "example.com"; type = "full" } resource "cloudflare_zone_settings_override" "example" { zone_id = cloudflare_zone.example.id settings { ssl = "strict"; always_use_https = "on"; min_tls_version = "1.2"; tls_1_3 = "on"; http3 = "on" } } # DNS records (A, CNAME, MX, TXT) resource "cloudflare_dns_record" "www" { zone_id = cloudflare_zone.example.id; name = "www"; content = "192.0.2.1"; type = "A"; proxied = true } resource "cloudflare_dns_record" "mx" { for_each = { "10" = "mail1.example.com", "20" = "mail2.example.com" } zone_id = cloudflare_zone.example.id; name = "@"; content = each.value; type = "MX"; priority = each.key } ``` ## Workers ### Simple Pattern (Legacy - Still Works) ```hcl resource "cloudflare_workers_script" "api" { account_id = var.account_id; name = "api-worker"; content = file("worker.js") module = true; compatibility_date = "2025-01-01" kv_namespace_binding { name = "KV"; namespace_id = cloudflare_workers_kv_namespace.cache.id } r2_bucket_binding { name = "BUCKET"; bucket_name = cloudflare_r2_bucket.assets.name } d1_database_binding { name = "DB"; database_id = cloudflare_d1_database.app.id } secret_text_binding { name = "SECRET"; text = var.secret } } ``` ### Gradual Rollouts (Recommended for Production) ```hcl resource "cloudflare_worker" "api" { account_id = var.account_id; name = "api-worker" } resource "cloudflare_worker_version" "api_v1" { account_id = var.account_id; worker_name = cloudflare_worker.api.name content = file("worker.js"); content_sha256 = filesha256("worker.js") compatibility_date = "2025-01-01" bindings { kv_namespace { name = "KV"; namespace_id = cloudflare_workers_kv_namespace.cache.id } r2_bucket { name = "BUCKET"; bucket_name = cloudflare_r2_bucket.assets.name } } } resource "cloudflare_workers_deployment" "api" { account_id = var.account_id; worker_name = cloudflare_worker.api.name versions { version_id = cloudflare_worker_version.api_v1.id; percentage = 100 } } ``` ### Worker Binding Types (v5) | Binding | Attribute | Example | |---------|-----------|---------| | KV | `kv_namespace_binding` | `{ name = "KV", namespace_id = "..." }` | | R2 | `r2_bucket_binding` | `{ name = "BUCKET", bucket_name = "..." }` | | D1 | `d1_database_binding` | `{ name = "DB", database_id = "..." }` | | Service | `service_binding` | `{ name = "AUTH", service = "auth-worker" }` | | Secret | `secret_text_binding` | `{ name = "API_KEY", text = "..." }` | | Queue | `queue_binding` | `{ name = "QUEUE", queue_name = "..." }` | | Vectorize | `vectorize_binding` | `{ name = "INDEX", index_name = "..." }` | | Hyperdrive | `hyperdrive_binding` | `{ name = "DB", id = "..." }` | | AI | `ai_binding` | `{ name = "AI" }` | | Browser | `browser_binding` | `{ name = "BROWSER" }` | | Analytics | `analytics_engine_binding` | `{ name = "ANALYTICS", dataset = "..." }` | | mTLS | `mtls_certificate_binding` | `{ name = "CERT", certificate_id = "..." }` | ### Routes & Triggers ```hcl resource "cloudflare_worker_route" "api" { zone_id = cloudflare_zone.example.id; pattern = "api.example.com/*" script_name = cloudflare_workers_script.api.name } resource "cloudflare_worker_cron_trigger" "task" { account_id = var.account_id; script_name = cloudflare_workers_script.api.name schedules = ["*/5 * * * *"] } ``` ## Storage (KV, R2, D1) ```hcl # KV resource "cloudflare_workers_kv_namespace" "cache" { account_id = var.account_id; title = "cache" } resource "cloudflare_workers_kv" "config" { account_id = var.account_id; namespace_id = cloudflare_workers_kv_namespace.cache.id key_name = "config"; value = jsonencode({ version = "1.0" }) } # R2 resource "cloudflare_r2_bucket" "assets" { account_id = var.account_id; name = "assets"; location = "WNAM" } # D1 (migrations via wrangler) & Queues resource "cloudflare_d1_database" "app" { account_id = var.account_id; name = "app-db" } resource "cloudflare_queue" "events" { account_id = var.account_id; name = "events-queue" } ``` ## Pages ```hcl resource "cloudflare_pages_project" "site" { account_id = var.account_id; name = "site"; production_branch = "main" deployment_configs { production { compatibility_date = "2025-01-01" environment_variables = { NODE_ENV = "production" } kv_namespaces = { KV = cloudflare_workers_kv_namespace.cache.id } d1_databases = { DB = cloudflare_d1_database.app.id } } } build_config { build_command = "npm run build"; destination_dir = "dist" } source { type = "github"; config { owner = "org"; repo_name = "site"; production_branch = "main" }} } resource "cloudflare_pages_domain" "custom" { account_id = var.account_id; project_name = cloudflare_pages_project.site.name; domain = "site.example.com" } ``` ## Rulesets (WAF, Redirects, Cache) ```hcl # WAF resource "cloudflare_ruleset" "waf" { zone_id = cloudflare_zone.example.id; name = "WAF"; kind = "zone"; phase = "http_request_firewall_custom" rules { action = "block"; enabled = true; expression = "(cf.client.bot) and not (cf.verified_bot)" } } # Redirects resource "cloudflare_ruleset" "redirects" { zone_id = cloudflare_zone.example.id; name = "Redirects"; kind = "zone"; phase = "http_request_dynamic_redirect" rules { action = "redirect"; enabled = true; expression = "(http.request.uri.path eq \"/old\")" action_parameters { from_value { status_code = 301; target_url { value = "https://example.com/new" }}} } } # Cache rules resource "cloudflare_ruleset" "cache" { zone_id = cloudflare_zone.example.id; name = "Cache"; kind = "zone"; phase = "http_request_cache_settings" rules { action = "set_cache_settings"; enabled = true; expression = "(http.request.uri.path matches \"\\.(jpg|png|css|js)$\")" action_parameters { cache = true; edge_ttl { mode = "override_origin"; default = 86400 }} } } ``` ## Load Balancers ```hcl resource "cloudflare_load_balancer_monitor" "http" { account_id = var.account_id; type = "http"; path = "/health"; interval = 60; timeout = 5 } resource "cloudflare_load_balancer_pool" "api" { account_id = var.account_id; name = "api-pool"; monitor = cloudflare_load_balancer_monitor.http.id origins { name = "api-1"; address = "192.0.2.1" } origins { name = "api-2"; address = "192.0.2.2" } } resource "cloudflare_load_balancer" "api" { zone_id = cloudflare_zone.example.id; name = "api.example.com" default_pool_ids = [cloudflare_load_balancer_pool.api.id]; steering_policy = "geo" } ``` ## Access (Zero Trust) ```hcl resource "cloudflare_access_application" "admin" { account_id = var.account_id; name = "Admin"; domain = "admin.example.com"; type = "self_hosted" session_duration = "24h"; allowed_idps = [cloudflare_access_identity_provider.github.id] } resource "cloudflare_access_policy" "allow" { account_id = var.account_id; application_id = cloudflare_access_application.admin.id name = "Allow"; decision = "allow"; precedence = 1 include { email = ["admin@example.com"] } } resource "cloudflare_access_identity_provider" "github" { account_id = var.account_id; name = "GitHub"; type = "github" config { client_id = var.github_id; client_secret = var.github_secret } } ``` ## See Also - [README](./README.md) - Provider setup - [API](./api.md) - Data sources - [Patterns](./patterns.md) - Use cases - [Troubleshooting](./gotchas.md) - Issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/terraform/gotchas.md ================================================ # Terraform Troubleshooting & Best Practices Common issues, security considerations, and best practices. ## State Drift Issues Some resources have known state drift. Add lifecycle blocks to prevent perpetual diffs: | Resource | Drift Attributes | Workaround | |----------|------------------|------------| | `cloudflare_pages_project` | `deployment_configs.*` | `ignore_changes = [deployment_configs]` | | `cloudflare_workers_script` | secrets returned as REDACTED | `ignore_changes = [secret_text_binding]` | | `cloudflare_load_balancer` | `adaptive_routing`, `random_steering` | `ignore_changes = [adaptive_routing, random_steering]` | | `cloudflare_workers_kv` | special chars in keys (< 5.16.0) | Upgrade to 5.16.0+ | ```hcl # Example: Ignore secret drift resource "cloudflare_workers_script" "api" { account_id = var.account_id name = "api-worker" content = file("worker.js") secret_text_binding { name = "API_KEY"; text = var.api_key } lifecycle { ignore_changes = [secret_text_binding] } } ``` ## v5 Breaking Changes Provider v5 is current (auto-generated from OpenAPI). v4→v5 has breaking changes: **Resource Renames:** | v4 Resource | v5 Resource | Notes | |-------------|-------------|-------| | `cloudflare_record` | `cloudflare_dns_record` | | | `cloudflare_worker_script` | `cloudflare_workers_script` | Note: plural | | `cloudflare_worker_*` | `cloudflare_workers_*` | All worker resources | | `cloudflare_access_*` | `cloudflare_zero_trust_*` | Access → Zero Trust | **Attribute Changes:** | v4 Attribute | v5 Attribute | Resources | |--------------|--------------|-----------| | `zone` | `name` | zone | | `account_id` | `account.id` | zone (object syntax) | | `key` | `key_name` | KV | | `location_hint` | `location` | R2 | **State Migration:** ```bash # Rename resources in state after v5 upgrade terraform state mv cloudflare_record.example cloudflare_dns_record.example terraform state mv cloudflare_worker_script.api cloudflare_workers_script.api ``` ## Resource-Specific Gotchas ### R2 Location Case Sensitivity **Problem:** Terraform creates R2 bucket but fails on subsequent applies **Cause:** Location must be UPPERCASE **Solution:** Use `WNAM`, `ENAM`, `WEUR`, `EEUR`, `APAC` (not `wnam`, `enam`, etc.) ```hcl resource "cloudflare_r2_bucket" "assets" { account_id = var.account_id name = "assets" location = "WNAM" # UPPERCASE required } ``` ### KV Special Characters (< 5.16.0) **Problem:** Keys with `+`, `#`, `%` cause encoding issues **Cause:** URL encoding bug in provider < 5.16.0 **Solution:** Upgrade to 5.16.0+ or avoid special chars in keys ### D1 Migrations **Problem:** Terraform creates database but schema is empty **Cause:** Terraform only creates D1 resource, not schema **Solution:** Run migrations via wrangler after Terraform apply ```bash # After terraform apply wrangler d1 migrations apply ``` ### Worker Script Size Limit **Problem:** Worker deployment fails with "script too large" **Cause:** Worker script + dependencies exceed 10 MB limit **Solution:** Use code splitting, external dependencies, or minification ### Pages Project Drift **Problem:** Pages project shows perpetual diff on `deployment_configs` **Cause:** Cloudflare API adds default values not in Terraform state **Solution:** Add lifecycle ignore block (see State Drift table above) ## Common Errors ### "Error: couldn't find resource" **Cause:** Resource was deleted outside Terraform **Solution:** Import resource back into state with `terraform import cloudflare_zone.example ` or remove from state with `terraform state rm cloudflare_zone.example` ### "409 Conflict on worker deployment" **Cause:** Worker being deployed by both Terraform and wrangler simultaneously **Solution:** Choose one deployment method; if using Terraform, remove wrangler deployments ### "DNS record already exists" **Cause:** Existing DNS record not imported into Terraform state **Solution:** Find record ID in Cloudflare dashboard and import with `terraform import cloudflare_dns_record.example /` ### "Invalid provider configuration" **Cause:** API token missing, invalid, or lacking required permissions **Solution:** Set `CLOUDFLARE_API_TOKEN` environment variable or check token permissions in dashboard ### "State locking errors" **Cause:** Multiple concurrent Terraform runs or stale lock from crashed process **Solution:** Remove stale lock with `terraform force-unlock ` (use with caution) ## Limits | Resource | Limit | Notes | |----------|-------|-------| | API token rate limit | Varies by plan | Use `api_client_logging = true` to debug | Worker script size | 10 MB | Includes all dependencies | KV keys per namespace | Unlimited | Pay per operation | R2 storage | Unlimited | Pay per GB | D1 databases | 50,000 per account | Free tier: 10 | Pages projects | 500 per account | 100 for free accounts | DNS records | 3,500 per zone | Free plan ## See Also - [README](./README.md) - Provider setup - [Configuration](./configuration.md) - Resources - [API](./api.md) - Data sources - [Patterns](./patterns.md) - Use cases - Provider docs: https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs ================================================ FILE: skills/.curated/cloudflare-deploy/references/terraform/patterns.md ================================================ # Terraform Patterns & Use Cases Architecture patterns, multi-environment setups, and real-world use cases. ## Recommended Directory Structure ``` terraform/ ├── environments/ │ ├── production/ │ │ ├── main.tf │ │ └── terraform.tfvars │ └── staging/ │ ├── main.tf │ └── terraform.tfvars ├── modules/ │ ├── zone/ │ ├── worker/ │ └── dns/ └── shared/ # Shared resources across envs └── main.tf ``` **Note:** Cloudflare recommends avoiding modules for provider resources due to v5 auto-generation complexity. Prefer environment directories + shared state instead. ## Multi-Environment Setup ```hcl # Directory: environments/{production,staging}/main.tf + modules/{zone,worker,pages} module "zone" { source = "../../modules/zone"; account_id = var.account_id; zone_name = "example.com"; environment = "production" } module "api_worker" { source = "../../modules/worker"; account_id = var.account_id; zone_id = module.zone.zone_id name = "api-worker-prod"; script = file("../../workers/api.js"); environment = "production" } ``` ## R2 State Backend ```hcl terraform { backend "s3" { bucket = "terraform-state" key = "cloudflare.tfstate" region = "auto" endpoints = { s3 = "https://.r2.cloudflarestorage.com" } skip_credentials_validation = true skip_region_validation = true skip_requesting_account_id = true skip_metadata_api_check = true skip_s3_checksum = true } } ``` ## Worker with All Bindings ```hcl locals { worker_name = "full-stack-worker" } resource "cloudflare_workers_kv_namespace" "app" { account_id = var.account_id; title = "${local.worker_name}-kv" } resource "cloudflare_r2_bucket" "app" { account_id = var.account_id; name = "${local.worker_name}-bucket" } resource "cloudflare_d1_database" "app" { account_id = var.account_id; name = "${local.worker_name}-db" } resource "cloudflare_worker_script" "app" { account_id = var.account_id; name = local.worker_name; content = file("worker.js"); module = true compatibility_date = "2025-01-01" kv_namespace_binding { name = "KV"; namespace_id = cloudflare_workers_kv_namespace.app.id } r2_bucket_binding { name = "BUCKET"; bucket_name = cloudflare_r2_bucket.app.name } d1_database_binding { name = "DB"; database_id = cloudflare_d1_database.app.id } secret_text_binding { name = "API_KEY"; text = var.api_key } } ``` ## Wrangler Integration **CRITICAL**: Wrangler and Terraform must NOT manage same resources. **Terraform**: Zones, DNS, security rules, Access, load balancers, worker deployments (CI/CD), KV/R2/D1 resource creation **Wrangler**: Local dev (`wrangler dev`), manual deploys, D1 migrations, KV bulk ops, log streaming (`wrangler tail`) ### CI/CD Pattern ```hcl # Terraform creates infrastructure resource "cloudflare_workers_kv_namespace" "app" { account_id = var.account_id; title = "app-kv" } resource "cloudflare_d1_database" "app" { account_id = var.account_id; name = "app-db" } output "kv_namespace_id" { value = cloudflare_workers_kv_namespace.app.id } output "d1_database_id" { value = cloudflare_d1_database.app.id } ``` ```yaml # GitHub Actions: terraform apply → envsubst wrangler.jsonc.template → wrangler deploy - run: terraform apply -auto-approve - run: | export KV_NAMESPACE_ID=$(terraform output -raw kv_namespace_id) envsubst < wrangler.jsonc.template > wrangler.jsonc - run: wrangler deploy ``` ## Use Cases ### Static Site + API Worker ```hcl resource "cloudflare_pages_project" "frontend" { account_id = var.account_id; name = "frontend"; production_branch = "main" build_config { build_command = "npm run build"; destination_dir = "dist" } } resource "cloudflare_worker_script" "api" { account_id = var.account_id; name = "api"; content = file("api-worker.js") d1_database_binding { name = "DB"; database_id = cloudflare_d1_database.api_db.id } } resource "cloudflare_dns_record" "frontend" { zone_id = cloudflare_zone.main.id; name = "app"; content = cloudflare_pages_project.frontend.subdomain; type = "CNAME"; proxied = true } resource "cloudflare_worker_route" "api" { zone_id = cloudflare_zone.main.id; pattern = "api.example.com/*"; script_name = cloudflare_worker_script.api.name } ``` ### Multi-Region Load Balancing ```hcl resource "cloudflare_load_balancer_pool" "us" { account_id = var.account_id; name = "us-pool"; monitor = cloudflare_load_balancer_monitor.http.id origins { name = "us-east"; address = var.us_east_ip } } resource "cloudflare_load_balancer_pool" "eu" { account_id = var.account_id; name = "eu-pool"; monitor = cloudflare_load_balancer_monitor.http.id origins { name = "eu-west"; address = var.eu_west_ip } } resource "cloudflare_load_balancer" "global" { zone_id = cloudflare_zone.main.id; name = "api.example.com"; steering_policy = "geo" default_pool_ids = [cloudflare_load_balancer_pool.us.id] region_pools { region = "WNAM"; pool_ids = [cloudflare_load_balancer_pool.us.id] } region_pools { region = "WEU"; pool_ids = [cloudflare_load_balancer_pool.eu.id] } } ``` ### Secure Admin with Access ```hcl resource "cloudflare_pages_project" "admin" { account_id = var.account_id; name = "admin"; production_branch = "main" } resource "cloudflare_access_application" "admin" { account_id = var.account_id; name = "Admin"; domain = "admin.example.com"; type = "self_hosted"; session_duration = "24h" allowed_idps = [cloudflare_access_identity_provider.google.id] } resource "cloudflare_access_policy" "allow" { account_id = var.account_id; application_id = cloudflare_access_application.admin.id name = "Allow admins"; decision = "allow"; precedence = 1; include { email = var.admin_emails } } ``` ### Reusable Module ```hcl # modules/cloudflare-zone/main.tf variable "account_id" { type = string }; variable "domain" { type = string }; variable "ssl_mode" { default = "strict" } resource "cloudflare_zone" "main" { account = { id = var.account_id }; name = var.domain } resource "cloudflare_zone_settings_override" "main" { zone_id = cloudflare_zone.main.id; settings { ssl = var.ssl_mode; always_use_https = "on" } } output "zone_id" { value = cloudflare_zone.main.id } # Usage: module "prod" { source = "./modules/cloudflare-zone"; account_id = var.account_id; domain = "example.com" } ``` ## See Also - [README](./README.md) - Provider setup - [Configuration Reference](./configuration.md) - All resource types - [API Reference](./api.md) - Data sources - [Troubleshooting](./gotchas.md) - Best practices, common issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/tunnel/README.md ================================================ # Cloudflare Tunnel Secure outbound-only connections between infrastructure and Cloudflare's global network. ## Overview Cloudflare Tunnel (formerly Argo Tunnel) enables: - **Outbound-only connections** - No inbound ports or firewall changes - **Public hostname routing** - Expose local services to internet - **Private network access** - Connect internal networks via WARP - **Zero Trust integration** - Built-in access policies **Architecture**: Tunnel (persistent object) → Replica (`cloudflared` process) → Origin services **Terminology:** - **Tunnel**: Named persistent object with UUID - **Replica**: Individual `cloudflared` process connected to tunnel - **Config Source**: Where ingress rules stored (local file vs Cloudflare dashboard) - **Connector**: Legacy term for replica ## Quick Start ### Local Config ```bash # Install cloudflared brew install cloudflared # macOS # Authenticate cloudflared tunnel login # Create tunnel cloudflared tunnel create my-tunnel # Route DNS cloudflared tunnel route dns my-tunnel app.example.com # Run tunnel cloudflared tunnel run my-tunnel ``` ### Dashboard Config (Recommended) 1. **Zero Trust** > **Networks** > **Tunnels** > **Create** 2. Name tunnel, copy token 3. Configure routes in dashboard 4. Run: `cloudflared tunnel --no-autoupdate run --token ` ## Decision Tree **Choose config source:** ``` Need centralized config updates? ├─ Yes → Token-based (dashboard config) └─ No → Local config file Multiple environments (dev/staging/prod)? ├─ Yes → Local config (version controlled) └─ No → Either works Need firewall approval? └─ See networking.md first ``` ## Core Commands ```bash # Tunnel lifecycle cloudflared tunnel create cloudflared tunnel list cloudflared tunnel info cloudflared tunnel delete # DNS routing cloudflared tunnel route dns cloudflared tunnel route list # Private network cloudflared tunnel route ip add 10.0.0.0/8 # Run tunnel cloudflared tunnel run ``` ## Configuration Example ```yaml # ~/.cloudflared/config.yml tunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef credentials-file: /root/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json ingress: - hostname: app.example.com service: http://localhost:8000 - hostname: api.example.com service: https://localhost:8443 originRequest: noTLSVerify: true - service: http_status:404 ``` ## Reading Order **New to Cloudflare Tunnel:** 1. This README (overview, quick start) 2. [networking.md](./networking.md) - Firewall rules, connectivity pre-checks 3. [configuration.md](./configuration.md) - Config file options, ingress rules 4. [patterns.md](./patterns.md) - Docker, Kubernetes, production deployment 5. [gotchas.md](./gotchas.md) - Troubleshooting, best practices **Enterprise deployment:** 1. [networking.md](./networking.md) - Corporate firewall requirements 2. [gotchas.md](./gotchas.md) - HA setup, security best practices 3. [patterns.md](./patterns.md) - Kubernetes, rolling updates **Programmatic control:** 1. [api.md](./api.md) - REST API, TypeScript SDK ## In This Reference - [networking.md](./networking.md) - Firewall rules, ports, connectivity pre-checks - [configuration.md](./configuration.md) - Config file options, ingress rules, TLS settings - [api.md](./api.md) - REST API, TypeScript SDK, token-based tunnels - [patterns.md](./patterns.md) - Docker, Kubernetes, Terraform, HA, use cases - [gotchas.md](./gotchas.md) - Troubleshooting, limitations, best practices ## See Also - [workers](../workers/) - Workers with Tunnel integration - [access](../access/) - Zero Trust access policies - [warp](../warp/) - WARP client for private networks ================================================ FILE: skills/.curated/cloudflare-deploy/references/tunnel/api.md ================================================ # Tunnel API ## Cloudflare API Access **Base URL**: `https://api.cloudflare.com/client/v4` **Authentication**: ```bash Authorization: Bearer ${CF_API_TOKEN} ``` ## TypeScript SDK Install: `npm install cloudflare` ```typescript import Cloudflare from 'cloudflare'; const cf = new Cloudflare({ apiToken: process.env.CF_API_TOKEN, }); const accountId = process.env.CF_ACCOUNT_ID; ``` ## Create Tunnel ### cURL ```bash curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "my-tunnel", "tunnel_secret": "" }' ``` ### TypeScript ```typescript const tunnel = await cf.zeroTrust.tunnels.create({ account_id: accountId, name: 'my-tunnel', tunnel_secret: Buffer.from(crypto.randomBytes(32)).toString('base64'), }); console.log(`Tunnel ID: ${tunnel.id}`); ``` ## List Tunnels ### cURL ```bash curl -X GET "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels" \ -H "Authorization: Bearer ${CF_API_TOKEN}" ``` ### TypeScript ```typescript const tunnels = await cf.zeroTrust.tunnels.list({ account_id: accountId, }); for (const tunnel of tunnels.result) { console.log(`${tunnel.name}: ${tunnel.id}`); } ``` ## Get Tunnel Info ### cURL ```bash curl -X GET "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" ``` ### TypeScript ```typescript const tunnel = await cf.zeroTrust.tunnels.get(tunnelId, { account_id: accountId, }); console.log(`Status: ${tunnel.status}`); console.log(`Connections: ${tunnel.connections?.length || 0}`); ``` ## Update Tunnel Config ### cURL ```bash curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/configurations" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "config": { "ingress": [ {"hostname": "app.example.com", "service": "http://localhost:8000"}, {"service": "http_status:404"} ] } }' ``` ### TypeScript ```typescript const config = await cf.zeroTrust.tunnels.configurations.update( tunnelId, { account_id: accountId, config: { ingress: [ { hostname: 'app.example.com', service: 'http://localhost:8000' }, { service: 'http_status:404' }, ], }, } ); ``` ## Delete Tunnel ### cURL ```bash curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" ``` ### TypeScript ```typescript await cf.zeroTrust.tunnels.delete(tunnelId, { account_id: accountId, }); ``` ## Token-Based Tunnels (Config Source: Cloudflare) Token-based tunnels store config in Cloudflare dashboard instead of local files. ### Via Dashboard 1. **Zero Trust** > **Networks** > **Tunnels** 2. **Create a tunnel** > **Cloudflared** 3. Configure routes in dashboard 4. Copy token 5. Run on origin: ```bash cloudflared service install ``` ### Via Token ```bash # Run with token (no config file needed) cloudflared tunnel --no-autoupdate run --token ${TUNNEL_TOKEN} # Docker docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token ${TUNNEL_TOKEN} ``` ### Get Tunnel Token (TypeScript) ```typescript // Get tunnel to retrieve token const tunnel = await cf.zeroTrust.tunnels.get(tunnelId, { account_id: accountId, }); // Token available in tunnel.token (only for config source: cloudflare) const token = tunnel.token; ``` ## DNS Routes API ```bash # Create DNS route curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/connections" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ --data '{"hostname": "app.example.com"}' # Delete route curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/connections/{route_id}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" ``` ## Private Network Routes API ```bash # Add IP route curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/routes" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ --data '{"ip_network": "10.0.0.0/8"}' # List IP routes curl -X GET "https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/routes" \ -H "Authorization: Bearer ${CF_API_TOKEN}" ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/tunnel/configuration.md ================================================ # Tunnel Configuration ## Config Source Tunnels use one of two config sources: | Config Source | Storage | Updates | Use Case | |---------------|---------|---------|----------| | Local | `config.yml` file | Edit file, restart | Dev, multi-env, version control | | Cloudflare | Dashboard/API | Instant, no restart | Production, centralized management | **Token-based tunnels** = config source: Cloudflare **Locally-managed tunnels** = config source: local ## Config File Location ``` ~/.cloudflared/config.yml # User config /etc/cloudflared/config.yml # System-wide (Linux) ``` ## Basic Structure ```yaml tunnel: credentials-file: /path/to/.json ingress: - hostname: app.example.com service: http://localhost:8000 - service: http_status:404 # Required catch-all ``` ## Ingress Rules Rules evaluated **top to bottom**, first match wins. ```yaml ingress: # Exact hostname + path regex - hostname: static.example.com path: \.(jpg|png|css|js)$ service: https://localhost:8001 # Wildcard hostname - hostname: "*.example.com" service: https://localhost:8002 # Path only (all hostnames) - path: /api/.* service: http://localhost:9000 # Catch-all (required) - service: http_status:404 ``` **Validation**: ```bash cloudflared tunnel ingress validate cloudflared tunnel ingress rule https://foo.example.com ``` ## Service Types | Protocol | Format | Client Requirement | |----------|--------|-------------------| | HTTP | `http://localhost:8000` | Browser | | HTTPS | `https://localhost:8443` | Browser | | TCP | `tcp://localhost:2222` | `cloudflared access tcp` | | SSH | `ssh://localhost:22` | `cloudflared access ssh` | | RDP | `rdp://localhost:3389` | `cloudflared access rdp` | | Unix | `unix:/path/to/socket` | Browser | | Test | `hello_world` | Browser | ## Origin Configuration ### Connection Settings ```yaml originRequest: connectTimeout: 30s tlsTimeout: 10s tcpKeepAlive: 30s keepAliveTimeout: 90s keepAliveConnections: 100 ``` ### TLS Settings ```yaml originRequest: noTLSVerify: true # Disable cert verification originServerName: "app.internal" # Override SNI caPool: /path/to/ca.pem # Custom CA ``` ### HTTP Settings ```yaml originRequest: disableChunkedEncoding: true httpHostHeader: "app.internal" http2Origin: true ``` ## Private Network Mode ```yaml tunnel: credentials-file: /path/to/creds.json warp-routing: enabled: true ``` ```bash cloudflared tunnel route ip add 10.0.0.0/8 my-tunnel cloudflared tunnel route ip add 192.168.1.100/32 my-tunnel ``` ## Config Source Comparison ### Local Config ```yaml # config.yml tunnel: credentials-file: /path/to/.json ingress: - hostname: app.example.com service: http://localhost:8000 - service: http_status:404 ``` ```bash cloudflared tunnel run my-tunnel ``` **Pros:** Version control, multi-environment, offline edits **Cons:** Requires file distribution, manual restarts ### Cloudflare Config (Token-Based) ```bash # No config file needed cloudflared tunnel --no-autoupdate run --token ``` Configure routes in dashboard: **Zero Trust** > **Networks** > **Tunnels** > [Tunnel] > **Public Hostname** **Pros:** Centralized updates, no file management, instant route changes **Cons:** Requires dashboard/API access, less portable ## Environment Variables ```bash TUNNEL_TOKEN= # Token for config source: cloudflare TUNNEL_ORIGIN_CERT=/path/to/cert.pem # Override cert path (local config) NO_AUTOUPDATE=true # Disable auto-updates TUNNEL_LOGLEVEL=debug # Log level ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/tunnel/gotchas.md ================================================ # Tunnel Gotchas ## Common Errors ### "Error 1016 (Origin DNS Error)" **Cause:** Tunnel not running or not connected **Solution:** ```bash cloudflared tunnel info my-tunnel # Check status ps aux | grep cloudflared # Verify running journalctl -u cloudflared -n 100 # Check logs ``` ### "Self-signed certificate rejected" **Cause:** Origin using self-signed certificate **Solution:** ```yaml originRequest: noTLSVerify: true # Dev only caPool: /path/to/ca.pem # Custom CA ``` ### "Connection timeout" **Cause:** Origin slow to respond or timeout settings too low **Solution:** ```yaml originRequest: connectTimeout: 60s tlsTimeout: 20s keepAliveTimeout: 120s ``` ### "Tunnel not starting" **Cause:** Invalid config, missing credentials, or tunnel doesn't exist **Solution:** ```bash cloudflared tunnel ingress validate # Validate config ls -la ~/.cloudflared/*.json # Verify credentials cloudflared tunnel list # Verify tunnel exists ``` ### "Connection already registered" **Cause:** Multiple replicas with same connector ID or stale connection **Solution:** ```bash # Check active connections cloudflared tunnel info my-tunnel # Wait 60s for stale connection cleanup, or restart with new connector ID cloudflared tunnel run my-tunnel ``` ### "Tunnel credentials rotated but connections fail" **Cause:** Old cloudflared processes using expired credentials **Solution:** ```bash # Stop all cloudflared processes pkill cloudflared # Verify stopped ps aux | grep cloudflared # Restart with new credentials cloudflared tunnel run my-tunnel ``` ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | Free tier | Unlimited tunnels | Unlimited traffic | | Tunnel replicas | 1000 per tunnel | Max concurrent | | Connection duration | No hard limit | Hours to days | | Long-lived connections | May drop during updates | WebSocket, SSH, UDP | | Replica registration | ~5s TTL | Old replica dropped after 5s no heartbeat | | Token rotation grace | 24 hours | Old tokens work during grace period | ## Best Practices ### Security 1. Use token-based tunnels (config source: cloudflare) for centralized control 2. Enable Access policies for sensitive services 3. Rotate tunnel credentials regularly 4. After rotation: stop all old cloudflared processes within 24h grace period 5. Verify TLS certs (`noTLSVerify: false`) 6. Restrict `bastion` service type ### Performance 1. Run multiple replicas for HA (2-4 typical, load balanced automatically) 2. Replicas share same tunnel UUID, get unique connector IDs 3. Place `cloudflared` close to origin (same network) 4. Use HTTP/2 for gRPC (`http2Origin: true`) 5. Tune keepalive for long-lived connections 6. Monitor connection counts ### Configuration 1. Use environment variables for secrets 2. Version control config files 3. Validate before deploying (`cloudflared tunnel ingress validate`) 4. Test rules (`cloudflared tunnel ingress rule `) 5. Document rule order (first match wins) ### Operations 1. Monitor tunnel health in dashboard (shows active replicas) 2. Set up disconnect alerts (when replica count drops to 0) 3. Graceful shutdown for config updates 4. Update replicas in rolling fashion (update 1, wait, update next) 5. Keep `cloudflared` updated (1 year support window) 6. Use `--no-autoupdate` in prod; control updates manually ## Debug Mode ```bash cloudflared tunnel --loglevel debug run my-tunnel cloudflared tunnel ingress rule https://app.example.com ``` ## Migration Strategies ### From Ngrok ```yaml # Ngrok: ngrok http 8000 # Cloudflare Tunnel: ingress: - hostname: app.example.com service: http://localhost:8000 - service: http_status:404 ``` ### From VPN ```yaml # Replace VPN with private network routing warp-routing: enabled: true ``` ```bash cloudflared tunnel route ip add 10.0.0.0/8 my-tunnel ``` Users install WARP client instead of VPN. ================================================ FILE: skills/.curated/cloudflare-deploy/references/tunnel/networking.md ================================================ # Tunnel Networking ## Connectivity Requirements ### Outbound Ports Cloudflared requires outbound access on: | Port | Protocol | Purpose | Required | |------|----------|---------|----------| | 7844 | TCP/UDP | Primary tunnel protocol (QUIC) | Yes | | 443 | TCP | Fallback (HTTP/2) | Yes | **Network path:** ``` cloudflared → edge.argotunnel.com:7844 (preferred) cloudflared → region.argotunnel.com:443 (fallback) ``` ### Firewall Rules #### Minimal (Production) ```bash # Outbound only ALLOW tcp/udp 7844 to *.argotunnel.com ALLOW tcp 443 to *.argotunnel.com ``` #### Full (Recommended) ```bash # Tunnel connectivity ALLOW tcp/udp 7844 to *.argotunnel.com ALLOW tcp 443 to *.argotunnel.com # API access (for token-based tunnels) ALLOW tcp 443 to api.cloudflare.com # Updates (optional) ALLOW tcp 443 to github.com ALLOW tcp 443 to objects.githubusercontent.com ``` ### IP Ranges Cloudflare Anycast IPs (tunnel endpoints): ``` # IPv4 198.41.192.0/24 198.41.200.0/24 # IPv6 2606:4700::/32 ``` **Note:** Use DNS resolution for `*.argotunnel.com` rather than hardcoding IPs. Cloudflare may add edge locations. ## Pre-Flight Check Test connectivity before deploying: ```bash # Test DNS resolution dig edge.argotunnel.com +short # Test port 7844 (QUIC/UDP) nc -zvu edge.argotunnel.com 7844 # Test port 443 (HTTP/2 fallback) nc -zv edge.argotunnel.com 443 # Test with cloudflared cloudflared tunnel --loglevel debug run my-tunnel # Look for "Registered tunnel connection" ``` ### Common Connectivity Errors | Error | Cause | Solution | |-------|-------|----------| | "no such host" | DNS blocked | Allow port 53 UDP/TCP | | "context deadline exceeded" | Port 7844 blocked | Allow UDP/TCP 7844 | | "TLS handshake timeout" | Port 443 blocked | Allow TCP 443, disable SSL inspection | ## Protocol Selection Cloudflared automatically selects protocol: | Protocol | Port | Priority | Use Case | |----------|------|----------|----------| | QUIC | 7844 UDP | 1st (preferred) | Low latency, best performance | | HTTP/2 | 443 TCP | 2nd (fallback) | QUIC blocked by firewall | **Force HTTP/2 fallback:** ```bash cloudflared tunnel --protocol http2 run my-tunnel ``` **Verify active protocol:** ```bash cloudflared tunnel info my-tunnel # Shows "connections" with protocol type ``` ## Private Network Routing ### WARP Client Requirements Users accessing private IPs via WARP need: ```bash # Outbound (WARP client) ALLOW udp 500,4500 to 162.159.*.* (IPsec) ALLOW udp 2408 to 162.159.*.* (WireGuard) ALLOW tcp 443 to *.cloudflareclient.com ``` ### Split Tunnel Configuration Route only private networks through tunnel: ```yaml # warp-routing config warp-routing: enabled: true ``` ```bash # Add specific routes cloudflared tunnel route ip add 10.0.0.0/8 my-tunnel cloudflared tunnel route ip add 172.16.0.0/12 my-tunnel cloudflared tunnel route ip add 192.168.0.0/16 my-tunnel ``` WARP users can access these IPs without VPN. ## Network Diagnostics ### Connection Diagnostics ```bash # Check edge selection and connection health cloudflared tunnel info my-tunnel --output json | jq '.connections[]' # Enable metrics endpoint cloudflared tunnel --metrics localhost:9090 run my-tunnel curl localhost:9090/metrics | grep cloudflared_tunnel # Test latency curl -w "time_total: %{time_total}\n" -o /dev/null https://myapp.example.com ``` ## Corporate Network Considerations Cloudflared honors proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`). If corporate proxy intercepts TLS, add corporate root CA to system trust store. ## Bandwidth and Rate Limits | Limit | Value | Notes | |-------|-------|-------| | Request size | 100 MB | Single HTTP request | | Upload speed | No hard limit | Governed by network/plan | | Concurrent connections | 1000 per tunnel | Across all replicas | | Requests per second | No limit | Subject to DDoS detection | **Large file transfers:** Use R2 or Workers with chunked uploads instead of streaming through tunnel. ================================================ FILE: skills/.curated/cloudflare-deploy/references/tunnel/patterns.md ================================================ # Tunnel Patterns ## Docker Deployment ### Token-Based (Recommended) ```yaml services: cloudflared: image: cloudflare/cloudflared:latest command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN} restart: unless-stopped ``` ### Local Config ```yaml services: cloudflared: image: cloudflare/cloudflared:latest volumes: - ./config.yml:/etc/cloudflared/config.yml:ro - ./credentials.json:/etc/cloudflared/credentials.json:ro command: tunnel run ``` ## Kubernetes Deployment ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: cloudflared spec: replicas: 2 selector: matchLabels: app: cloudflared template: metadata: labels: app: cloudflared spec: containers: - name: cloudflared image: cloudflare/cloudflared:latest args: - tunnel - --no-autoupdate - run - --token - $(TUNNEL_TOKEN) env: - name: TUNNEL_TOKEN valueFrom: secretKeyRef: name: tunnel-credentials key: token ``` ## High Availability ```yaml # Same config on multiple servers tunnel: credentials-file: /path/to/creds.json ingress: - hostname: app.example.com service: http://localhost:8000 - service: http_status:404 ``` Run same config on multiple machines. Cloudflare automatically load balances. Long-lived connections (WebSocket, SSH) may drop during updates. ## Use Cases ### Web Application ```yaml ingress: - hostname: myapp.example.com service: http://localhost:3000 - service: http_status:404 ``` ### SSH Access ```yaml ingress: - hostname: ssh.example.com service: ssh://localhost:22 - service: http_status:404 ``` Client: `cloudflared access ssh --hostname ssh.example.com` ### gRPC Service ```yaml ingress: - hostname: grpc.example.com service: http://localhost:50051 originRequest: http2Origin: true - service: http_status:404 ``` ## Infrastructure as Code ### Terraform ```hcl resource "random_id" "tunnel_secret" { byte_length = 32 } resource "cloudflare_tunnel" "app" { account_id = var.cloudflare_account_id name = "app-tunnel" secret = random_id.tunnel_secret.b64_std } resource "cloudflare_tunnel_config" "app" { account_id = var.cloudflare_account_id tunnel_id = cloudflare_tunnel.app.id config { ingress_rule { hostname = "app.example.com" service = "http://localhost:8000" } ingress_rule { service = "http_status:404" } } } resource "cloudflare_record" "app" { zone_id = var.cloudflare_zone_id name = "app" value = cloudflare_tunnel.app.cname type = "CNAME" proxied = true } output "tunnel_token" { value = cloudflare_tunnel.app.tunnel_token sensitive = true } ``` ### Pulumi ```typescript import * as cloudflare from "@pulumi/cloudflare"; import * as random from "@pulumi/random"; const secret = new random.RandomId("secret", { byteLength: 32 }); const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("tunnel", { accountId: accountId, name: "app-tunnel", secret: secret.b64Std, }); const config = new cloudflare.ZeroTrustTunnelCloudflaredConfig("config", { accountId: accountId, tunnelId: tunnel.id, config: { ingressRules: [ { hostname: "app.example.com", service: "http://localhost:8000" }, { service: "http_status:404" }, ], }, }); new cloudflare.Record("dns", { zoneId: zoneId, name: "app", value: tunnel.cname, type: "CNAME", proxied: true, }); ``` ## Service Installation ### Linux systemd ```bash cloudflared service install systemctl start cloudflared && systemctl enable cloudflared journalctl -u cloudflared -f # Logs ``` ### macOS launchd ```bash sudo cloudflared service install sudo launchctl start com.cloudflare.cloudflared ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/turn/README.md ================================================ # Cloudflare TURN Service Expert guidance for implementing Cloudflare TURN Service in WebRTC applications. ## Overview Cloudflare TURN (Traversal Using Relays around NAT) Service is a managed relay service for WebRTC applications. TURN acts as a relay point for traffic between WebRTC clients and SFUs, particularly when direct peer-to-peer communication is obstructed by NATs or firewalls. The service runs on Cloudflare's global anycast network across 310+ cities. ## Key Characteristics - **Anycast Architecture**: Automatically connects clients to the closest Cloudflare location - **Global Network**: Available across Cloudflare's entire network (excluding China Network) - **Zero Configuration**: No need to manually select regions or servers - **Protocol Support**: STUN/TURN over UDP, TCP, and TLS - **Free Tier**: Free when used with Cloudflare Calls SFU, otherwise $0.05/GB outbound ## In This Reference | File | Purpose | |------|---------| | [api.md](./api.md) | Credentials API, TURN key management, types, constraints | | [configuration.md](./configuration.md) | Worker setup, wrangler.jsonc, env vars, IP allowlisting | | [patterns.md](./patterns.md) | Implementation patterns, use cases, integration examples | | [gotchas.md](./gotchas.md) | Troubleshooting, limits, security, common mistakes | ## Reading Order | Task | Files to Read | Est. Tokens | |------|---------------|-------------| | Quick start | README only | ~500 | | Generate credentials | README → api | ~1300 | | Worker integration | README → configuration → patterns | ~2000 | | Debug connection | gotchas | ~700 | | Security review | api → gotchas | ~1500 | | Enterprise firewall | configuration | ~600 | ## Service Addresses and Ports ### STUN over UDP - **Primary**: `stun.cloudflare.com:3478/udp` - **Alternate**: `stun.cloudflare.com:53/udp` (blocked by browsers, not recommended) ### TURN over UDP - **Primary**: `turn.cloudflare.com:3478/udp` - **Alternate**: `turn.cloudflare.com:53/udp` (blocked by browsers) ### TURN over TCP - **Primary**: `turn.cloudflare.com:3478/tcp` - **Alternate**: `turn.cloudflare.com:80/tcp` ### TURN over TLS - **Primary**: `turn.cloudflare.com:5349/tcp` - **Alternate**: `turn.cloudflare.com:443/tcp` ## Quick Start 1. **Create TURN key via API**: see [api.md#create-turn-key](./api.md#create-turn-key) 2. **Generate credentials**: see [api.md#generate-temporary-credentials](./api.md#generate-temporary-credentials) 3. **Configure Worker**: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration) 4. **Implement client**: see [patterns.md#basic-turn-configuration-browser](./patterns.md#basic-turn-configuration-browser) ## When to Use TURN - **Restrictive NATs**: Symmetric NATs that block direct connections - **Corporate firewalls**: Environments blocking WebRTC ports - **Mobile networks**: Carrier-grade NAT scenarios - **Predictable connectivity**: When reliability > efficiency ## Related Cloudflare Services - **Cloudflare Calls SFU**: Managed Selective Forwarding Unit (TURN free when used with SFU) - **Cloudflare Stream**: Video streaming with WHIP/WHEP support - **Cloudflare Workers**: Backend for credential generation - **Cloudflare KV**: Credential caching - **Cloudflare Durable Objects**: Session state management ## Additional Resources - [Cloudflare Calls Documentation](https://developers.cloudflare.com/calls/) - [Cloudflare TURN Service Docs](https://developers.cloudflare.com/realtime/turn/) - [Cloudflare API Reference](https://developers.cloudflare.com/api/resources/calls/subresources/turn/) - [Orange Meets (Open Source Example)](https://github.com/cloudflare/orange) ================================================ FILE: skills/.curated/cloudflare-deploy/references/turn/api.md ================================================ # TURN API Reference Complete API documentation for Cloudflare TURN service credentials and key management. ## Authentication All endpoints require Cloudflare API token with "Calls Write" permission. Base URL: `https://api.cloudflare.com/client/v4` ## TURN Key Management ### List TURN Keys ``` GET /accounts/{account_id}/calls/turn_keys ``` ### Get TURN Key Details ``` GET /accounts/{account_id}/calls/turn_keys/{key_id} ``` ### Create TURN Key ``` POST /accounts/{account_id}/calls/turn_keys Content-Type: application/json { "name": "my-turn-key" } ``` **Response includes**: - `uid`: Key identifier - `key`: The actual secret key (only returned on creation—save immediately) - `name`: Human-readable name - `created`: ISO 8601 timestamp - `modified`: ISO 8601 timestamp ### Update TURN Key ``` PUT /accounts/{account_id}/calls/turn_keys/{key_id} Content-Type: application/json { "name": "updated-name" } ``` ### Delete TURN Key ``` DELETE /accounts/{account_id}/calls/turn_keys/{key_id} ``` ## Generate Temporary Credentials ``` POST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/generate Authorization: Bearer {key_secret} Content-Type: application/json { "ttl": 86400 } ``` ### Credential Constraints | Parameter | Min | Max | Default | Notes | |-----------|-----|-----|---------|-------| | ttl | 1 | 172800 (48hrs) | varies | API rejects values >172800 | **CRITICAL**: Maximum TTL is 48 hours (172800 seconds). API will reject requests exceeding this limit. ### Response Schema ```json { "iceServers": { "urls": [ "stun:stun.cloudflare.com:3478", "turn:turn.cloudflare.com:3478?transport=udp", "turn:turn.cloudflare.com:3478?transport=tcp", "turn:turn.cloudflare.com:53?transport=udp", "turn:turn.cloudflare.com:80?transport=tcp", "turns:turn.cloudflare.com:5349?transport=tcp", "turns:turn.cloudflare.com:443?transport=tcp" ], "username": "1738035200:user123", "credential": "base64encodedhmac==" } } ``` **Port 53 Warning**: Filter port 53 URLs for browser clients—blocked by Chrome/Firefox. See [gotchas.md](./gotchas.md#using-port-53-in-browsers). ## Revoke Credentials ``` POST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/revoke Authorization: Bearer {key_secret} Content-Type: application/json { "username": "1738035200:user123" } ``` **Response**: 204 No Content Billing stops immediately. Active connection drops after short delay (~seconds). ## TypeScript Types ```typescript interface CloudflareTURNConfig { keyId: string; keySecret: string; ttl?: number; // Max 172800 (48 hours) } interface TURNCredentialsRequest { ttl?: number; // Max 172800 seconds } interface TURNCredentialsResponse { iceServers: { urls: string[]; username: string; credential: string; }; } interface RTCIceServer { urls: string | string[]; username?: string; credential?: string; credentialType?: "password"; } interface TURNKeyResponse { uid: string; key: string; // Only present on creation name: string; created: string; modified: string; } ``` ## Validation Function ```typescript function validateRTCIceServer(obj: unknown): obj is RTCIceServer { if (!obj || typeof obj !== 'object') { return false; } const server = obj as Record; if (typeof server.urls !== 'string' && !Array.isArray(server.urls)) { return false; } if (server.username && typeof server.username !== 'string') { return false; } if (server.credential && typeof server.credential !== 'string') { return false; } return true; } ``` ## Type-Safe Credential Generation ```typescript async function fetchTURNServers( config: CloudflareTURNConfig ): Promise { // Validate TTL constraint const ttl = config.ttl ?? 3600; if (ttl > 172800) { throw new Error('TTL cannot exceed 172800 seconds (48 hours)'); } const response = await fetch( `https://rtc.live.cloudflare.com/v1/turn/keys/${config.keyId}/credentials/generate`, { method: 'POST', headers: { 'Authorization': `Bearer ${config.keySecret}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ ttl }) } ); if (!response.ok) { throw new Error(`TURN credential generation failed: ${response.status}`); } const data = await response.json(); // Filter port 53 for browser clients const filteredUrls = data.iceServers.urls.filter( (url: string) => !url.includes(':53') ); const iceServers = [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: filteredUrls, username: data.iceServers.username, credential: data.iceServers.credential, credentialType: 'password' as const } ]; // Validate before returning if (!iceServers.every(validateRTCIceServer)) { throw new Error('Invalid ICE server configuration received'); } return iceServers; } ``` ## See Also - [configuration.md](./configuration.md) - Worker setup, environment variables - [patterns.md](./patterns.md) - Implementation examples using these APIs - [gotchas.md](./gotchas.md) - Security best practices, common mistakes ================================================ FILE: skills/.curated/cloudflare-deploy/references/turn/configuration.md ================================================ # TURN Configuration Setup and configuration for Cloudflare TURN service in Workers and applications. ## Environment Variables ```bash # .env CLOUDFLARE_ACCOUNT_ID=your_account_id CLOUDFLARE_API_TOKEN=your_api_token TURN_KEY_ID=your_turn_key_id TURN_KEY_SECRET=your_turn_key_secret ``` Validate with zod: ```typescript import { z } from 'zod'; const envSchema = z.object({ CLOUDFLARE_ACCOUNT_ID: z.string().min(1), CLOUDFLARE_API_TOKEN: z.string().min(1), TURN_KEY_ID: z.string().min(1), TURN_KEY_SECRET: z.string().min(1) }); export const config = envSchema.parse(process.env); ``` ## wrangler.jsonc ```jsonc { "name": "turn-credentials-api", "main": "src/index.ts", "compatibility_date": "2025-01-01", "vars": { "TURN_KEY_ID": "your-turn-key-id" // Non-sensitive, can be in vars }, "env": { "production": { "kv_namespaces": [ { "binding": "CREDENTIALS_CACHE", "id": "your-kv-namespace-id" } ] } } } ``` **Store secrets separately**: ```bash wrangler secret put TURN_KEY_SECRET ``` ## Cloudflare Worker Integration ### Worker Binding Types ```typescript interface Env { TURN_KEY_ID: string; TURN_KEY_SECRET: string; CREDENTIALS_CACHE?: KVNamespace; } export default { async fetch(request: Request, env: Env): Promise { // See patterns.md for implementation } } ``` ### Basic Worker Example ```typescript export default { async fetch(request: Request, env: Env): Promise { if (request.url.endsWith('/turn-credentials')) { // Validate client auth const authHeader = request.headers.get('Authorization'); if (!authHeader) { return new Response('Unauthorized', { status: 401 }); } const response = await fetch( `https://rtc.live.cloudflare.com/v1/turn/keys/${env.TURN_KEY_ID}/credentials/generate`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.TURN_KEY_SECRET}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ ttl: 3600 }) } ); if (!response.ok) { return new Response('Failed to generate credentials', { status: 500 }); } const data = await response.json(); // Filter port 53 for browser clients const filteredUrls = data.iceServers.urls.filter( (url: string) => !url.includes(':53') ); return Response.json({ iceServers: [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: filteredUrls, username: data.iceServers.username, credential: data.iceServers.credential } ] }); } return new Response('Not found', { status: 404 }); } }; ``` ## IP Allowlisting (Enterprise/Firewall) For strict firewalls, allowlist these IPs for `turn.cloudflare.com`: | Type | Address | Protocol | |------|---------|----------| | IPv4 | 141.101.90.1/32 | All | | IPv4 | 162.159.207.1/32 | All | | IPv6 | 2a06:98c1:3200::1/128 | All | | IPv6 | 2606:4700:48::1/128 | All | **IMPORTANT**: These IPs may change with 14-day notice. Monitor DNS: ```bash # Check A and AAAA records dig turn.cloudflare.com A dig turn.cloudflare.com AAAA ``` Set up automated monitoring to detect IP changes and update allowlists within 14 days. ## IPv6 Support - **Client-to-TURN**: Both IPv4 and IPv6 supported - **Relay addresses**: IPv4 only (no RFC 6156 support) - **TCP relaying**: Not supported (RFC 6062) Clients can connect via IPv6, but relayed traffic uses IPv4 addresses. ## TLS Configuration ### Supported TLS Versions - TLS 1.1 - TLS 1.2 - TLS 1.3 ### Recommended Ciphers (TLS 1.3) - AEAD-AES128-GCM-SHA256 - AEAD-AES256-GCM-SHA384 - AEAD-CHACHA20-POLY1305-SHA256 ### Recommended Ciphers (TLS 1.2) - ECDHE-ECDSA-AES128-GCM-SHA256 - ECDHE-RSA-AES128-GCM-SHA256 - ECDHE-RSA-AES128-SHA (also TLS 1.1) - AES128-GCM-SHA256 ## See Also - [api.md](./api.md) - TURN key creation, credential generation API - [patterns.md](./patterns.md) - Full Worker implementation patterns - [gotchas.md](./gotchas.md) - Security best practices, troubleshooting ================================================ FILE: skills/.curated/cloudflare-deploy/references/turn/gotchas.md ================================================ # TURN Gotchas & Troubleshooting Common mistakes, security best practices, and troubleshooting for Cloudflare TURN. ## Quick Reference | Issue | Solution | Details | |-------|----------|---------| | Credentials not working | Check TTL ≤ 48hrs | [See Troubleshooting](#issue-turn-credentials-not-working) | | Connection drops after ~48hrs | Implement credential refresh | [See Connection Drops](#issue-connection-drops-after-48-hours) | | Port 53 fails in browser | Filter server-side | [See Port 53](#using-port-53-in-browsers) | | High packet loss | Check rate limits | [See Rate Limits](#limits-per-turn-allocation) | | Connection fails after maintenance | Implement ICE restart | [See ICE Restart](#ice-restart-required-scenarios) | ## Critical Constraints | Constraint | Value | Consequence if Violated | |------------|-------|-------------------------| | Max credential TTL | 48 hours (172800s) | API rejects request | | Credential revocation delay | ~seconds | Billing stops immediately, connection drops shortly | | IP allowlist update window | 14 days (if IPs change) | Connection fails if IPs change | | Packet rate | 5-10k pps per allocation | Packet drops | | Data rate | 50-100 Mbps per allocation | Packet drops | | Unique IP rate | >5 new IPs/sec | Packet drops | ## Limits Per TURN Allocation **Per user** (not account-wide): - **IP addresses**: >5 new unique IPs per second - **Packet rate**: 5-10k packets per second (inbound/outbound) - **Data rate**: 50-100 Mbps (inbound/outbound) - **MTU**: No specific limit - **Burst rates**: Higher than documented Exceeding limits results in **packet drops**. ## Common Mistakes ### Setting TTL > 48 hours ```typescript // ❌ BAD: API will reject const creds = await generate({ ttl: 604800 }); // 7 days // ✅ GOOD: const creds = await generate({ ttl: 86400 }); // 24 hours ``` ### Hardcoding IPs without monitoring ```typescript // ❌ BAD: IPs can change with 14-day notice const iceServers = [{ urls: 'turn:141.101.90.1:3478' }]; // ✅ GOOD: Use DNS const iceServers = [{ urls: 'turn:turn.cloudflare.com:3478' }]; ``` ### Using port 53 in browsers ```typescript // ❌ BAD: Blocked by Chrome/Firefox urls: ['turn:turn.cloudflare.com:53'] // ✅ GOOD: Filter port 53 urls: urls.filter(url => !url.includes(':53')) ``` ### Not handling credential expiry ```typescript // ❌ BAD: Credentials expire but call continues → connection drops const creds = await fetchCreds(); const pc = new RTCPeerConnection({ iceServers: creds }); // ✅ GOOD: Refresh before expiry setInterval(() => refreshCredentials(pc), 3000000); // 50 min ``` ### Missing ICE restart support ```typescript // ❌ BAD: No recovery from TURN maintenance pc.addEventListener('iceconnectionstatechange', () => { console.log('State changed:', pc.iceConnectionState); }); // ✅ GOOD: Implement ICE restart pc.addEventListener('iceconnectionstatechange', async () => { if (pc.iceConnectionState === 'failed') { await refreshCredentials(pc); pc.restartIce(); } }); ``` ### Exposing TURN key secret client-side ```typescript // ❌ BAD: Secret exposed to client const secret = 'your-turn-key-secret'; const response = await fetch(`https://rtc.live.cloudflare.com/v1/turn/...`, { headers: { 'Authorization': `Bearer ${secret}` } }); // ✅ GOOD: Generate credentials server-side const response = await fetch('/api/turn-credentials'); ``` ## ICE Restart Required Scenarios These events require ICE restart (see [patterns.md](./patterns.md#ice-restart-pattern)): 1. **TURN server maintenance** (occasional on Cloudflare's network) 2. **Network topology changes** (anycast routing changes) 3. **Credential refresh** during long sessions (>1 hour) 4. **Connection failure** (iceConnectionState === 'failed') Implement in all production apps: ```typescript pc.addEventListener('iceconnectionstatechange', async () => { if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') { await refreshTURNCredentials(pc); pc.restartIce(); const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); // Send offer to peer via signaling... } }); ``` Reference: [RFC 8445 Section 2.4](https://datatracker.ietf.org/doc/html/rfc8445#section-2.4) ## Security Checklist - [ ] Credentials generated server-side only (never client-side) - [ ] TURN_KEY_SECRET in wrangler secrets, not vars - [ ] TTL ≤ expected session duration (and ≤ 48 hours) - [ ] Rate limiting on credential generation endpoint - [ ] Client authentication before issuing credentials - [ ] Credential revocation API for compromised sessions - [ ] No hardcoded IPs (or DNS monitoring in place) - [ ] Port 53 filtered for browser clients ## Troubleshooting ### Issue: TURN credentials not working **Check:** - Key ID and secret are correct - Credentials haven't expired (check TTL) - TTL doesn't exceed 172800 seconds (48 hours) - Server can reach rtc.live.cloudflare.com - Network allows outbound HTTPS **Solution:** ```typescript // Validate before using if (ttl > 172800) { throw new Error('TTL cannot exceed 48 hours'); } ``` ### Issue: Slow connection establishment **Solutions:** - Ensure proper ICE candidate gathering - Check network latency to Cloudflare edge - Verify firewall allows WebRTC ports (3478, 5349, 443) - Consider using TURN over TLS (port 443) for corporate networks ### Issue: High packet loss **Check:** - Not exceeding rate limits (5-10k pps) - Not exceeding bandwidth limits (50-100 Mbps) - Not connecting to too many unique IPs (>5/sec) - Client network quality ### Issue: Connection drops after ~48 hours **Cause**: Credentials expired (48hr max) **Solution**: - Set TTL to expected session duration - Implement credential refresh with setConfiguration() - Use ICE restart if connection fails ```typescript // Refresh credentials before expiry const refreshInterval = ttl * 1000 - 60000; // 1 min early setInterval(async () => { await refreshTURNCredentials(pc); }, refreshInterval); ``` ### Issue: Port 53 URLs in browser fail silently **Cause**: Chrome/Firefox block port 53 **Solution**: Filter port 53 URLs server-side: ```typescript const filtered = urls.filter(url => !url.includes(':53')); ``` ### Issue: Hardcoded IPs stop working **Cause**: Cloudflare changed IP addresses (14-day notice) **Solution**: - Use DNS hostnames (`turn.cloudflare.com`) - Monitor DNS changes with automated alerts - Update allowlists within 14 days if using IP allowlisting ## Cost Optimization 1. Use appropriate TTLs (don't over-provision) 2. Implement credential caching 3. Set `iceTransportPolicy: 'all'` to try direct first (use `'relay'` only when necessary) 4. Monitor bandwidth usage 5. Free when used with Cloudflare Calls SFU ## See Also - [api.md](./api.md) - Credential generation API, revocation - [configuration.md](./configuration.md) - IP allowlisting, monitoring - [patterns.md](./patterns.md) - ICE restart, credential refresh patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/turn/patterns.md ================================================ # TURN Implementation Patterns Production-ready patterns for implementing Cloudflare TURN in WebRTC applications. ## Prerequisites Before implementing these patterns, ensure you have: - TURN key created: see [api.md#create-turn-key](./api.md#create-turn-key) - Worker configured: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration) ## Basic TURN Configuration (Browser) ```typescript interface RTCIceServer { urls: string | string[]; username?: string; credential?: string; credentialType?: "password" | "oauth"; } async function getTURNConfig(): Promise { const response = await fetch('/api/turn-credentials'); const data = await response.json(); return [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: [ 'turn:turn.cloudflare.com:3478?transport=udp', 'turn:turn.cloudflare.com:3478?transport=tcp', 'turns:turn.cloudflare.com:5349?transport=tcp', 'turns:turn.cloudflare.com:443?transport=tcp' ], username: data.username, credential: data.credential, credentialType: 'password' } ]; } // Use in RTCPeerConnection const iceServers = await getTURNConfig(); const peerConnection = new RTCPeerConnection({ iceServers }); ``` ## Port Selection Strategy Recommended order for browser clients: 1. **3478/udp** (primary, lowest latency) 2. **3478/tcp** (fallback for UDP-blocked networks) 3. **5349/tls** (corporate firewalls, most reliable) 4. **443/tls** (alternate TLS port, firewall-friendly) **Avoid port 53**—blocked by Chrome and Firefox. ```typescript function filterICEServersForBrowser(urls: string[]): string[] { return urls .filter(url => !url.includes(':53')) // Remove port 53 .sort((a, b) => { // Prioritize UDP over TCP over TLS if (a.includes('transport=udp')) return -1; if (b.includes('transport=udp')) return 1; if (a.includes('transport=tcp') && !a.startsWith('turns:')) return -1; if (b.includes('transport=tcp') && !b.startsWith('turns:')) return 1; return 0; }); } ``` ## Credential Refresh (Mid-Session) When credentials expire during long calls: ```typescript async function refreshTURNCredentials(pc: RTCPeerConnection): Promise { const newCreds = await fetch('/turn-credentials').then(r => r.json()); const config = pc.getConfiguration(); config.iceServers = newCreds.iceServers; pc.setConfiguration(config); // Note: setConfiguration() does NOT trigger ICE restart // Combine with restartIce() if connection fails } // Auto-refresh before expiry setInterval(async () => { await refreshTURNCredentials(peerConnection); }, 3000000); // 50 minutes if TTL is 1 hour ``` ## ICE Restart Pattern After network change, TURN server maintenance, or credential expiry: ```typescript pc.addEventListener('iceconnectionstatechange', async () => { if (pc.iceConnectionState === 'failed') { console.warn('ICE connection failed, restarting...'); // Refresh credentials await refreshTURNCredentials(pc); // Trigger ICE restart pc.restartIce(); const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); // Send offer to peer via signaling channel... } }); ``` ## Credentials Caching Pattern ```typescript class TURNCredentialsManager { private creds: { username: string; credential: string; urls: string[]; expiresAt: number; } | null = null; async getCredentials(keyId: string, keySecret: string): Promise { const now = Date.now(); if (this.creds && this.creds.expiresAt > now) { return this.buildIceServers(this.creds); } const ttl = 3600; if (ttl > 172800) throw new Error('TTL max 48hrs'); const res = await fetch( `https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`, { method: 'POST', headers: { 'Authorization': `Bearer ${keySecret}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ ttl }) } ); const data = await res.json(); const filteredUrls = data.iceServers.urls.filter((url: string) => !url.includes(':53')); this.creds = { username: data.iceServers.username, credential: data.iceServers.credential, urls: filteredUrls, expiresAt: now + (ttl * 1000) - 60000 }; return this.buildIceServers(this.creds); } private buildIceServers(c: { username: string; credential: string; urls: string[] }): RTCIceServer[] { return [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: c.urls, username: c.username, credential: c.credential, credentialType: 'password' as const } ]; } } ``` ## Common Use Cases ```typescript // Video conferencing: TURN as fallback const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'all' }; // IoT/predictable connectivity: force TURN const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'relay' }; // Screen sharing: reduce overhead const pc = new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: 'max-bundle' }); ``` ## Integration with Cloudflare Calls SFU ```typescript // TURN is automatically used when needed // Cloudflare Calls handles TURN + SFU coordination const session = await callsClient.createSession({ appId: 'your-app-id', sessionId: 'meeting-123' }); ``` ## Debugging ICE Connectivity ```typescript pc.addEventListener('icecandidate', (event) => { if (event.candidate) { console.log('ICE candidate:', event.candidate.type, event.candidate.protocol); } }); pc.addEventListener('iceconnectionstatechange', () => { console.log('ICE state:', pc.iceConnectionState); }); // Check selected candidate pair const stats = await pc.getStats(); stats.forEach(report => { if (report.type === 'candidate-pair' && report.selected) { console.log('Selected:', report); } }); ``` ## See Also - [api.md](./api.md) - Credential generation API, types - [configuration.md](./configuration.md) - Worker setup, environment variables - [gotchas.md](./gotchas.md) - Common mistakes, troubleshooting ================================================ FILE: skills/.curated/cloudflare-deploy/references/turnstile/README.md ================================================ # Cloudflare Turnstile Implementation Skill Reference Expert guidance for implementing Cloudflare Turnstile - a smart CAPTCHA alternative that protects websites from bots without showing traditional CAPTCHA puzzles. ## Overview Turnstile is a user-friendly CAPTCHA alternative that runs challenges in the background without user interaction. It validates visitors automatically using signals like browser behavior, device fingerprinting, and machine learning. ## Widget Types | Type | Interaction | Use Case | |------|-------------|----------| | **Managed** (default) | Shows checkbox when needed | Forms, logins - balance UX and security | | **Non-Interactive** | Invisible, runs automatically | Frictionless UX, low-risk actions | | **Invisible** | Hidden, triggered programmatically | Pre-clearance, API calls, headless | ## Quick Start ### Implicit Rendering (HTML-based) ```html
``` ### Explicit Rendering (JavaScript-based) ```html
``` ### Server Validation (Required) ```javascript // Cloudflare Workers export default { async fetch(request) { const formData = await request.formData(); const token = formData.get('cf-turnstile-response'); const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: env.TURNSTILE_SECRET, response: token, remoteip: request.headers.get('CF-Connecting-IP') }) }); const validation = await result.json(); if (!validation.success) { return new Response('Invalid CAPTCHA', { status: 400 }); } // Process form... } } ``` ## Testing Keys **Critical for development/testing:** | Type | Key | Behavior | |------|-----|----------| | **Site Key (Always Passes)** | `1x00000000000000000000AA` | Widget succeeds, token validates | | **Site Key (Always Blocks)** | `2x00000000000000000000AB` | Widget fails visibly | | **Site Key (Force Challenge)** | `3x00000000000000000000FF` | Always shows interactive challenge | | **Secret Key (Testing)** | `1x0000000000000000000000000000000AA` | Validates test tokens | **Note:** Test keys work on `localhost` and any domain. Do NOT use in production. ## Key Constraints - **Token expiry:** 5 minutes after generation - **Single-use:** Each token can only be validated once - **Server validation required:** Client-side checks are insufficient ## Reading Order 1. **[configuration.md](configuration.md)** - Setup, widget options, script loading 2. **[api.md](api.md)** - JavaScript API, siteverify endpoints, TypeScript types 3. **[patterns.md](patterns.md)** - Form integration, framework examples, validation patterns 4. **[gotchas.md](gotchas.md)** - Common errors, debugging, limitations ## See Also - [Cloudflare Turnstile Docs](https://developers.cloudflare.com/turnstile/) - [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile) ================================================ FILE: skills/.curated/cloudflare-deploy/references/turnstile/api.md ================================================ # API Reference ## Client-Side JavaScript API The Turnstile JavaScript API is available at `window.turnstile` after loading the script. ### `turnstile.render(container, options)` Renders a Turnstile widget into a container element. **Parameters:** - `container` (string | HTMLElement): CSS selector or DOM element - `options` (TurnstileOptions): Configuration object (see [configuration.md](configuration.md)) **Returns:** `string` - Widget ID for use with other API methods **Example:** ```javascript const widgetId = window.turnstile.render('#my-container', { sitekey: 'YOUR_SITE_KEY', callback: (token) => console.log('Success:', token), 'error-callback': (code) => console.error('Error:', code) }); ``` ### `turnstile.reset(widgetId)` Resets a widget (clears token, resets challenge state). Useful when form validation fails. **Parameters:** - `widgetId` (string): Widget ID from `render()`, or container element **Returns:** `void` **Example:** ```javascript // Reset on form error if (!validateForm()) { window.turnstile.reset(widgetId); } ``` ### `turnstile.remove(widgetId)` Removes a widget from the DOM completely. **Parameters:** - `widgetId` (string): Widget ID from `render()` **Returns:** `void` **Example:** ```javascript // Cleanup on navigation window.turnstile.remove(widgetId); ``` ### `turnstile.getResponse(widgetId)` Gets the current token from a widget (if challenge completed). **Parameters:** - `widgetId` (string): Widget ID from `render()`, or container element **Returns:** `string | undefined` - Token string, or undefined if not ready **Example:** ```javascript const token = window.turnstile.getResponse(widgetId); if (token) { submitForm(token); } ``` ### `turnstile.isExpired(widgetId)` Checks if a widget's token has expired (>5 minutes old). **Parameters:** - `widgetId` (string): Widget ID from `render()` **Returns:** `boolean` - True if expired **Example:** ```javascript if (window.turnstile.isExpired(widgetId)) { window.turnstile.reset(widgetId); } ``` ## Callback Signatures ```typescript type TurnstileCallback = (token: string) => void; type ErrorCallback = (errorCode: string) => void; type TimeoutCallback = () => void; type ExpiredCallback = () => void; type BeforeInteractiveCallback = () => void; type AfterInteractiveCallback = () => void; type UnsupportedCallback = () => void; ``` ## Siteverify API (Server-Side) **Endpoint:** `https://challenges.cloudflare.com/turnstile/v0/siteverify` ### Request **Method:** POST **Content-Type:** `application/json` or `application/x-www-form-urlencoded` ```typescript interface SiteverifyRequest { secret: string; // Your secret key (never expose client-side) response: string; // Token from cf-turnstile-response remoteip?: string; // User's IP (optional but recommended) idempotency_key?: string; // Unique key for idempotent validation } ``` **Example:** ```javascript // Cloudflare Workers const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: env.TURNSTILE_SECRET, response: token, remoteip: request.headers.get('CF-Connecting-IP') }) }); const data = await result.json(); ``` ### Response ```typescript interface SiteverifyResponse { success: boolean; // Validation result challenge_ts?: string; // ISO timestamp of challenge hostname?: string; // Hostname where widget was solved 'error-codes'?: string[]; // Error codes if success=false action?: string; // Action name from widget config cdata?: string; // Custom data from widget config } ``` **Example Success:** ```json { "success": true, "challenge_ts": "2024-01-15T10:30:00Z", "hostname": "example.com", "action": "login", "cdata": "user123" } ``` **Example Failure:** ```json { "success": false, "error-codes": ["timeout-or-duplicate"] } ``` ## Error Codes | Code | Cause | Solution | |------|-------|----------| | `missing-input-secret` | Secret key not provided | Include `secret` in request | | `invalid-input-secret` | Secret key is wrong | Check secret key in dashboard | | `missing-input-response` | Token not provided | Include `response` token | | `invalid-input-response` | Token is invalid/malformed | Verify token from widget | | `timeout-or-duplicate` | Token expired (>5min) or reused | Generate new token, validate once | | `internal-error` | Cloudflare server error | Retry with exponential backoff | | `bad-request` | Malformed request | Check JSON/form encoding | ## TypeScript Types ```typescript interface TurnstileOptions { sitekey: string; action?: string; cData?: string; callback?: (token: string) => void; 'error-callback'?: (errorCode: string) => void; 'expired-callback'?: () => void; 'timeout-callback'?: () => void; 'before-interactive-callback'?: () => void; 'after-interactive-callback'?: () => void; 'unsupported-callback'?: () => void; theme?: 'light' | 'dark' | 'auto'; size?: 'normal' | 'compact' | 'flexible'; tabindex?: number; 'response-field'?: boolean; 'response-field-name'?: string; retry?: 'auto' | 'never'; 'retry-interval'?: number; language?: string; execution?: 'render' | 'execute'; appearance?: 'always' | 'execute' | 'interaction-only'; 'refresh-expired'?: 'auto' | 'manual' | 'never'; } interface Turnstile { render(container: string | HTMLElement, options: TurnstileOptions): string; reset(widgetId: string): void; remove(widgetId: string): void; getResponse(widgetId: string): string | undefined; isExpired(widgetId: string): boolean; execute(container?: string | HTMLElement, options?: TurnstileOptions): void; } declare global { interface Window { turnstile: Turnstile; onloadTurnstileCallback?: () => void; } } ``` ## Script Loading ```html ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/turnstile/configuration.md ================================================ # Configuration ## Script Loading ### Basic (Implicit Rendering) ```html ``` Automatically renders widgets with `class="cf-turnstile"` on page load. ### Explicit Rendering ```html ``` Manual control over when/where widgets render via `window.turnstile.render()`. ### With Load Callback ```html ``` ### Compatibility Mode ```html ``` Provides `grecaptcha` API for Google reCAPTCHA drop-in replacement. ## Widget Configuration ### Complete Options Object ```javascript { // Required sitekey: 'YOUR_SITE_KEY', // Widget sitekey from dashboard // Callbacks callback: (token) => {}, // Success - token ready 'error-callback': (code) => {}, // Error occurred 'expired-callback': () => {}, // Token expired (>5min) 'timeout-callback': () => {}, // Challenge timeout 'before-interactive-callback': () => {}, // Before showing checkbox 'after-interactive-callback': () => {}, // After user interacts 'unsupported-callback': () => {}, // Browser doesn't support Turnstile // Appearance theme: 'auto', // 'light' | 'dark' | 'auto' size: 'normal', // 'normal' | 'compact' | 'flexible' tabindex: 0, // Tab order (accessibility) language: 'auto', // ISO 639-1 code or 'auto' // Behavior execution: 'render', // 'render' (auto) | 'execute' (manual) appearance: 'always', // 'always' | 'execute' | 'interaction-only' retry: 'auto', // 'auto' | 'never' 'retry-interval': 8000, // Retry interval (ms), default 8000 'refresh-expired': 'auto', // 'auto' | 'manual' | 'never' // Form Integration 'response-field': true, // Add hidden input (default: true) 'response-field-name': 'cf-turnstile-response', // Hidden input name // Analytics & Data action: 'login', // Action name (for analytics) cData: 'user-session-123', // Custom data (returned in siteverify) } ``` ### Key Options Explained **`execution`:** - `'render'` (default): Challenge starts immediately on render - `'execute'`: Wait for `turnstile.execute()` call **`appearance`:** - `'always'` (default): Widget always visible - `'execute'`: Hidden until `execute()` called - `'interaction-only'`: Hidden until user interaction needed **`refresh-expired`:** - `'auto'` (default): Auto-refresh expired tokens - `'manual'`: App must call `reset()` after expiry - `'never'`: No refresh, expired-callback triggered **`retry`:** - `'auto'` (default): Auto-retry failed challenges - `'never'`: Don't retry, trigger error-callback ## HTML Data Attributes For implicit rendering, use data attributes on `
`: | JavaScript Property | HTML Data Attribute | Example | |---------------------|---------------------|---------| | `sitekey` | `data-sitekey` | `data-sitekey="YOUR_KEY"` | | `action` | `data-action` | `data-action="login"` | | `cData` | `data-cdata` | `data-cdata="session-123"` | | `callback` | `data-callback` | `data-callback="onSuccess"` | | `error-callback` | `data-error-callback` | `data-error-callback="onError"` | | `expired-callback` | `data-expired-callback` | `data-expired-callback="onExpired"` | | `timeout-callback` | `data-timeout-callback` | `data-timeout-callback="onTimeout"` | | `theme` | `data-theme` | `data-theme="dark"` | | `size` | `data-size` | `data-size="compact"` | | `tabindex` | `data-tabindex` | `data-tabindex="0"` | | `response-field` | `data-response-field` | `data-response-field="false"` | | `response-field-name` | `data-response-field-name` | `data-response-field-name="token"` | | `retry` | `data-retry` | `data-retry="never"` | | `retry-interval` | `data-retry-interval` | `data-retry-interval="5000"` | | `language` | `data-language` | `data-language="en"` | | `execution` | `data-execution` | `data-execution="execute"` | | `appearance` | `data-appearance` | `data-appearance="interaction-only"` | | `refresh-expired` | `data-refresh-expired` | `data-refresh-expired="manual"` | **Example:** ```html
``` ## Content Security Policy Add these directives to CSP header/meta tag: ``` script-src https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; ``` **Full Example:** ```html ``` ## Framework-Specific Setup ### React ```bash npm install @marsidev/react-turnstile ``` ```jsx import Turnstile from '@marsidev/react-turnstile'; console.log(token)} /> ``` ### Vue ```bash npm install vue-turnstile ``` ```vue ``` ### Svelte ```bash npm install svelte-turnstile ``` ```svelte ``` ### Next.js (App Router) ```tsx // app/components/TurnstileWidget.tsx 'use client'; import { useEffect, useRef } from 'react'; export default function TurnstileWidget({ sitekey, onSuccess }) { const ref = useRef(null); useEffect(() => { if (ref.current && window.turnstile) { const widgetId = window.turnstile.render(ref.current, { sitekey, callback: onSuccess }); return () => window.turnstile.remove(widgetId); } }, [sitekey, onSuccess]); return
; } ``` ## Cloudflare Pages Plugin ```bash npm install @cloudflare/pages-plugin-turnstile ``` ```typescript // functions/_middleware.ts import turnstilePlugin from '@cloudflare/pages-plugin-turnstile'; export const onRequest = turnstilePlugin({ secret: 'YOUR_SECRET_KEY', onError: () => new Response('CAPTCHA failed', { status: 403 }) }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/turnstile/gotchas.md ================================================ # Troubleshooting & Gotchas ## Critical Rules ### ❌ Skipping Server-Side Validation **Problem:** Client-only validation is easily bypassed. **Solution:** Always validate on server. ```javascript // CORRECT - Server validates token app.post('/submit', async (req, res) => { const token = req.body['cf-turnstile-response']; const validation = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: JSON.stringify({ secret: SECRET, response: token }) }).then(r => r.json()); if (!validation.success) return res.status(403).json({ error: 'CAPTCHA failed' }); }); ``` ### ❌ Exposing Secret Key **Problem:** Secret key leaked in client-side code. **Solution:** Server-side validation only. Never send secret to client. ### ❌ Reusing Tokens (Single-Use Rule) **Problem:** Tokens are single-use. Revalidation fails with `timeout-or-duplicate`. **Solution:** Generate new token for each submission. Reset widget on error. ```javascript if (!response.ok) window.turnstile.reset(widgetId); ``` ### ❌ Not Handling Token Expiry **Problem:** Tokens expire after 5 minutes. **Solution:** Handle expiry callback or use auto-refresh. ```javascript window.turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY', 'refresh-expired': 'auto', // or 'manual' with expired-callback 'expired-callback': () => window.turnstile.reset(widgetId) }); ``` ## Common Errors | Error | Cause | Solution | |-------|-------|----------| | **Widget not rendering** | Incorrect sitekey, CSP blocking, file:// protocol | Check sitekey, add CSP for challenges.cloudflare.com, use http:// | | **timeout-or-duplicate** | Token expired (>5min) or reused | Generate fresh token, don't cache >5min | | **invalid-input-secret** | Wrong secret key | Verify secret from dashboard, check env vars | | **missing-input-response** | Token not sent | Check form field name is 'cf-turnstile-response' | ## Framework Gotchas ### React: Widget Re-mounting **Problem:** Widget re-renders on state change, losing token. **Solution:** Control lifecycle with useRef. ```tsx function TurnstileWidget({ onToken }) { const containerRef = useRef(null); const widgetIdRef = useRef(null); useEffect(() => { if (containerRef.current && !widgetIdRef.current) { widgetIdRef.current = window.turnstile.render(containerRef.current, { sitekey: 'YOUR_SITE_KEY', callback: onToken }); } return () => { if (widgetIdRef.current) { window.turnstile.remove(widgetIdRef.current); widgetIdRef.current = null; } }; }, []); return
; } ``` ### React StrictMode: Double Render **Problem:** Widget renders twice in dev due to StrictMode. **Solution:** Use cleanup function. ```tsx useEffect(() => { const widgetId = window.turnstile.render('#container', { sitekey }); return () => window.turnstile.remove(widgetId); }, []); ``` ### Next.js: SSR Hydration **Problem:** `window.turnstile` undefined during SSR. **Solution:** Use `'use client'` or dynamic import with `ssr: false`. ```tsx 'use client'; export default function Turnstile() { /* component */ } ``` ### SPA: Navigation Without Cleanup **Problem:** Navigating leaves orphaned widgets. **Solution:** Remove widget in cleanup. ```javascript // Vue onBeforeUnmount(() => window.turnstile.remove(widgetId)); // React useEffect(() => () => window.turnstile.remove(widgetId), []); ``` ## Network & Security ### CSP Blocking **Problem:** Content Security Policy blocks script/iframe. **Solution:** Add CSP directives. ```html ``` ### IP Address Forwarding **Problem:** Server receives proxy IP instead of client IP. **Solution:** Use correct header. ```javascript // Cloudflare Workers const ip = request.headers.get('CF-Connecting-IP'); // Behind proxy const ip = request.headers.get('X-Forwarded-For')?.split(',')[0]; ``` ### CORS (Siteverify) **Problem:** CORS error calling siteverify from browser. **Solution:** Never call siteverify client-side. Call your backend, backend calls siteverify. ## Limits & Constraints | Limit | Value | Impact | |-------|-------|--------| | Token validity | 5 minutes | Must regenerate after expiry | | Token use | Single-use | Cannot revalidate same token | | Widget size | 300x65px (normal), 130x120px (compact) | Plan layout | ## Debugging ### Console Logging ```javascript window.turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY', callback: (token) => console.log('✓ Token:', token), 'error-callback': (code) => console.error('✗ Error:', code), 'expired-callback': () => console.warn('⏱ Expired'), 'timeout-callback': () => console.warn('⏱ Timeout') }); ``` ### Check Token State ```javascript const token = window.turnstile.getResponse(widgetId); console.log('Token:', token || 'NOT READY'); console.log('Expired:', window.turnstile.isExpired(widgetId)); ``` ### Test Keys (Use First) Always develop with test keys before production: - Site: `1x00000000000000000000AA` - Secret: `1x0000000000000000000000000000000AA` ### Network Tab - Verify `api.js` loads (200 OK) - Check siteverify request/response - Look for 4xx/5xx errors ## Misconfigurations ### Wrong Key Pairing **Problem:** Site key from one widget, secret from another. **Solution:** Verify site key and secret are from same widget in dashboard. ### Test Keys in Production **Problem:** Using test keys in production. **Solution:** Environment-based keys. ```javascript const SITE_KEY = process.env.NODE_ENV === 'production' ? process.env.TURNSTILE_SITE_KEY : '1x00000000000000000000AA'; ``` ### Missing Environment Variables **Problem:** Secret undefined on server. **Solution:** Check .env and verify loading. ```bash # .env TURNSTILE_SECRET=your_secret_here # Verify console.log('Secret loaded:', !!process.env.TURNSTILE_SECRET); ``` ## Reference - [Turnstile Docs](https://developers.cloudflare.com/turnstile/) - [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile) - [Error Codes](https://developers.cloudflare.com/turnstile/troubleshooting/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/turnstile/patterns.md ================================================ # Common Patterns ## Form Integration ### Basic Form (Implicit Rendering) ```html
``` ### Controlled Form (Explicit Rendering) ```javascript ``` ## Framework Patterns ### React ```tsx import { useState } from 'react'; import Turnstile from '@marsidev/react-turnstile'; export default function Form() { const [token, setToken] = useState(null); return (
{ e.preventDefault(); if (!token) return; await fetch('/api/submit', { method: 'POST', body: JSON.stringify({ 'cf-turnstile-response': token }) }); }}> ); } ``` ### Vue / Svelte ```vue token = e.detail.token} /> ``` ## Server Validation ### Cloudflare Workers ```typescript interface Env { TURNSTILE_SECRET: string; } export default { async fetch(request: Request, env: Env): Promise { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); } const formData = await request.formData(); const token = formData.get('cf-turnstile-response'); if (!token) { return new Response('Missing token', { status: 400 }); } // Validate token const ip = request.headers.get('CF-Connecting-IP'); const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: env.TURNSTILE_SECRET, response: token, remoteip: ip }) }); const validation = await result.json(); if (!validation.success) { return new Response('CAPTCHA validation failed', { status: 403 }); } // Process form... return new Response('Success'); } }; ``` ### Pages Functions ```typescript // functions/submit.ts - same pattern as Workers, use ctx.env and ctx.request export const onRequestPost: PagesFunction<{ TURNSTILE_SECRET: string }> = async (ctx) => { const token = (await ctx.request.formData()).get('cf-turnstile-response'); // Validate with ctx.env.TURNSTILE_SECRET (same as Workers pattern above) }; ``` ## Advanced Patterns ### Pre-Clearance (Invisible) ```html
``` ### Token Refresh on Expiry ```javascript let widgetId = window.turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY', 'refresh-expired': 'manual', 'expired-callback': () => { console.log('Token expired, refreshing...'); window.turnstile.reset(widgetId); } }); ``` ## Testing ### Environment-Based Keys ```javascript const SITE_KEY = process.env.NODE_ENV === 'production' ? 'YOUR_PRODUCTION_SITE_KEY' : '1x00000000000000000000AA'; // Always passes const SECRET_KEY = process.env.NODE_ENV === 'production' ? process.env.TURNSTILE_SECRET : '1x0000000000000000000000000000000AA'; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/vectorize/README.md ================================================ # Cloudflare Vectorize Globally distributed vector database for AI applications. Store and query vector embeddings for semantic search, recommendations, RAG, and classification. **Status:** Generally Available (GA) | **Last Updated:** 2026-01-27 ## Quick Start ```typescript // 1. Create index // npx wrangler vectorize create my-index --dimensions=768 --metric=cosine // 2. Configure binding (wrangler.jsonc) // { "vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }] } // 3. Query vectors const matches = await env.VECTORIZE.query(queryVector, { topK: 5 }); ``` ## Key Features - **10M vectors per index** (V2) - Dimensions up to 1536 (32-bit float) - Three distance metrics: cosine, euclidean, dot-product - Metadata filtering (up to 10 indexes) - Namespace support (50K namespaces paid, 1K free) - Seamless Workers AI integration - Global distribution ## Reading Order | Task | Files to Read | |------|---------------| | New to Vectorize | README only | | Implement feature | README + api + patterns | | Setup/configure | README + configuration | | Debug issues | gotchas | | Integrate with AI | README + patterns | | RAG implementation | README + patterns | ## File Guide - **README.md** (this file): Overview, quick decisions - **api.md**: Runtime API, types, operations (query/insert/upsert) - **configuration.md**: Setup, CLI, metadata indexes - **patterns.md**: RAG, Workers AI, OpenAI, LangChain, multi-tenant - **gotchas.md**: Limits, pitfalls, troubleshooting ## Distance Metric Selection Choose based on your use case: ``` What are you building? ├─ Text/semantic search → cosine (most common) ├─ Image similarity → euclidean ├─ Recommendation system → dot-product └─ Pre-normalized vectors → dot-product ``` | Metric | Best For | Score Interpretation | |--------|----------|---------------------| | `cosine` | Text embeddings, semantic similarity | Higher = closer (1.0 = identical) | | `euclidean` | Absolute distance, spatial data | Lower = closer (0.0 = identical) | | `dot-product` | Recommendations, normalized vectors | Higher = closer | **Note:** Index configuration is immutable. Cannot change dimensions or metric after creation. ## Multi-Tenancy Strategy ``` How many tenants? ├─ < 50K tenants → Use namespaces (recommended) │ ├─ Fastest (filter before vector search) │ └─ Strict isolation ├─ > 50K tenants → Use metadata filtering │ ├─ Slower (post-filter after vector search) │ └─ Requires metadata index └─ Per-tenant indexes → Only if compliance mandated └─ 50K index limit per account (paid plan) ``` ## Common Workflows ### Semantic Search ```typescript // 1. Generate embedding const result = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [query] }); // 2. Query Vectorize const matches = await env.VECTORIZE.query(result.data[0], { topK: 5, returnMetadata: "indexed" }); ``` ### RAG Pattern ```typescript // 1. Generate query embedding const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [query] }); // 2. Search Vectorize const matches = await env.VECTORIZE.query(embedding.data[0], { topK: 5 }); // 3. Fetch full documents from R2/D1/KV const docs = await Promise.all(matches.matches.map(m => env.R2.get(m.metadata.key).then(obj => obj?.text()) )); // 4. Generate LLM response with context const answer = await env.AI.run("@cf/meta/llama-3-8b-instruct", { prompt: `Context: ${docs.join("\n\n")}\n\nQuestion: ${query}\n\nAnswer:` }); ``` ## Critical Gotchas See `gotchas.md` for details. Most important: 1. **Async mutations**: Inserts take 5-10s to be queryable 2. **500 batch limit**: Workers API enforces 500 vectors per call (undocumented) 3. **Metadata truncation**: `"indexed"` returns first 64 bytes only 4. **topK with metadata**: Max 20 (not 100) when using returnValues or returnMetadata: "all" 5. **Metadata indexes first**: Must create before inserting vectors ## Resources - [Official Docs](https://developers.cloudflare.com/vectorize/) - [Client API Reference](https://developers.cloudflare.com/vectorize/reference/client-api/) - [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/#text-embeddings) - [Discord: #vectorize](https://discord.cloudflare.com) ================================================ FILE: skills/.curated/cloudflare-deploy/references/vectorize/api.md ================================================ # Vectorize API Reference ## Types ```typescript interface VectorizeVector { id: string; // Max 64 bytes values: number[]; // Must match index dimensions namespace?: string; // Optional partition (max 64 bytes) metadata?: Record; // Max 10 KiB } ``` ## Query ```typescript const matches = await env.VECTORIZE.query(queryVector, { topK: 10, // Max 100 (or 20 with returnValues/returnMetadata:"all") returnMetadata: "indexed", // "none" | "indexed" | "all" returnValues: false, namespace: "tenant-123", filter: { category: "docs" } }); // matches.matches[0] = { id, score, metadata? } ``` **returnMetadata:** `"none"` (fastest) → `"indexed"` (recommended) → `"all"` (topK max 20) **queryById (V2 only):** Search using existing vector as query. ```typescript await env.VECTORIZE.queryById("doc-123", { topK: 5 }); ``` ## Insert/Upsert ```typescript // Insert: ignores duplicates (keeps first) await env.VECTORIZE.insert([{ id, values, metadata }]); // Upsert: overwrites duplicates (keeps last) await env.VECTORIZE.upsert([{ id, values, metadata }]); ``` **Max 500 vectors per call.** Queryable after 5-10 seconds. ## Other Operations ```typescript // Get by IDs const vectors = await env.VECTORIZE.getByIds(["id1", "id2"]); // Delete (max 1000 IDs per call) await env.VECTORIZE.deleteByIds(["id1", "id2"]); // Index info const info = await env.VECTORIZE.describe(); // { dimensions, metric, vectorCount } ``` ## Filtering Requires metadata index. Filter operators: | Operator | Example | |----------|---------| | `$eq` (implicit) | `{ category: "docs" }` | | `$ne` | `{ status: { $ne: "deleted" } }` | | `$in` / `$nin` | `{ tag: { $in: ["sale"] } }` | | `$lt`, `$lte`, `$gt`, `$gte` | `{ price: { $lt: 100 } }` | **Constraints:** Max 2048 bytes, no dots/`$` in keys, values: string/number/boolean/null. ## Performance | Configuration | topK Limit | Speed | |--------------|------------|-------| | No metadata | 100 | Fastest | | `returnMetadata: "indexed"` | 100 | Fast | | `returnMetadata: "all"` | 20 | Slower | | `returnValues: true` | 20 | Slower | **Batch operations:** Always batch (500/call) for optimal throughput. ```typescript for (let i = 0; i < vectors.length; i += 500) { await env.VECTORIZE.upsert(vectors.slice(i, i + 500)); } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/vectorize/configuration.md ================================================ # Vectorize Configuration ## Create Index ```bash npx wrangler vectorize create my-index --dimensions=768 --metric=cosine ``` **⚠️ Dimensions and metric are immutable** - cannot change after creation. ## Worker Binding ```jsonc // wrangler.jsonc { "vectorize": [ { "binding": "VECTORIZE", "index_name": "my-index" } ] } ``` ```typescript interface Env { VECTORIZE: Vectorize; } ``` ## Metadata Indexes **Must create BEFORE inserting vectors** - existing vectors not retroactively indexed. ```bash wrangler vectorize create-metadata-index my-index --property-name=category --type=string wrangler vectorize create-metadata-index my-index --property-name=price --type=number ``` | Type | Use For | |------|---------| | `string` | Categories, tags (first 64 bytes indexed) | | `number` | Prices, timestamps | | `boolean` | Flags | ## CLI Commands ```bash # Index management wrangler vectorize list wrangler vectorize info wrangler vectorize delete # Vector operations wrangler vectorize insert --file=embeddings.ndjson wrangler vectorize get --ids=id1,id2 wrangler vectorize delete-by-ids --ids=id1,id2 # Metadata indexes wrangler vectorize list-metadata-index wrangler vectorize delete-metadata-index --property-name=field ``` ## Bulk Upload (NDJSON) ```json {"id": "1", "values": [0.1, 0.2, ...], "metadata": {"category": "docs"}} {"id": "2", "values": [0.4, 0.5, ...], "namespace": "tenant-abc"} ``` **Limits:** 5000 vectors per file, 100 MB max ## Cardinality Best Practice Bucket high-cardinality data: ```typescript // ❌ Millisecond timestamps metadata: { timestamp: Date.now() } // ✅ 5-minute buckets metadata: { timestamp_bucket: Math.floor(Date.now() / 300000) * 300000 } ``` ## Production Checklist 1. Create index with correct dimensions 2. Create metadata indexes FIRST 3. Test bulk upload 4. Configure bindings 5. Deploy Worker 6. Verify queries ================================================ FILE: skills/.curated/cloudflare-deploy/references/vectorize/gotchas.md ================================================ # Vectorize Gotchas ## Critical Warnings ### Async Mutations Insert/upsert/delete return immediately but vectors aren't queryable for 5-10 seconds. ### Batch Size Limit **Workers API: 500 vectors max per call** (undocumented, silently truncates) ```typescript // ✅ Chunk into 500 for (let i = 0; i < vectors.length; i += 500) { await env.VECTORIZE.upsert(vectors.slice(i, i + 500)); } ``` ### Metadata Truncation `returnMetadata: "indexed"` returns only first 64 bytes of strings. Use `"all"` for complete metadata (but max topK drops to 20). ### topK Limits | returnMetadata | returnValues | Max topK | |----------------|--------------|----------| | `"none"` / `"indexed"` | `false` | 100 | | `"all"` | any | **20** | | any | `true` | **20** | ### Metadata Indexes First Create BEFORE inserting - existing vectors not retroactively indexed. ```bash # ✅ Create index FIRST wrangler vectorize create-metadata-index my-index --property-name=category --type=string wrangler vectorize insert my-index --file=data.ndjson ``` ### Index Config Immutable Cannot change dimensions/metric after creation. Must create new index and migrate. ## Limits (V2) | Resource | Limit | |----------|-------| | Vectors per index | 10,000,000 | | Max dimensions | 1536 | | Batch upsert (Workers) | **500** | | Indexed string metadata | **64 bytes** | | Metadata indexes | 10 | | Namespaces | 50,000 (paid) / 1,000 (free) | ## Common Mistakes 1. **Wrong embedding shape:** Extract `result.data[0]` from Workers AI 2. **Metadata index after data:** Re-upsert all vectors 3. **Insert vs upsert:** `insert` ignores duplicates, `upsert` overwrites 4. **Not batching:** Individual inserts ~1K/min, batched ~200K+/min ## Troubleshooting **No results?** - Wait 5-10s after insert - Check namespace spelling (case-sensitive) - Verify metadata index exists - Check dimension mismatch **Metadata filter not working?** - Index must exist before data insert - Strings >64 bytes truncated - Use dot notation for nested: `"product.category"` ## Model Dimensions - `@cf/baai/bge-small-en-v1.5`: 384 - `@cf/baai/bge-base-en-v1.5`: 768 - `@cf/baai/bge-large-en-v1.5`: 1024 ================================================ FILE: skills/.curated/cloudflare-deploy/references/vectorize/patterns.md ================================================ # Vectorize Patterns ## Workers AI Integration ```typescript // Generate embedding + query const result = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [query] }); const matches = await env.VECTORIZE.query(result.data[0], { topK: 5 }); // Pass data[0]! ``` | Model | Dimensions | |-------|------------| | `@cf/baai/bge-small-en-v1.5` | 384 | | `@cf/baai/bge-base-en-v1.5` | 768 (recommended) | | `@cf/baai/bge-large-en-v1.5` | 1024 | ## OpenAI Integration ```typescript const response = await openai.embeddings.create({ model: "text-embedding-ada-002", input: query }); const matches = await env.VECTORIZE.query(response.data[0].embedding, { topK: 5 }); ``` ## RAG Pattern ```typescript // 1. Embed query const emb = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [query] }); // 2. Search vectors const matches = await env.VECTORIZE.query(emb.data[0], { topK: 5, returnMetadata: "indexed" }); // 3. Fetch full docs from R2/D1/KV const docs = await Promise.all(matches.matches.map(m => env.R2.get(m.metadata.key).then(o => o?.text()))); // 4. Generate with context const answer = await env.AI.run("@cf/meta/llama-3-8b-instruct", { prompt: `Context:\n${docs.filter(Boolean).join("\n\n")}\n\nQuestion: ${query}\n\nAnswer:` }); ``` ## Multi-Tenant ### Namespaces (< 50K tenants, fastest) ```typescript await env.VECTORIZE.upsert([{ id: "1", values: emb, namespace: `tenant-${id}` }]); await env.VECTORIZE.query(vec, { namespace: `tenant-${id}`, topK: 10 }); ``` ### Metadata Filter (> 50K tenants) ```bash wrangler vectorize create-metadata-index my-index --property-name=tenantId --type=string ``` ```typescript await env.VECTORIZE.upsert([{ id: "1", values: emb, metadata: { tenantId: id } }]); await env.VECTORIZE.query(vec, { filter: { tenantId: id }, topK: 10 }); ``` ## Hybrid Search ```typescript const matches = await env.VECTORIZE.query(vec, { topK: 20, filter: { category: { $in: ["tech", "science"] }, published: { $gte: lastMonthTimestamp } } }); ``` ## Batch Ingestion ```typescript const BATCH = 500; for (let i = 0; i < vectors.length; i += BATCH) { await env.VECTORIZE.upsert(vectors.slice(i, i + BATCH)); } ``` ## Best Practices 1. **Pass `data[0]`** not `data` or full response 2. **Batch 500** vectors per upsert 3. **Create metadata indexes** before inserting 4. **Use namespaces** for tenant isolation (faster than filters) 5. **`returnMetadata: "indexed"`** for best speed/data balance 6. **Handle 5-10s mutation delay** in async operations ================================================ FILE: skills/.curated/cloudflare-deploy/references/waf/README.md ================================================ # Cloudflare WAF Expert Skill Reference **Expertise**: Cloudflare Web Application Firewall (WAF) configuration, custom rules, managed rulesets, rate limiting, attack detection, and API integration ## Overview Cloudflare WAF protects web applications from attacks through managed rulesets and custom rules. **Detection (Managed Rulesets)** - Pre-configured rules maintained by Cloudflare - CVE-based rules, OWASP Top 10 coverage - Three main rulesets: Cloudflare Managed, OWASP CRS, Exposed Credentials - Actions: log, block, challenge, js_challenge, managed_challenge **Mitigation (Custom Rules & Rate Limiting)** - Custom expressions using Wirefilter syntax - Attack score-based blocking (`cf.waf.score`) - Rate limiting with per-IP, per-user, or custom characteristics - Actions: block, challenge, js_challenge, managed_challenge, log, skip ## Quick Start ### Deploy Cloudflare Managed Ruleset ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CF_API_TOKEN }); // Deploy managed ruleset to zone await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_request_firewall_managed', name: 'Deploy Cloudflare Managed Ruleset', rules: [{ action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', // Cloudflare Managed Ruleset }, expression: 'true', enabled: true, }], }); ``` ### Create Custom Rule ```typescript // Block requests with attack score >= 40 await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_request_firewall_custom', name: 'Custom WAF Rules', rules: [{ action: 'block', expression: 'cf.waf.score gt 40', description: 'Block high attack scores', enabled: true, }], }); ``` ### Create Rate Limit ```typescript await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_ratelimit', name: 'API Rate Limits', rules: [{ action: 'block', expression: 'http.request.uri.path eq "/api/login"', action_parameters: { ratelimit: { characteristics: ['cf.colo.id', 'ip.src'], period: 60, requests_per_period: 10, mitigation_timeout: 600, }, }, enabled: true, }], }); ``` ## Managed Ruleset Quick Reference | Ruleset Name | ID | Coverage | |--------------|----|---------| | Cloudflare Managed | `efb7b8c949ac4650a09736fc376e9aee` | OWASP Top 10, CVEs | | OWASP Core Ruleset | `4814384a9e5d4991b9815dcfc25d2f1f` | OWASP ModSecurity CRS | | Exposed Credentials Check | `c2e184081120413c86c3ab7e14069605` | Credential stuffing | ## Phases WAF rules execute in specific phases: - `http_request_firewall_managed` - Managed rulesets - `http_request_firewall_custom` - Custom rules - `http_ratelimit` - Rate limiting rules - `http_request_sbfm` - Super Bot Fight Mode (Pro+) ## Reading Order 1. **[api.md](api.md)** - SDK methods, expressions, actions, parameters 2. **[configuration.md](configuration.md)** - Setup with Wrangler, Terraform, Pulumi 3. **[patterns.md](patterns.md)** - Common patterns: deploy managed, rate limiting, skip, override 4. **[gotchas.md](gotchas.md)** - Execution order, limits, expression errors ## See Also - [Cloudflare WAF Docs](https://developers.cloudflare.com/waf/) - [Ruleset Engine](https://developers.cloudflare.com/ruleset-engine/) - [Expression Reference](https://developers.cloudflare.com/ruleset-engine/rules-language/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/waf/api.md ================================================ # API Reference ## SDK Setup ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CF_API_TOKEN, }); ``` ## Core Methods ```typescript // List rulesets await client.rulesets.list({ zone_id: 'zone_id', phase: 'http_request_firewall_managed' }); // Get ruleset await client.rulesets.get({ zone_id: 'zone_id', ruleset_id: 'ruleset_id' }); // Create ruleset await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_request_firewall_custom', name: 'Custom WAF Rules', rules: [{ action: 'block', expression: 'cf.waf.score gt 40', enabled: true }], }); // Update ruleset (include rule id to keep existing, omit id for new rules) await client.rulesets.update({ zone_id: 'zone_id', ruleset_id: 'ruleset_id', rules: [ { id: 'rule_id', action: 'block', expression: 'cf.waf.score gt 40', enabled: true }, { action: 'challenge', expression: 'http.request.uri.path contains "/admin"', enabled: true }, ], }); // Delete ruleset await client.rulesets.delete({ zone_id: 'zone_id', ruleset_id: 'ruleset_id' }); ``` ## Actions & Phases ### Actions by Phase | Action | Custom | Managed | Rate Limit | Description | |--------|--------|---------|------------|-------------| | `block` | ✅ | ❌ | ✅ | Block request with 403 | | `challenge` | ✅ | ❌ | ✅ | Show CAPTCHA challenge | | `js_challenge` | ✅ | ❌ | ✅ | JS-based challenge | | `managed_challenge` | ✅ | ❌ | ✅ | Smart challenge (recommended) | | `log` | ✅ | ❌ | ✅ | Log only, don't block | | `skip` | ✅ | ❌ | ❌ | Skip rule evaluation | | `execute` | ❌ | ✅ | ❌ | Deploy managed ruleset | ### Phases (Execution Order) 1. `http_request_firewall_custom` - Custom rules (first line of defense) 2. `http_request_firewall_managed` - Managed rulesets (pre-configured protection) 3. `http_ratelimit` - Rate limiting (request throttling) 4. `http_request_sbfm` - Super Bot Fight Mode (Pro+ only) ## Expression Syntax ### Fields ```typescript // Request properties http.request.method // GET, POST, etc. http.request.uri.path // /api/users http.host // example.com // IP and Geolocation ip.src // 192.0.2.1 ip.geoip.country // US, GB, etc. ip.geoip.continent // NA, EU, etc. // Attack detection cf.waf.score // 0-100 attack score cf.waf.score.sqli // SQL injection score cf.waf.score.xss // XSS score // Headers & Cookies http.request.headers["authorization"][0] http.request.cookies["session"][0] lower(http.user_agent) // Lowercase user agent ``` ### Operators ```typescript // Comparison eq // Equal ne // Not equal lt // Less than le // Less than or equal gt // Greater than ge // Greater than or equal // String matching contains // Substring match matches // Regex match (use carefully) starts_with // Prefix match ends_with // Suffix match // List operations in // Value in list not // Logical NOT and // Logical AND or // Logical OR ``` ### Expression Examples ```typescript 'cf.waf.score gt 40' // Attack score 'http.request.uri.path eq "/api/login" and http.request.method eq "POST"' // Path + method 'ip.src in {192.0.2.0/24 203.0.113.0/24}' // IP blocking 'ip.geoip.country in {"CN" "RU" "KP"}' // Country blocking 'http.user_agent contains "bot"' // User agent 'not http.request.headers["authorization"][0]' // Header check '(cf.waf.score.sqli gt 20 or cf.waf.score.xss gt 20) and http.request.uri.path starts_with "/api"' // Complex ``` ## Rate Limiting Configuration ```typescript { action: 'block', expression: 'http.request.uri.path starts_with "/api"', action_parameters: { ratelimit: { // Characteristics define uniqueness: 'ip.src', 'cf.colo.id', // 'http.request.headers["key"][0]', 'http.request.cookies["session"][0]' characteristics: ['cf.colo.id', 'ip.src'], // Recommended: per-IP per-datacenter period: 60, // Time window in seconds requests_per_period: 100, // Max requests in period mitigation_timeout: 600, // Block duration in seconds counting_expression: 'http.request.method ne "GET"', // Optional: filter counted requests requests_to_origin: false, // Count all requests (not just origin hits) }, }, enabled: true, } ``` ## Managed Ruleset Deployment ```typescript { action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', // Cloudflare Managed overrides: { // Override specific rules rules: [ { id: '5de7edfa648c4d6891dc3e7f84534ffa', action: 'log', enabled: true }, ], // Override categories: 'wordpress', 'sqli', 'xss', 'rce', etc. categories: [ { category: 'wordpress', enabled: false }, { category: 'sqli', action: 'log' }, ], }, }, expression: 'true', enabled: true, } ``` ## Skip Rules Skip rules bypass subsequent rule evaluation. Two skip types: **Skip current ruleset**: Skip remaining rules in current phase only ```typescript { action: 'skip', action_parameters: { ruleset: 'current', // Skip rest of current ruleset }, expression: 'http.request.uri.path ends_with ".jpg" or http.request.uri.path ends_with ".css"', enabled: true, } ``` **Skip entire phases**: Skip one or more phases completely ```typescript { action: 'skip', action_parameters: { phases: ['http_request_firewall_managed', 'http_ratelimit'], // Skip multiple phases }, expression: 'ip.src in {192.0.2.0/24 203.0.113.0/24}', enabled: true, } ``` **Note**: Skip rules in custom phase can skip managed/ratelimit phases, but not vice versa (execution order). ================================================ FILE: skills/.curated/cloudflare-deploy/references/waf/configuration.md ================================================ # Configuration ## Prerequisites **API Token**: Create at https://dash.cloudflare.com/profile/api-tokens - Permission: `Zone.WAF Edit` or `Zone.Firewall Services Edit` - Zone Resources: Include specific zones or all zones **Zone ID**: Found in dashboard > Overview > API section (right sidebar) ```bash # Set environment variables export CF_API_TOKEN="your_api_token_here" export ZONE_ID="your_zone_id_here" ``` ## TypeScript SDK Usage ```bash npm install cloudflare ``` ```typescript import Cloudflare from 'cloudflare'; const client = new Cloudflare({ apiToken: process.env.CF_API_TOKEN }); // Custom rules await client.rulesets.create({ zone_id: process.env.ZONE_ID, kind: 'zone', phase: 'http_request_firewall_custom', name: 'Custom WAF', rules: [ { action: 'block', expression: 'cf.waf.score gt 50', enabled: true }, { action: 'challenge', expression: 'http.request.uri.path eq "/admin"', enabled: true }, ], }); // Managed ruleset await client.rulesets.create({ zone_id: process.env.ZONE_ID, phase: 'http_request_firewall_managed', rules: [{ action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee' }, expression: 'true', }], }); // Rate limiting await client.rulesets.create({ zone_id: process.env.ZONE_ID, phase: 'http_ratelimit', rules: [{ action: 'block', expression: 'http.request.uri.path starts_with "/api"', action_parameters: { ratelimit: { characteristics: ['cf.colo.id', 'ip.src'], period: 60, requests_per_period: 100, mitigation_timeout: 600, }, }, }], }); ``` ## Terraform Configuration ```hcl provider "cloudflare" { api_token = var.cloudflare_api_token } resource "cloudflare_ruleset" "waf_custom" { zone_id = var.zone_id kind = "zone" phase = "http_request_firewall_custom" rules { action = "block" expression = "cf.waf.score gt 50" } } ``` **Managed Ruleset & Rate Limiting**: ```hcl resource "cloudflare_ruleset" "waf_managed" { zone_id = var.zone_id name = "Managed Ruleset" kind = "zone" phase = "http_request_firewall_managed" rules { action = "execute" action_parameters { id = "efb7b8c949ac4650a09736fc376e9aee" overrides { rules { id = "5de7edfa648c4d6891dc3e7f84534ffa" action = "log" } } } expression = "true" } } resource "cloudflare_ruleset" "rate_limiting" { zone_id = var.zone_id phase = "http_ratelimit" rules { action = "block" expression = "http.request.uri.path starts_with \"/api\"" ratelimit { characteristics = ["cf.colo.id", "ip.src"] period = 60 requests_per_period = 100 mitigation_timeout = 600 } } } ``` ## Pulumi Configuration ```typescript import * as cloudflare from '@pulumi/cloudflare'; const zoneId = 'zone_id'; // Custom rules const wafCustom = new cloudflare.Ruleset('waf-custom', { zoneId, phase: 'http_request_firewall_custom', rules: [ { action: 'block', expression: 'cf.waf.score gt 50', enabled: true }, { action: 'challenge', expression: 'http.request.uri.path eq "/admin"', enabled: true }, ], }); // Managed ruleset const wafManaged = new cloudflare.Ruleset('waf-managed', { zoneId, phase: 'http_request_firewall_managed', rules: [{ action: 'execute', actionParameters: { id: 'efb7b8c949ac4650a09736fc376e9aee' }, expression: 'true', }], }); // Rate limiting const rateLimiting = new cloudflare.Ruleset('rate-limiting', { zoneId, phase: 'http_ratelimit', rules: [{ action: 'block', expression: 'http.request.uri.path starts_with "/api"', ratelimit: { characteristics: ['cf.colo.id', 'ip.src'], period: 60, requestsPerPeriod: 100, mitigationTimeout: 600, }, }], }); ``` ## Dashboard Configuration 1. Navigate to: **Security** > **WAF** 2. Select tab: - **Managed rules** - Deploy/configure managed rulesets - **Custom rules** - Create custom rules - **Rate limiting rules** - Configure rate limits 3. Click **Deploy** or **Create rule** **Testing**: Use Security Events to test expressions before deploying. ## Wrangler Integration WAF configuration is zone-level (not Worker-specific). Configuration methods: - Dashboard UI - Cloudflare API via SDK - Terraform/Pulumi (IaC) **Workers benefit from WAF automatically** - no Worker code changes needed. **Example: Query WAF API from Worker**: ```typescript export default { async fetch(request: Request, env: Env): Promise { return fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/rulesets`, { headers: { 'Authorization': `Bearer ${env.CF_API_TOKEN}` }, }); }, }; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/waf/gotchas.md ================================================ # Gotchas & Troubleshooting ## Execution Order **Problem:** Rules execute in unexpected order **Cause:** Misunderstanding phase execution **Solution:** Phases execute sequentially (can't be changed): 1. `http_request_firewall_custom` - Custom rules 2. `http_request_firewall_managed` - Managed rulesets 3. `http_ratelimit` - Rate limiting Within phase: top-to-bottom, first match wins (unless `skip`) ```typescript // WRONG: Can't mix phase-specific actions await client.rulesets.create({ phase: 'http_request_firewall_custom', rules: [ { action: 'block', expression: 'cf.waf.score gt 50' }, { action: 'execute', action_parameters: { id: 'managed_id' } }, // WRONG ], }); // CORRECT: Separate rulesets per phase await client.rulesets.create({ phase: 'http_request_firewall_custom', rules: [...] }); await client.rulesets.create({ phase: 'http_request_firewall_managed', rules: [...] }); ``` ## Expression Errors **Problem:** Syntax errors prevent deployment **Cause:** Invalid field/operator/syntax **Solution:** ```typescript // Common mistakes 'http.request.path' → 'http.request.uri.path' // Correct field 'ip.geoip.country eq US' → 'ip.geoip.country eq "US"' // Quote strings 'http.user_agent eq "Mozilla"' → 'lower(http.user_agent) contains "mozilla"' // Case sensitivity 'matches ".*[.jpg"' → 'matches ".*\\.jpg$"' // Valid regex ``` Test expressions in Security Events before deploying. ## Skip Rule Pitfalls **Problem:** Skip rules don't work as expected **Cause:** Misunderstanding skip scope **Solution:** Skip types: - `ruleset: 'current'` - Skip remaining rules in current ruleset only - `phases: ['phase_name']` - Skip entire phases ```typescript // WRONG: Trying to skip managed rules from custom phase // In http_request_firewall_custom: { action: 'skip', action_parameters: { ruleset: 'current' }, expression: 'ip.src in {192.0.2.0/24}', } // This only skips remaining custom rules, not managed rules // CORRECT: Skip specific phases { action: 'skip', action_parameters: { phases: ['http_request_firewall_managed', 'http_ratelimit'], }, expression: 'ip.src in {192.0.2.0/24}', } ``` ## Update Replaces All Rules **Problem:** Updating ruleset deletes other rules **Cause:** `update()` replaces entire rule list **Solution:** ```typescript // WRONG: This deletes all existing rules! await client.rulesets.update({ zone_id: 'zone_id', ruleset_id: 'ruleset_id', rules: [{ action: 'block', expression: 'cf.waf.score gt 50' }], }); // CORRECT: Get existing rules first const ruleset = await client.rulesets.get({ zone_id: 'zone_id', ruleset_id: 'ruleset_id' }); await client.rulesets.update({ zone_id: 'zone_id', ruleset_id: 'ruleset_id', rules: [...ruleset.rules, { action: 'block', expression: 'cf.waf.score gt 50' }], }); ``` ## Override Conflicts **Problem:** Managed ruleset overrides don't apply **Cause:** Rule ID doesn't exist or category name incorrect **Solution:** ```typescript // List managed ruleset rules to find IDs const ruleset = await client.rulesets.get({ zone_id: 'zone_id', ruleset_id: 'efb7b8c949ac4650a09736fc376e9aee', }); console.log(ruleset.rules.map(r => ({ id: r.id, description: r.description }))); // Use correct IDs in overrides { action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', overrides: { rules: [{ id: '5de7edfa648c4d6891dc3e7f84534ffa', action: 'log' }] } } } ``` ## False Positives **Problem:** Legitimate traffic blocked **Cause:** Aggressive rules/thresholds **Solution:** 1. Start with log mode: `overrides: { action: 'log' }` 2. Review Security Events to identify false positives 3. Override specific rules: `overrides: { rules: [{ id: 'rule_id', action: 'log' }] }` ## Rate Limiting NAT Issues **Problem:** Users behind NAT hit rate limits too quickly **Cause:** Multiple users sharing single IP **Solution:** Add more characteristics: User-Agent, session cookie, or authorization header ```typescript { action: 'block', expression: 'http.request.uri.path starts_with "/api"', action_parameters: { ratelimit: { characteristics: ['cf.colo.id', 'ip.src', 'http.request.cookies["session"][0]'], period: 60, requests_per_period: 100, }, }, } ``` ## Performance Issues **Problem:** Increased latency **Cause:** Complex expressions, excessive rules **Solution:** 1. Skip static assets early: `action: 'skip'` for `\\.(jpg|css|js)$` 2. Path-based deployment: Only run managed on `/api` or `/admin` 3. Disable unused categories: `{ category: 'wordpress', enabled: false }` 4. Prefer string operators over regex: `starts_with` vs `matches` ## Limits & Quotas | Resource | Free | Pro | Business | Enterprise | |----------|------|-----|----------|------------| | Custom rules | 5 | 20 | 100 | 1000 | | Rate limiting rules | 1 | 10 | 25 | 100 | | Rule expression length | 4096 chars | 4096 chars | 4096 chars | 4096 chars | | Rules per ruleset | 75 | 75 | 400 | 1000 | | Managed rulesets | Yes | Yes | Yes | Yes | | Rate limit characteristics | 2 | 3 | 5 | 5 | **Important Notes:** - Rules execute in order; first match wins (except skip rules) - Expression evaluation stops at first `false` in AND chains - `matches` regex operator is slower than string operators - Rate limit counting happens before mitigation ## API Errors **Problem:** API calls fail with cryptic errors **Cause:** Invalid parameters or permissions **Solution:** ```typescript // Error: "Invalid phase" → Use exact phase name phase: 'http_request_firewall_custom' // Error: "Ruleset already exists" → Use update() or list first const rulesets = await client.rulesets.list({ zone_id, phase: 'http_request_firewall_custom' }); if (rulesets.result.length > 0) { await client.rulesets.update({ zone_id, ruleset_id: rulesets.result[0].id, rules: [...] }); } // Error: "Action not supported" → Check phase/action compatibility // 'execute' only in http_request_firewall_managed // Rate limit config only in http_ratelimit phase // Error: "Expression parse error" → Common fixes: 'ip.geoip.country eq "US"' // Quote strings 'cf.waf.score gt 40' // Use 'gt' not '>' 'http.request.uri.path' // Not 'http.request.path' ``` **Tip**: Test expressions in dashboard Security Events before deploying. ================================================ FILE: skills/.curated/cloudflare-deploy/references/waf/patterns.md ================================================ # Common Patterns ## Deploy Managed Rulesets ```typescript // Deploy Cloudflare Managed Ruleset (default) await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_request_firewall_managed', name: 'Cloudflare Managed Ruleset', rules: [{ action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', // Cloudflare Managed // Or: '4814384a9e5d4991b9815dcfc25d2f1f' for OWASP CRS // Or: 'c2e184081120413c86c3ab7e14069605' for Exposed Credentials }, expression: 'true', // All requests // Or: 'http.request.uri.path starts_with "/api"' for specific paths enabled: true, }], }); ``` ## Override Managed Ruleset ```typescript await client.rulesets.create({ zone_id: 'zone_id', phase: 'http_request_firewall_managed', rules: [{ action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', overrides: { // Override specific rules rules: [ { id: '5de7edfa648c4d6891dc3e7f84534ffa', action: 'log' }, { id: '75a0060762034b9dad4e883afc121b4c', enabled: false }, ], // Override categories: wordpress, sqli, xss, rce, etc. categories: [ { category: 'wordpress', enabled: false }, { category: 'sqli', action: 'log' }, ], }, }, expression: 'true', }], }); ``` ## Custom Rules ```typescript await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_request_firewall_custom', name: 'Custom WAF Rules', rules: [ // Attack score-based { action: 'block', expression: 'cf.waf.score gt 50', enabled: true }, { action: 'challenge', expression: 'cf.waf.score gt 20', enabled: true }, // Specific attack types { action: 'block', expression: 'cf.waf.score.sqli gt 30 or cf.waf.score.xss gt 30', enabled: true }, // Geographic blocking { action: 'block', expression: 'ip.geoip.country in {"CN" "RU"}', enabled: true }, ], }); ``` ## Rate Limiting ```typescript await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_ratelimit', name: 'Rate Limits', rules: [ // Per-IP global limit { action: 'block', expression: 'true', action_parameters: { ratelimit: { characteristics: ['cf.colo.id', 'ip.src'], period: 60, requests_per_period: 100, mitigation_timeout: 600, }, }, }, // Login endpoint (stricter) { action: 'block', expression: 'http.request.uri.path eq "/api/login"', action_parameters: { ratelimit: { characteristics: ['ip.src'], period: 60, requests_per_period: 5, mitigation_timeout: 600, }, }, }, // API writes only (using counting_expression) { action: 'block', expression: 'http.request.uri.path starts_with "/api"', action_parameters: { ratelimit: { characteristics: ['cf.colo.id', 'ip.src'], period: 60, requests_per_period: 50, counting_expression: 'http.request.method ne "GET"', }, }, }, ], }); ``` ## Skip Rules ```typescript await client.rulesets.create({ zone_id: 'zone_id', kind: 'zone', phase: 'http_request_firewall_custom', name: 'Skip Rules', rules: [ // Skip static assets (current ruleset only) { action: 'skip', action_parameters: { ruleset: 'current' }, expression: 'http.request.uri.path matches "\\.(jpg|css|js|woff2?)$"', }, // Skip all WAF phases for trusted IPs { action: 'skip', action_parameters: { phases: ['http_request_firewall_managed', 'http_ratelimit'], }, expression: 'ip.src in {192.0.2.0/24}', }, ], }); ``` ## Complete Setup Example Combine all three phases for comprehensive protection: ```typescript const client = new Cloudflare({ apiToken: process.env.CF_API_TOKEN }); const zoneId = process.env.ZONE_ID; // 1. Custom rules (execute first) await client.rulesets.create({ zone_id: zoneId, phase: 'http_request_firewall_custom', rules: [ { action: 'skip', action_parameters: { phases: ['http_request_firewall_managed', 'http_ratelimit'] }, expression: 'ip.src in {192.0.2.0/24}' }, { action: 'block', expression: 'cf.waf.score gt 50' }, { action: 'managed_challenge', expression: 'cf.waf.score gt 20' }, ], }); // 2. Managed ruleset (execute second) await client.rulesets.create({ zone_id: zoneId, phase: 'http_request_firewall_managed', rules: [{ action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', overrides: { categories: [{ category: 'wordpress', enabled: false }] } }, expression: 'true', }], }); // 3. Rate limiting (execute third) await client.rulesets.create({ zone_id: zoneId, phase: 'http_ratelimit', rules: [ { action: 'block', expression: 'true', action_parameters: { ratelimit: { characteristics: ['cf.colo.id', 'ip.src'], period: 60, requests_per_period: 100, mitigation_timeout: 600 } } }, { action: 'block', expression: 'http.request.uri.path eq "/api/login"', action_parameters: { ratelimit: { characteristics: ['ip.src'], period: 60, requests_per_period: 5, mitigation_timeout: 600 } } }, ], }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/web-analytics/README.md ================================================ # Cloudflare Web Analytics Privacy-first web analytics providing Core Web Vitals, traffic metrics, and user insights without compromising visitor privacy. ## Overview Cloudflare Web Analytics provides: - **Core Web Vitals** - LCP, FID, CLS, INP, TTFB monitoring - **Page views & visits** - Traffic patterns without cookies - **Referrers & paths** - Traffic sources and popular pages - **Device & browser data** - User agent breakdown - **Geographic data** - Country-level visitor distribution - **Privacy-first** - No cookies, fingerprinting, or PII collection - **Free** - No cost, unlimited pageviews **Important:** Web Analytics is **dashboard-only**. No API exists for programmatic data access. ## Quick Start Decision Tree ``` Is your site proxied through Cloudflare? ├─ YES → Use automatic injection (configuration.md) │ ├─ Enable auto-injection in dashboard │ └─ No code changes needed (unless Cache-Control: no-transform) │ └─ NO → Use manual beacon integration (integration.md) ├─ Add JS snippet to HTML ├─ Use spa: true for React/Vue/Next.js └─ Configure CSP if needed ``` ## Reading Order 1. **[configuration.md](configuration.md)** - Setup for proxied vs non-proxied sites 2. **[integration.md](integration.md)** - Framework-specific beacon integration (React, Next.js, Vue, Nuxt, etc.) 3. **[patterns.md](patterns.md)** - Common use cases (performance monitoring, GDPR consent, multi-site tracking) 4. **[gotchas.md](gotchas.md)** - Troubleshooting (SPA tracking, CSP issues, hash routing limitations) ## When to Use Each File - **Setting up for first time?** → Start with configuration.md - **Using React/Next.js/Vue/Nuxt?** → Go to integration.md for framework code - **Need GDPR consent loading?** → See patterns.md - **Beacon not loading or no data?** → Check gotchas.md - **SPA not tracking navigation?** → See integration.md for `spa: true` config ## Key Concepts ### Proxied vs Non-Proxied Sites | Type | Description | Beacon Injection | Limit | |------|-------------|------------------|-------| | **Proxied** | DNS through Cloudflare (orange cloud) | Automatic or manual | Unlimited | | **Non-proxied** | External hosting, manual beacon | Manual only | 10 sites max | ### SPA Mode **Critical for modern frameworks:** ```json {"token": "YOUR_TOKEN", "spa": true} ``` Without `spa: true`, client-side navigation (React Router, Vue Router, Next.js routing) will NOT be tracked. Only initial page loads will register. ### CSP Requirements If using Content Security Policy, allow both domains: ``` script-src https://static.cloudflareinsights.com https://cloudflareinsights.com; ``` ## Features ### Core Web Vitals Debugging - **LCP (Largest Contentful Paint)** - Identifies slow-loading hero images/elements - **FID (First Input Delay)** - Interaction responsiveness (legacy metric) - **INP (Interaction to Next Paint)** - Modern interaction responsiveness metric - **CLS (Cumulative Layout Shift)** - Visual stability issues - **TTFB (Time to First Byte)** - Server response performance Dashboard shows top 5 problematic elements with CSS selectors for debugging. ### Traffic Filters - **Bot filtering** - Exclude automated traffic from metrics - **Date ranges** - Custom time period analysis - **Geographic** - Country-level filtering - **Device type** - Desktop, mobile, tablet breakdown - **Browser/OS** - User agent filtering ### Rules (Advanced - Plan-dependent) Create custom tracking rules for advanced configurations: **Sample Rate Rules:** - Reduce data collection percentage for high-traffic sites - Example: Track only 50% of visitors to reduce volume **Path-Based Rules:** - Different behavior per route - Example: Exclude `/admin/*` or `/internal/*` from tracking **Host-Based Rules:** - Multi-domain configurations - Example: Separate tracking for staging vs production subdomains **Availability:** Rules feature depends on your Cloudflare plan. Check dashboard under Web Analytics → Rules to see if available. Free plans may have limited or no access. ## Plan Limits | Feature | Free | Notes | |---------|------|-------| | Proxied sites | Unlimited | DNS through Cloudflare | | Non-proxied sites | 10 | External hosting | | Pageviews | Unlimited | No volume limits | | Data retention | 6 months | Rolling window | | Rules | Plan-dependent | Check dashboard | ## Privacy & Compliance - **No cookies** - Zero client-side storage - **No fingerprinting** - No tracking across sites - **No PII** - IP addresses not stored - **GDPR-friendly** - Minimal data collection - **CCPA-compliant** - No personal data sale **EU opt-out:** Dashboard option to exclude EU visitor data entirely. ## Limitations - **Dashboard-only** - No API for programmatic access - **No real-time** - 5-10 minute data delay - **No custom events** - Automatic pageview/navigation tracking only - **History API only** - Hash-based routing (`#/path`) not supported - **No session replay** - Metrics only, no user recordings - **No form tracking** - Page navigation tracking only ## See Also - [Cloudflare Web Analytics Docs](https://developers.cloudflare.com/analytics/web-analytics/) - [Core Web Vitals Guide](https://web.dev/vitals/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/web-analytics/configuration.md ================================================ # Configuration ## Setup Methods ### Proxied Sites (Automatic) Dashboard → Web Analytics → Add site → Select hostname → Done | Injection Option | Description | |------------------|-------------| | Enable | Auto-inject for all visitors (default) | | Enable, excluding EU | No injection for EU (GDPR) | | Enable with manual snippet | You add beacon manually | | Disable | Pause tracking | **Fails if response has:** `Cache-Control: public, no-transform` **CSP required:** ``` script-src https://static.cloudflareinsights.com https://cloudflareinsights.com; ``` ### Non-Proxied Sites (Manual) Dashboard → Web Analytics → Add site → Enter hostname → Copy snippet ```html ``` **Limits:** 10 non-proxied sites per account ## SPA Mode **Enable `spa: true` for:** React Router, Next.js, Vue Router, Nuxt, SvelteKit, Angular **Keep `spa: false` for:** Traditional multi-page apps, static sites, WordPress **Hash routing (`#/path`) NOT supported** - use History API routing. ## Token Management - Found in: Dashboard → Web Analytics → Manage site - **Not secrets** - domain-locked, safe to expose in HTML - Each site gets unique token ## Environment Config ```typescript // Only load in production if (process.env.NODE_ENV === 'production') { // Load beacon } ``` Or use environment-specific tokens via env vars. ## Verify Installation 1. DevTools Network → filter `cloudflareinsights` → see `beacon.min.js` + data request 2. No CSP/CORS errors in console 3. Dashboard shows pageviews after 5-10 min delay ## Rules (Plan-dependent) Configure in dashboard for: - **Sample rate** - reduce collection % for high-traffic - **Path-based** - different behavior per route - **Host-based** - separate tracking per domain ## Data Retention - 6 months rolling window - 1-hour bucket granularity - No raw export, dashboard only ================================================ FILE: skills/.curated/cloudflare-deploy/references/web-analytics/gotchas.md ================================================ # Web Analytics Gotchas ## Critical Issues ### SPA Navigation Not Tracked **Symptom:** Only initial pageload counted **Fix:** Add `spa: true`: ```html ``` ### CSP Blocking Beacon **Symptom:** Console error "Refused to load script" **Fix:** Allow both domains: ``` script-src 'self' https://static.cloudflareinsights.com https://cloudflareinsights.com; ``` ### Hash-Based Routing Unsupported **Symptom:** `#/path` URLs not tracked **Fix:** Migrate to History API (`BrowserRouter`, not `HashRouter`). No workaround for hash routing. ### No Data Appearing **Causes & Fixes:** 1. **Delay** - Wait 5-15 minutes 2. **Wrong token** - Verify matches dashboard exactly 3. **Script blocked** - Check DevTools Network tab for beacon.min.js 4. **Domain mismatch** - Dashboard site must match actual URL ### Auto-Injection Fails **Cause:** `Cache-Control: no-transform` header **Fix:** Remove `no-transform` or install beacon manually ### Duplicate Pageviews **Cause:** Multiple beacon scripts **Fix:** Keep only one beacon per page ## Configuration Issues | Issue | Fix | |-------|-----| | 10-site limit reached | Delete old sites or proxy through CF (unlimited) | | Token not recognized | Use exact alphanumeric token from dashboard | ## Framework-Specific ### Next.js Hydration Warning ```tsx ``` Place before closing `` tag. ## Framework Examples | Framework | Location | Notes | |-----------|----------|-------| | React/Vite | `public/index.html` | Add `spa: true` | | Next.js App Router | `app/layout.tsx` | Use ` ``` Without `spa: true`: only initial pageload tracked. ## Staging/Production Separation ```typescript // Use env-specific tokens const token = process.env.NEXT_PUBLIC_CF_ANALYTICS_TOKEN; // .env.production: production token // .env.staging: staging token (or empty to disable) ``` ## Bot Filtering Dashboard → Filters → "Exclude Bot Traffic" Filters: Search crawlers, monitoring services, known bots. Not filtered: Headless browsers (Playwright/Puppeteer). ## Ad-Blocker Impact ~25-40% of users may block `cloudflareinsights.com`. No official workaround. Dashboard shows minimum baseline; use server logs for complete picture. ## Limitations - No UTM parameter tracking - No webhooks/alerts/API - No custom beacon domains - Max 10 non-proxied sites ================================================ FILE: skills/.curated/cloudflare-deploy/references/workerd/README.md ================================================ # Workerd Runtime V8-based JS/Wasm runtime powering Cloudflare Workers. Use as app server, dev tool, or HTTP proxy. ## ⚠️ IMPORTANT SECURITY NOTICE **workerd is NOT a hardened sandbox.** Do not run untrusted code. It's designed for deploying YOUR code locally/self-hosted, not multi-tenant SaaS. Cloudflare production adds security layers not present in open-source workerd. ## Decision Tree: When to Use What **95% of users:** Use Wrangler - Local development: `wrangler dev` (uses workerd internally) - Deployment: `wrangler deploy` (deploys to Cloudflare) - Types: `wrangler types` (generates TypeScript types) **Use raw workerd directly only if:** - Self-hosting Workers runtime in production - Embedding runtime in C++ application - Custom tooling/testing infrastructure - Debugging workerd-specific behavior **Never use workerd for:** - Running untrusted/user-submitted code - Multi-tenant isolation (not hardened) - Production without additional security layers ## Key Features - **Standards-based**: Fetch API, Web Crypto, Streams, WebSocket - **Nanoservices**: Service bindings with local call performance - **Capability security**: Explicit bindings prevent SSRF - **Backwards compatible**: Version = max compat date supported ## Architecture ``` Config (workerd.capnp) ├── Services (workers/endpoints) ├── Sockets (HTTP/HTTPS listeners) └── Extensions (global capabilities) ``` ## Quick Start ```bash workerd serve config.capnp workerd compile config.capnp myConfig -o binary workerd test config.capnp ``` ## Platform Support & Beta Status | Platform | Status | Notes | |----------|--------|-------| | Linux (x64) | Stable | Primary platform | | macOS (x64/ARM) | Stable | Full support | | Windows | Beta | Use WSL2 for best results | | Linux (ARM64) | Experimental | Limited testing | workerd is in **active development**. Breaking changes possible. Pin versions in production. ## Core Concepts - **Service**: Named endpoint (worker/network/disk/external) - **Binding**: Capability-based resource access (KV/DO/R2/services) - **Compatibility date**: Feature gate (always set!) - **Modules**: ES modules (recommended) or service worker syntax ## Reading Order (Progressive Disclosure) **Start here:** 1. This README (overview, decision tree) 2. [patterns.md](./patterns.md) - Common workflows, framework examples **When you need details:** 3. [configuration.md](./configuration.md) - Config format, services, bindings 4. [api.md](./api.md) - Runtime APIs, TypeScript types 5. [gotchas.md](./gotchas.md) - Common errors, debugging ## Related References - [workers](../workers/) - Workers runtime API documentation - [miniflare](../miniflare/) - Testing tool built on workerd - [wrangler](../wrangler/) - CLI that uses workerd for local dev ================================================ FILE: skills/.curated/cloudflare-deploy/references/workerd/api.md ================================================ # Workerd APIs ## Worker Code (JS/TS) ### ES Modules (Recommended) ```javascript export default { async fetch(request, env, ctx) { const value = await env.KV.get("key"); // Bindings in env const response = await env.API.fetch(request); // Service binding ctx.waitUntil(logRequest(request)); // Background task return new Response("OK"); }, async adminApi(request, env, ctx) { /* Named entrypoint */ }, async queue(batch, env, ctx) { /* Queue consumer */ }, async scheduled(event, env, ctx) { /* Cron handler */ } }; ``` ### TypeScript Types **Generate from wrangler.toml (Recommended):** ```bash wrangler types # Output: worker-configuration.d.ts ``` **Manual types:** ```typescript interface Env { API: Fetcher; CACHE: KVNamespace; STORAGE: R2Bucket; ROOMS: DurableObjectNamespace; API_KEY: string; } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { return new Response(await env.CACHE.get("key")); } }; ``` **Setup:** ```bash npm install -D @cloudflare/workers-types ``` ```json // tsconfig.json {"compilerOptions": {"types": ["@cloudflare/workers-types"]}} ``` ### Service Worker Syntax (Legacy) ```javascript addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { const value = await KV.get("key"); // Bindings as globals return new Response("OK"); } ``` ### Durable Objects ```javascript export class Room { constructor(state, env) { this.state = state; this.env = env; } async fetch(request) { const url = new URL(request.url); if (url.pathname === "/increment") { const value = (await this.state.storage.get("counter")) || 0; await this.state.storage.put("counter", value + 1); return new Response(String(value + 1)); } return new Response("Not found", {status: 404}); } } ``` ### RPC Between Services ```javascript // Caller: env.AUTH.validateToken(token) returns structured data const user = await env.AUTH.validateToken(request.headers.get("Authorization")); // Callee: export methods that return data export default { async validateToken(token) { return {id: 123, name: "Alice"}; } }; ``` ## Web Platform APIs ### Fetch - `fetch()`, `Request`, `Response`, `Headers` - `AbortController`, `AbortSignal` ### Streams - `ReadableStream`, `WritableStream`, `TransformStream` - Byte streams, BYOB readers ### Web Crypto - `crypto.subtle` (encrypt/decrypt/sign/verify) - `crypto.randomUUID()`, `crypto.getRandomValues()` ### Encoding - `TextEncoder`, `TextDecoder` - `atob()`, `btoa()` ### Web Standards - `URL`, `URLSearchParams` - `Blob`, `File`, `FormData` - `WebSocket` ### Server-Sent Events (EventSource) ```javascript // Server-side SSE const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); writer.write(new TextEncoder().encode('data: Hello\n\n')); return new Response(readable, {headers: {'Content-Type': 'text/event-stream'}}); ``` ### HTMLRewriter (HTML Parsing/Transformation) ```javascript const response = await fetch('https://example.com'); return new HTMLRewriter() .on('a[href]', { element(el) { el.setAttribute('href', `/proxy?url=${encodeURIComponent(el.getAttribute('href'))}`); } }) .on('script', { element(el) { el.remove(); } }) .transform(response); ``` ### TCP Sockets (Experimental) ```javascript const socket = await connect({ hostname: 'example.com', port: 80 }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode('GET / HTTP/1.1\r\n\r\n')); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new Response(value); ``` ### Performance - `performance.now()`, `performance.timeOrigin` - `setTimeout()`, `setInterval()`, `queueMicrotask()` ### Console - `console.log()`, `console.error()`, `console.warn()` ### Node.js Compat (`nodejs_compat` flag) ```javascript import { Buffer } from 'node:buffer'; import { randomBytes } from 'node:crypto'; const buf = Buffer.from('Hello'); const random = randomBytes(16); ``` **Available:** `node:buffer`, `node:crypto`, `node:stream`, `node:util`, `node:events`, `node:assert`, `node:path`, `node:querystring`, `node:url` **NOT available:** `node:fs`, `node:http`, `node:net`, `node:child_process` ## CLI Commands ```bash workerd serve config.capnp [constantName] # Start server workerd serve config.capnp --socket-addr http=*:3000 --verbose workerd compile config.capnp constantName -o binary # Compile to binary workerd test config.capnp [--test-only=test.js] # Run tests ``` ## Wrangler Integration Use Wrangler for development: ```bash wrangler dev # Uses workerd internally wrangler types # Generate TypeScript types from wrangler.toml ``` See [patterns.md](./patterns.md) for usage examples, [configuration.md](./configuration.md) for config details. ================================================ FILE: skills/.curated/cloudflare-deploy/references/workerd/configuration.md ================================================ # Workerd Configuration ## Basic Structure ```capnp using Workerd = import "/workerd/workerd.capnp"; const config :Workerd.Config = ( services = [(name = "main", worker = .mainWorker)], sockets = [(name = "http", address = "*:8080", http = (), service = "main")] ); const mainWorker :Workerd.Worker = ( modules = [(name = "index.js", esModule = embed "src/index.js")], compatibilityDate = "2024-01-15", bindings = [...] ); ``` ## Services **Worker**: Run JS/Wasm code ```capnp (name = "api", worker = ( modules = [(name = "index.js", esModule = embed "index.js")], compatibilityDate = "2024-01-15", bindings = [...] )) ``` **Network**: Internet access ```capnp (name = "internet", network = (allow = ["public"], tlsOptions = (trustBrowserCas = true))) ``` **External**: Reverse proxy ```capnp (name = "backend", external = (address = "api.com:443", http = (style = tls))) ``` **Disk**: Static files ```capnp (name = "assets", disk = (path = "/var/www", writable = false)) ``` ## Sockets ```capnp (name = "http", address = "*:8080", http = (), service = "main") (name = "https", address = "*:443", https = (options = (), tlsOptions = (keypair = (...))), service = "main") (name = "app", address = "unix:/tmp/app.sock", http = (), service = "main") ``` ## Worker Formats ```capnp # ES Modules (recommended) modules = [(name = "index.js", esModule = embed "src/index.js"), (name = "wasm.wasm", wasm = embed "build/module.wasm")] # Service Worker (legacy) serviceWorkerScript = embed "worker.js" # CommonJS (name = "legacy.js", commonJsModule = embed "legacy.js", namedExports = ["foo"]) ``` ## Bindings Bindings expose resources to workers. ES modules: `env.BINDING`, Service workers: globals. ### Primitive Types ```capnp (name = "API_KEY", text = "secret") # String (name = "CONFIG", json = '{"key":"val"}') # Parsed JSON (name = "DATA", data = embed "data.bin") # ArrayBuffer (name = "DATABASE_URL", fromEnvironment = "DB_URL") # System env var ``` ### Service Binding ```capnp (name = "AUTH", service = "auth-worker") # Basic (name = "API", service = ( name = "backend", entrypoint = "adminApi", # Named export props = (json = '{"role":"admin"}') # ctx.props )) ``` ### Storage ```capnp (name = "CACHE", kvNamespace = "kv-service") # KV (name = "STORAGE", r2Bucket = "r2-service") # R2 (name = "ROOMS", durableObjectNamespace = ( serviceName = "room-service", className = "Room" )) (name = "FAST", memoryCache = ( id = "cache-id", limits = (maxKeys = 1000, maxValueSize = 1048576) )) ``` ### Other ```capnp (name = "TASKS", queue = "queue-service") (name = "ANALYTICS", analyticsEngine = "analytics") (name = "LOADER", workerLoader = (id = "dynamic")) (name = "KEY", cryptoKey = (format = raw, algorithm = (name = "HMAC", hash = "SHA-256"), keyData = embed "key.bin", usages = [sign, verify], extractable = false)) (name = "TRACED", wrapped = (moduleName = "tracing", entrypoint = "makeTracer", innerBindings = [(name = "backend", service = "backend")])) ``` ## Compatibility ```capnp compatibilityDate = "2024-01-15" # Always set! compatibilityFlags = ["nodejs_compat", "streams_enable_constructors"] ``` Version = max compat date. Update carefully after testing. ## Parameter Bindings (Inheritance) ```capnp const base :Workerd.Worker = ( modules = [...], compatibilityDate = "2024-01-15", bindings = [(name = "API_URL", parameter = (type = text)), (name = "DB", parameter = (type = service))] ); const derived :Workerd.Worker = ( inherit = "base-service", bindings = [(name = "API_URL", text = "https://api.com"), (name = "DB", service = "postgres")] ); ``` ## Durable Objects Config ```capnp const worker :Workerd.Worker = ( modules = [...], compatibilityDate = "2024-01-15", bindings = [(name = "ROOMS", durableObjectNamespace = "Room")], durableObjectNamespaces = [(className = "Room", uniqueKey = "v1")], durableObjectStorage = (localDisk = "/var/do") ); ``` ## Remote Bindings (Development) Connect local workerd to production Cloudflare resources: ```capnp bindings = [ # Remote KV (requires API token) (name = "PROD_KV", kvNamespace = ( remote = ( accountId = "your-account-id", namespaceId = "your-namespace-id", apiToken = .envVar("CF_API_TOKEN") ) )), # Remote R2 (name = "PROD_R2", r2Bucket = ( remote = ( accountId = "your-account-id", bucketName = "my-bucket", apiToken = .envVar("CF_API_TOKEN") ) )), # Remote Durable Object (name = "PROD_DO", durableObjectNamespace = ( remote = ( accountId = "your-account-id", scriptName = "my-worker", className = "MyDO", apiToken = .envVar("CF_API_TOKEN") ) )) ] ``` **Note:** Remote bindings require network access and valid Cloudflare API credentials. ## Logging & Debugging ```capnp logging = (structuredLogging = true, stdoutPrefix = "OUT: ", stderrPrefix = "ERR: ") v8Flags = ["--expose-gc", "--max-old-space-size=2048"] # ⚠️ Unsupported in production ``` See [patterns.md](./patterns.md) for multi-service examples, [gotchas.md](./gotchas.md) for config errors. ================================================ FILE: skills/.curated/cloudflare-deploy/references/workerd/gotchas.md ================================================ # Workerd Gotchas ## Common Errors ### "Missing compatibility date" **Cause:** Compatibility date not set **Solution:** ❌ Wrong: ```capnp const worker :Workerd.Worker = ( serviceWorkerScript = embed "worker.js" ) ``` ✅ Correct: ```capnp const worker :Workerd.Worker = ( serviceWorkerScript = embed "worker.js", compatibilityDate = "2024-01-15" # Always set! ) ``` ### Wrong Binding Type **Problem:** JSON not parsed **Cause:** Using `text = '{"key":"value"}'` instead of `json` **Solution:** Use `json = '{"key":"value"}'` for parsed objects ### Service vs Namespace **Problem:** Cannot create DO instance **Cause:** Using `service = "room-service"` for Durable Object **Solution:** Use `durableObjectNamespace = "Room"` for DO bindings ### Module Name Mismatch **Problem:** Import fails **Cause:** Module name includes path: `name = "src/index.js"` **Solution:** Use simple names: `name = "index.js"`, embed with path ## Network Access **Problem:** Fetch fails with network error **Cause:** No network service configured (workerd has no global fetch) **Solution:** Add network service binding: ```capnp services = [(name = "internet", network = (allow = ["public"]))] bindings = [(name = "NET", service = "internet")] ``` Or external service: ```capnp bindings = [(name = "API", service = (external = (address = "api.com:443", http = (style = tls))))] ``` ### "Worker not responding" **Cause:** Socket misconfigured, no fetch handler, or port unavailable **Solution:** Verify socket `address` matches, worker exports `fetch()`, port available ### "Binding not found" **Cause:** Name mismatch or service doesn't exist **Solution:** Check binding name in config matches code (`env.BINDING` for ES modules) ### "Module not found" **Cause:** Module name doesn't match import or bad embed path **Solution:** Module `name` must match import path exactly, verify `embed` path ### "Compatibility error" **Cause:** Date not set or API unavailable on that date **Solution:** Set `compatibilityDate`, verify API available on that date ## Performance Issues **Problem:** High memory usage **Cause:** Large caches or many isolates **Solution:** Set cache limits, reduce isolate count, or use V8 flags (caution) **Problem:** Slow startup **Cause:** Many modules or complex config **Solution:** Compile to binary (`workerd compile`), reduce imports **Problem:** Request timeouts **Cause:** External service issues or DNS problems **Solution:** Check connectivity, DNS resolution, TLS handshake ## Build Issues **Problem:** Cap'n Proto syntax errors **Cause:** Invalid config or missing schema **Solution:** Install capnproto tools, validate: `capnp compile -I. config.capnp` **Problem:** Embed path not found **Cause:** Path relative to config file **Solution:** Use correct relative path or absolute path **Problem:** V8 flags cause crashes **Cause:** Unsafe V8 flags **Solution:** ⚠️ V8 flags unsupported in production. Test thoroughly before use. ## Security Issues **Problem:** Hardcoded secrets in config **Cause:** `text` binding with secret value **Solution:** Use `fromEnvironment` to load from env vars **Problem:** Overly broad network access **Cause:** `network = (allow = ["*"])` **Solution:** Restrict to `allow = ["public"]` or specific hosts **Problem:** Extractable crypto keys **Cause:** `cryptoKey = (extractable = true, ...)` **Solution:** Set `extractable = false` unless export required ## Compatibility Changes **Problem:** Breaking changes after compat date update **Cause:** New flags enabled between dates **Solution:** Review [compat dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/), test locally first **Problem:** "Compatibility date not supported" **Cause:** Workerd version older than compat date **Solution:** Update workerd binary (version = max compat date supported) ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | V8 flags | Unsupported in production | Use with caution | | Compatibility date | Must match workerd version | Update if mismatch | | Module count | Affects startup time | Many imports slow | ## Troubleshooting Steps 1. **Enable verbose logging**: `workerd serve config.capnp --verbose` 2. **Check logs**: Look for error messages, stack traces 3. **Validate config**: `capnp compile -I. config.capnp` 4. **Test bindings**: Log `Object.keys(env)` to verify 5. **Check versions**: Workerd version vs compat date 6. **Isolate issue**: Minimal repro config 7. **Review schema**: [workerd.capnp](https://github.com/cloudflare/workerd/blob/main/src/workerd/server/workerd.capnp) See [configuration.md](./configuration.md) for config details, [patterns.md](./patterns.md) for working examples, [api.md](./api.md) for runtime APIs. ================================================ FILE: skills/.curated/cloudflare-deploy/references/workerd/patterns.md ================================================ # Workerd Patterns ## Multi-Service Architecture ```capnp const config :Workerd.Config = ( services = [ (name = "frontend", worker = ( modules = [(name = "index.js", esModule = embed "frontend/index.js")], compatibilityDate = "2024-01-15", bindings = [(name = "API", service = "api")] )), (name = "api", worker = ( modules = [(name = "index.js", esModule = embed "api/index.js")], compatibilityDate = "2024-01-15", bindings = [(name = "DB", service = "postgres"), (name = "CACHE", kvNamespace = "kv")] )), (name = "postgres", external = (address = "db.internal:5432", http = ())), (name = "kv", disk = (path = "/var/kv", writable = true)) ], sockets = [(name = "http", address = "*:8080", http = (), service = "frontend")] ); ``` ## Durable Objects ```capnp const worker :Workerd.Worker = ( modules = [(name = "index.js", esModule = embed "index.js"), (name = "room.js", esModule = embed "room.js")], compatibilityDate = "2024-01-15", bindings = [(name = "ROOMS", durableObjectNamespace = "Room")], durableObjectNamespaces = [(className = "Room", uniqueKey = "v1")], durableObjectStorage = (localDisk = "/var/do") ); ``` ## Dev vs Prod Configs ```capnp # Use parameter bindings for env-specific config const baseWorker :Workerd.Worker = ( modules = [(name = "index.js", esModule = embed "src/index.js")], compatibilityDate = "2024-01-15", bindings = [(name = "API_URL", parameter = (type = text))] ); const prodWorker :Workerd.Worker = ( inherit = "base-service", bindings = [(name = "API_URL", text = "https://api.prod.com")] ); ``` ## HTTP Reverse Proxy ```capnp services = [ (name = "proxy", worker = (serviceWorkerScript = embed "proxy.js", compatibilityDate = "2024-01-15", bindings = [(name = "BACKEND", service = "backend")])), (name = "backend", external = (address = "internal:8080", http = ())) ] ``` ## Local Development **Recommended:** Use Wrangler ```bash wrangler dev # Uses workerd internally ``` **Direct workerd:** ```bash workerd serve config.capnp --socket-addr http=*:3000 --verbose ``` **Environment variables:** ```capnp bindings = [(name = "DATABASE_URL", fromEnvironment = "DATABASE_URL")] ``` ## Testing ```bash workerd test config.capnp workerd test config.capnp --test-only=test.js ``` Test files must be included in `modules = [...]` config. ## Production Deployment ### Compiled Binary (Recommended) ```bash workerd compile config.capnp myConfig -o production-server ./production-server ``` ### Docker ```dockerfile FROM debian:bookworm-slim RUN apt-get update && apt-get install -y ca-certificates COPY workerd /usr/local/bin/ COPY config.capnp /etc/workerd/ COPY src/ /etc/workerd/src/ EXPOSE 8080 CMD ["workerd", "serve", "/etc/workerd/config.capnp"] ``` ### Systemd ```ini # /etc/systemd/system/workerd.service [Service] ExecStart=/usr/bin/workerd serve /etc/workerd/config.capnp --socket-fd http=3 Restart=always User=nobody ``` See systemd socket activation docs for complete setup. ## Framework Integration ### Hono ```javascript import { Hono } from 'hono'; const app = new Hono(); app.get('/', (c) => c.text('Hello Hono!')); app.get('/api/:id', async (c) => { const id = c.req.param('id'); const data = await c.env.KV.get(id); return c.json({ id, data }); }); export default app; ``` ### itty-router ```javascript import { Router } from 'itty-router'; const router = Router(); router.get('/', () => new Response('Hello itty!')); router.get('/api/:id', async (request, env) => { const { id } = request.params; const data = await env.KV.get(id); return Response.json({ id, data }); }); export default { fetch: (request, env, ctx) => router.handle(request, env, ctx) }; ``` ## Best Practices 1. **Use ES modules** over service worker syntax 2. **Explicit bindings** - no global namespace assumptions 3. **Type safety** - define `Env` interfaces (use `wrangler types`) 4. **Service isolation** - split concerns into multiple services 5. **Pin compat date** in production after testing 6. **Use ctx.waitUntil()** for background tasks 7. **Handle errors gracefully** with try/catch 8. **Configure resource limits** on caches/storage ## Common Patterns ### Error Handling ```javascript export default { async fetch(request, env, ctx) { try { return await handleRequest(request, env); } catch (error) { console.error("Request failed", error); return new Response("Internal Error", {status: 500}); } } }; ``` ### Background Tasks ```javascript export default { async fetch(request, env, ctx) { const response = new Response("OK"); // Fire-and-forget background work ctx.waitUntil( env.ANALYTICS.put(request.url, Date.now()) ); return response; } }; ``` See [configuration.md](./configuration.md) for config syntax, [api.md](./api.md) for runtime APIs, [gotchas.md](./gotchas.md) for common errors. ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers/README.md ================================================ # Cloudflare Workers Expert guidance for building, deploying, and optimizing Cloudflare Workers applications. ## Overview Cloudflare Workers run on V8 isolates (NOT containers/VMs): - Extremely fast cold starts (< 1ms) - Global deployment across 300+ locations - Web standards compliant (fetch, URL, Headers, Request, Response) - Support JS/TS, Python, Rust, and WebAssembly **Key principle**: Workers use web platform APIs wherever possible for portability. ## Module Worker Pattern (Recommended) ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { return new Response('Hello World!'); }, }; ``` **Handler parameters**: - `request`: Incoming HTTP request (standard Request object) - `env`: Environment bindings (KV, D1, R2, secrets, vars) - `ctx`: Execution context (`waitUntil`, `passThroughOnException`) ## Essential Commands ```bash npx wrangler dev # Local dev npx wrangler dev --remote # Remote dev (actual resources) npx wrangler deploy # Production npx wrangler deploy --env staging # Specific environment npx wrangler tail # Stream logs npx wrangler secret put API_KEY # Set secret ``` ## When to Use Workers - API endpoints at the edge - Request/response transformation - Authentication/authorization layers - Static asset optimization - A/B testing and feature flags - Rate limiting and security - Proxy/routing logic - WebSocket applications ## Quick Start ```bash npm create cloudflare@latest my-worker -- --type hello-world cd my-worker npx wrangler dev ``` ## Handler Signatures ```typescript // HTTP requests async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise // Cron triggers async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise // Queue consumer async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise // Tail consumer async tail(events: TraceItem[], env: Env, ctx: ExecutionContext): Promise ``` ## Resources **Docs**: https://developers.cloudflare.com/workers/ **Examples**: https://developers.cloudflare.com/workers/examples/ **Runtime APIs**: https://developers.cloudflare.com/workers/runtime-apis/ ## In This Reference - [Configuration](./configuration.md) - wrangler.jsonc setup, bindings, environments - [API](./api.md) - Runtime APIs, bindings, execution context - [Patterns](./patterns.md) - Common workflows, testing, optimization - [Frameworks](./frameworks.md) - Hono, routing, validation - [Gotchas](./gotchas.md) - Common issues, limits, troubleshooting ## Reading Order | Task | Start With | Then Read | |------|------------|-----------| | First Worker | README → Configuration → API | Patterns | | Add framework | Frameworks | Configuration (bindings) | | Add storage/bindings | Configuration → API (binding usage) | See Also links | | Debug issues | Gotchas | API (specific binding docs) | | Production optimization | Patterns | API (caching, streaming) | | Type safety | Configuration (TypeScript) | Frameworks (Hono typing) | ## See Also - [KV](../kv/README.md) - Key-value storage - [D1](../d1/README.md) - SQL database - [R2](../r2/README.md) - Object storage - [Durable Objects](../durable-objects/README.md) - Stateful coordination - [Queues](../queues/README.md) - Message queues - [Wrangler](../wrangler/README.md) - CLI tool reference ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers/api.md ================================================ # Workers Runtime APIs ## Fetch Handler ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); if (request.method === 'POST' && url.pathname === '/api') { const body = await request.json(); return new Response(JSON.stringify({ id: 1 }), { headers: { 'Content-Type': 'application/json' } }); } return fetch(request); // Subrequest to origin }, }; ``` ## Execution Context ```typescript ctx.waitUntil(logAnalytics(request)); // Background work, don't block response ctx.passThroughOnException(); // Failover to origin on error ``` **Never** `await` background operations - use `ctx.waitUntil()`. ## Bindings ```typescript // KV await env.MY_KV.get('key'); await env.MY_KV.put('key', 'value', { expirationTtl: 3600 }); // R2 const obj = await env.MY_BUCKET.get('file.txt'); await env.MY_BUCKET.put('file.txt', 'content'); // D1 const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1).first(); // D1 Sessions (2024+) - read-after-write consistency const session = env.DB.withSession(); await session.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run(); const user = await session.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first(); // Guaranteed fresh // Queues await env.MY_QUEUE.send({ timestamp: Date.now() }); // Secrets/vars const key = env.API_KEY; ``` ## Cache API ```typescript const cache = caches.default; let response = await cache.match(request); if (!response) { response = await fetch(request); response = new Response(response.body, response); response.headers.set('Cache-Control', 'max-age=3600'); ctx.waitUntil(cache.put(request, response.clone())); // Clone before caching } ``` ## HTMLRewriter ```typescript return new HTMLRewriter() .on('a[href]', { element(el) { const href = el.getAttribute('href'); if (href?.startsWith('http://')) { el.setAttribute('href', href.replace('http://', 'https://')); } } }) .transform(response); ``` **Use cases**: A/B testing, analytics injection, link rewriting ## WebSockets ### Standard WebSocket ```typescript const [client, server] = Object.values(new WebSocketPair()); server.accept(); server.addEventListener('message', event => { server.send(`Echo: ${event.data}`); }); return new Response(null, { status: 101, webSocket: client }); ``` ### WebSocket Hibernation (Recommended for idle connections) ```typescript // In Durable Object export class WebSocketDO { async webSocketMessage(ws: WebSocket, message: string) { ws.send(`Echo: ${message}`); } async webSocketClose(ws: WebSocket, code: number, reason: string) { // Cleanup on close } async webSocketError(ws: WebSocket, error: Error) { console.error('WebSocket error:', error); } } ``` Hibernation automatically suspends inactive connections (no CPU cost), wakes on events ## Durable Objects ### RPC Pattern (Recommended 2024+) ```typescript export class Counter { private value = 0; constructor(private state: DurableObjectState) { state.blockConcurrencyWhile(async () => { this.value = (await state.storage.get('value')) || 0; }); } // Export methods directly - called via RPC (type-safe, zero serialization) async increment(): Promise { this.value++; await this.state.storage.put('value', this.value); return this.value; } async getValue(): Promise { return this.value; } } // Worker usage: const stub = env.COUNTER.get(env.COUNTER.idFromName('global')); const count = await stub.increment(); // Direct method call, full type safety ``` ### Legacy Fetch Pattern (Pre-2024) ```typescript async fetch(request: Request): Promise { const url = new URL(request.url); if (url.pathname === '/increment') { await this.state.storage.put('value', ++this.value); } return new Response(String(this.value)); } // Usage: await stub.fetch('http://x/increment') ``` **When to use DOs**: Real-time collaboration, rate limiting, strongly consistent state ## Other Handlers ```typescript // Cron: async scheduled(event, env, ctx) { ctx.waitUntil(doCleanup(env)); } // Queue: async queue(batch) { for (const msg of batch.messages) { await process(msg.body); msg.ack(); } } // Tail: async tail(events, env) { for (const e of events) if (e.outcome === 'exception') await log(e); } ``` ## Service Bindings ```typescript // Worker-to-worker RPC (zero latency, no internet round-trip) return env.SERVICE_B.fetch(request); // With RPC (2024+) - same as Durable Objects RPC export class ServiceWorker { async getData() { return { data: 'value' }; } } // Usage: const data = await env.SERVICE_B.getData(); ``` **Benefits**: Type-safe method calls, no HTTP overhead, share code between Workers ## See Also - [Configuration](./configuration.md) - Binding setup - [Patterns](./patterns.md) - Common workflows - [KV](../kv/README.md), [D1](../d1/README.md), [R2](../r2/README.md), [Durable Objects](../durable-objects/README.md), [Queues](../queues/README.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers/configuration.md ================================================ # Workers Configuration ## wrangler.jsonc (Recommended) ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date for new projects // Bindings (non-inheritable) "vars": { "ENVIRONMENT": "production" }, "kv_namespaces": [{ "binding": "MY_KV", "id": "abc123" }], "r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }], "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }], // Environments "env": { "staging": { "vars": { "ENVIRONMENT": "staging" }, "kv_namespaces": [{ "binding": "MY_KV", "id": "staging-id" }] } } } ``` ## Configuration Rules **Inheritable**: `name`, `main`, `compatibility_date`, `routes`, `workers_dev` **Non-inheritable**: All bindings (`vars`, `kv_namespaces`, `r2_buckets`, etc.) **Top-level only**: `migrations`, `keep_vars`, `send_metrics` **ALWAYS set `compatibility_date` to current date for new projects** ## Bindings ```jsonc { // Environment variables - access via env.VAR_NAME "vars": { "ENVIRONMENT": "production" }, // KV (key-value storage) "kv_namespaces": [{ "binding": "MY_KV", "id": "abc123" }], // R2 (object storage) "r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }], // D1 (SQL database) "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }], // Durable Objects (stateful coordination) "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }] }, // Queues (message queues) "queues": { "producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }], "consumers": [{ "queue": "my-queue", "max_batch_size": 10 }] }, // Service bindings (worker-to-worker RPC) "services": [{ "binding": "SERVICE_B", "service": "service-b" }], // Analytics Engine "analytics_engine_datasets": [{ "binding": "ANALYTICS" }] } ``` ### Secrets Set via CLI (never in config): ```bash npx wrangler secret put API_KEY ``` Access: `env.API_KEY` ### Automatic Provisioning (Beta) Bindings without IDs are auto-created: ```jsonc { "kv_namespaces": [{ "binding": "MY_KV" }] } // ID added on deploy ``` ## Routes & Triggers ```jsonc { "routes": [ { "pattern": "example.com/*", "zone_name": "example.com" } ], "triggers": { "crons": ["0 */6 * * *"] // Every 6 hours } } ``` ## TypeScript Setup ### Automatic Type Generation (Recommended) ```bash npm install -D @cloudflare/workers-types npx wrangler types # Generates .wrangler/types/runtime.d.ts from wrangler.jsonc ``` `tsconfig.json`: ```jsonc { "compilerOptions": { "target": "ES2022", "lib": ["ES2022"], "types": ["@cloudflare/workers-types"] }, "include": [".wrangler/types/**/*.ts", "src/**/*"] } ``` Import generated types: ```typescript import type { Env } from './.wrangler/types/runtime'; export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { await env.MY_KV.get('key'); // Fully typed, autocomplete works return new Response('OK'); }, }; ``` Re-run `npx wrangler types` after changing bindings in wrangler.jsonc ### Manual Type Definition (Legacy) ```typescript interface Env { MY_KV: KVNamespace; DB: D1Database; API_KEY: string; } ``` ## Advanced Options ```jsonc { // Auto-locate compute near data sources "placement": { "mode": "smart" }, // Enable Node.js built-ins (Buffer, process, path, etc.) "compatibility_flags": ["nodejs_compat_v2"], // Observability (10% sampling) "observability": { "enabled": true, "head_sampling_rate": 0.1 } } ``` ### Node.js Compatibility `nodejs_compat_v2` enables: - `Buffer`, `process.env`, `path`, `stream` - CommonJS `require()` for Node modules - `node:` imports (e.g., `import { Buffer } from 'node:buffer'`) **Note:** Adds ~1-2ms cold start overhead. Use Workers APIs (R2, KV) when possible ## Deployment Commands ```bash npx wrangler deploy # Production npx wrangler deploy --env staging npx wrangler deploy --dry-run # Validate only ``` ## See Also - [API](./api.md) - Runtime APIs and bindings usage - [Patterns](./patterns.md) - Deployment strategies - [Wrangler](../wrangler/README.md) - CLI reference ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers/frameworks.md ================================================ # Workers Frameworks ## Hono (Recommended) Workers-native web framework with excellent TypeScript support and middleware ecosystem. ```bash npm install hono ``` ### Basic Setup ```typescript import { Hono } from 'hono'; const app = new Hono(); app.get('/', (c) => c.text('Hello World!')); app.post('/api/users', async (c) => { const body = await c.req.json(); return c.json({ id: 1, ...body }, 201); }); export default app; ``` ### Typed Environment ```typescript import type { Env } from './.wrangler/types/runtime'; const app = new Hono<{ Bindings: Env }>(); app.get('/data', async (c) => { const value = await c.env.MY_KV.get('key'); // Fully typed return c.text(value || 'Not found'); }); ``` ### Middleware ```typescript import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; app.use('*', logger()); app.use('/api/*', cors({ origin: '*' })); // Custom middleware app.use('/protected/*', async (c, next) => { const auth = c.req.header('Authorization'); if (!auth?.startsWith('Bearer ')) return c.text('Unauthorized', 401); await next(); }); ``` ### Request Validation (Zod) ```typescript import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; const schema = z.object({ name: z.string().min(1), email: z.string().email(), }); app.post('/users', zValidator('json', schema), async (c) => { const validated = c.req.valid('json'); // Type-safe, validated data return c.json({ id: 1, ...validated }); }); ``` **Error handling**: Automatic 400 response with validation errors ### Route Groups ```typescript const api = new Hono().basePath('/api'); api.get('/users', (c) => c.json([])); api.post('/users', (c) => c.json({ id: 1 })); app.route('/', api); // Mounts at /api/* ``` ### Error Handling ```typescript app.onError((err, c) => { console.error(err); return c.json({ error: err.message }, 500); }); app.notFound((c) => c.json({ error: 'Not Found' }, 404)); ``` ### Accessing ExecutionContext ```typescript export default { fetch(request: Request, env: Env, ctx: ExecutionContext) { return app.fetch(request, env, ctx); }, }; // In route handlers: app.get('/log', (c) => { c.executionCtx.waitUntil(logRequest(c.req)); return c.text('OK'); }); ``` ### OpenAPI/Swagger (Hono OpenAPI) ```typescript import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; const app = new OpenAPIHono(); const route = createRoute({ method: 'get', path: '/users/{id}', request: { params: z.object({ id: z.string() }) }, responses: { 200: { description: 'User found', content: { 'application/json': { schema: z.object({ id: z.string() }) } } }, }, }); app.openapi(route, (c) => { const { id } = c.req.valid('param'); return c.json({ id }); }); app.doc('/openapi.json', { openapi: '3.0.0', info: { version: '1.0.0', title: 'API' } }); ``` ### Testing with Hono ```typescript import { describe, it, expect } from 'vitest'; import app from '../src/index'; describe('API', () => { it('GET /', async () => { const res = await app.request('/'); expect(res.status).toBe(200); expect(await res.text()).toBe('Hello World!'); }); }); ``` ## Other Frameworks ### itty-router (Minimalist) ```typescript import { Router } from 'itty-router'; const router = Router(); router.get('/users/:id', ({ params }) => new Response(params.id)); export default { fetch: router.handle }; ``` **Use case**: Tiny bundle size (~500 bytes), simple routing needs ### Worktop (Advanced) ```typescript import { Router } from 'worktop'; const router = new Router(); router.add('GET', '/users/:id', (req, res) => { res.send(200, { id: req.params.id }); }); router.listen(); ``` **Use case**: Advanced routing, built-in CORS/cache utilities ## Framework Comparison | Framework | Bundle Size | TypeScript | Middleware | Validation | Best For | |-----------|-------------|------------|------------|------------|----------| | Hono | ~12KB | Excellent | Rich | Zod | Production apps | | itty-router | ~500B | Good | Basic | Manual | Minimal APIs | | Worktop | ~8KB | Good | Advanced | Manual | Complex routing | ## See Also - [Patterns](./patterns.md) - Common workflows - [API](./api.md) - Runtime APIs - [Gotchas](./gotchas.md) - Framework-specific issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers/gotchas.md ================================================ # Workers Gotchas ## Common Errors ### "Too much CPU time used" **Cause:** Worker exceeded CPU time limit (10ms standard, 30ms unbound) **Solution:** Use `ctx.waitUntil()` for background work, offload heavy compute to Durable Objects, or consider Workers AI for ML workloads ### "Module-Level State Lost" **Cause:** Workers are stateless between requests; module-level variables reset unpredictably **Solution:** Use KV, D1, or Durable Objects for persistent state; don't rely on module-level variables ### "Body has already been used" **Cause:** Attempting to read response body twice (bodies are streams) **Solution:** Clone response before reading: `response.clone()` or read once and create new Response with the text ### "Node.js module not found" **Cause:** Node.js built-ins not available by default **Solution:** Use Workers APIs (e.g., R2 for file storage) or enable Node.js compat with `"compatibility_flags": ["nodejs_compat_v2"]` ### "Cannot fetch in global scope" **Cause:** Attempting to use fetch during module initialization **Solution:** Move fetch calls inside handler functions (fetch, scheduled, etc.) where they're allowed ### "Subrequest depth limit exceeded" **Cause:** Too many nested subrequests creating deep call chain **Solution:** Flatten request chain or use service bindings for direct Worker-to-Worker communication ### "D1 read-after-write inconsistency" **Cause:** D1 is eventually consistent; reads may not reflect recent writes **Solution:** Use D1 Sessions (2024+) to guarantee read-after-write consistency within a session: ```typescript const session = env.DB.withSession(); await session.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run(); const user = await session.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first(); // Guaranteed to see Alice ``` **When to use sessions:** Write → Read patterns, transactions requiring consistency ### "wrangler types not generating TypeScript definitions" **Cause:** Type generation not configured or outdated **Solution:** Run `npx wrangler types` after changing bindings in wrangler.jsonc: ```bash npx wrangler types # Generates .wrangler/types/runtime.d.ts ``` Add to `tsconfig.json`: `"include": [".wrangler/types/**/*.ts"]` Then import: `import type { Env } from './.wrangler/types/runtime';` ### "Durable Object RPC errors with deprecated fetch pattern" **Cause:** Using old `stub.fetch()` pattern instead of RPC (2024+) **Solution:** Export methods directly, call via RPC: ```typescript // ❌ Old fetch pattern export class MyDO { async fetch(request: Request) { const { method } = await request.json(); if (method === 'increment') return new Response(String(await this.increment())); } async increment() { return ++this.value; } } const stub = env.DO.get(id); const res = await stub.fetch('http://x', { method: 'POST', body: JSON.stringify({ method: 'increment' }) }); // ✅ RPC pattern (type-safe, no serialization overhead) export class MyDO { async increment() { return ++this.value; } } const stub = env.DO.get(id); const count = await stub.increment(); // Direct method call ``` ### "WebSocket connection closes unexpectedly" **Cause:** Worker reaches CPU limit while maintaining WebSocket connection **Solution:** Use WebSocket hibernation (2024+) to offload idle connections: ```typescript export class WebSocketDO { async webSocketMessage(ws: WebSocket, message: string) { // Handle message } async webSocketClose(ws: WebSocket, code: number) { // Cleanup } } ``` Hibernation automatically suspends inactive connections, wakes on events ### "Framework middleware not working with Workers" **Cause:** Framework expects Node.js primitives (e.g., Express uses Node streams) **Solution:** Use Workers-native frameworks (Hono, itty-router, Worktop) or adapt middleware: ```typescript // ✅ Hono (Workers-native) import { Hono } from 'hono'; const app = new Hono(); app.use('*', async (c, next) => { /* middleware */ await next(); }); ``` See [frameworks.md](./frameworks.md) for full patterns ## Limits | Limit | Value | Notes | |-------|-------|-------| | Request size | 100 MB | Maximum incoming request size | | Response size | Unlimited | Supports streaming | | CPU time (standard) | 10ms | Standard Workers | | CPU time (unbound) | 30ms | Unbound Workers | | Subrequests | 1000 | Per request | | KV reads | 1000 | Per request | | KV write size | 25 MB | Maximum per write | | Environment size | 5 MB | Total size of env bindings | ## See Also - [Patterns](./patterns.md) - Best practices - [API](./api.md) - Runtime APIs - [Configuration](./configuration.md) - Setup - [Frameworks](./frameworks.md) - Hono, routing, validation ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers/patterns.md ================================================ # Workers Patterns ## Error Handling ```typescript class HTTPError extends Error { constructor(public status: number, message: string) { super(message); } } export default { async fetch(request: Request, env: Env): Promise { try { return await handleRequest(request, env); } catch (error) { if (error instanceof HTTPError) { return new Response(JSON.stringify({ error: error.message }), { status: error.status, headers: { 'Content-Type': 'application/json' } }); } return new Response('Internal Server Error', { status: 500 }); } }, }; ``` ## CORS ```typescript const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' }; if (request.method === 'OPTIONS') return new Response(null, { headers: corsHeaders }); ``` ## Routing ```typescript const router = { 'GET /api/users': handleGetUsers, 'POST /api/users': handleCreateUser }; const handler = router[`${request.method} ${url.pathname}`]; return handler ? handler(request, env) : new Response('Not Found', { status: 404 }); ``` **Production**: Use Hono, itty-router, or Worktop (see [frameworks.md](./frameworks.md)) ## Request Validation (Zod) ```typescript import { z } from 'zod'; const userSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().positive().optional(), }); async function handleCreateUser(request: Request) { try { const body = await request.json(); const validated = userSchema.parse(body); // Throws on invalid data return new Response(JSON.stringify({ id: 1, ...validated }), { status: 201, headers: { 'Content-Type': 'application/json' }, }); } catch (err) { if (err instanceof z.ZodError) { return new Response(JSON.stringify({ errors: err.errors }), { status: 400 }); } throw err; } } ``` **With Hono**: Use `@hono/zod-validator` for automatic validation (see [frameworks.md](./frameworks.md)) ## Performance ```typescript // ❌ Sequential const user = await fetch('/api/user/1'); const posts = await fetch('/api/posts?user=1'); // ✅ Parallel const [user, posts] = await Promise.all([fetch('/api/user/1'), fetch('/api/posts?user=1')]); ``` ## Streaming ```typescript const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < 1000; i++) { controller.enqueue(new TextEncoder().encode(`Item ${i}\n`)); if (i % 100 === 0) await new Promise(r => setTimeout(r, 0)); } controller.close(); } }); ``` ## Transform Streams ```typescript response.body.pipeThrough(new TextDecoderStream()).pipeThrough( new TransformStream({ transform(chunk, c) { c.enqueue(chunk.toUpperCase()); } }) ).pipeThrough(new TextEncoderStream()); ``` ## Testing ```typescript import { describe, it, expect } from 'vitest'; import worker from '../src/index'; describe('Worker', () => { it('returns 200', async () => { const req = new Request('http://localhost/'); const env = { MY_VAR: 'test' }; const ctx = { waitUntil: () => {}, passThroughOnException: () => {} }; expect((await worker.fetch(req, env, ctx)).status).toBe(200); }); }); ``` ## Deployment ```bash npx wrangler deploy # production npx wrangler deploy --env staging npx wrangler versions upload --message "Add feature" npx wrangler rollback ``` ## Monitoring ```typescript const start = Date.now(); const response = await handleRequest(request, env); ctx.waitUntil(env.ANALYTICS.writeDataPoint({ doubles: [Date.now() - start], blobs: [request.url, String(response.status)] })); ``` ## Security & Rate Limiting ```typescript // Security headers const security = { 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY' }; // Auth const auth = request.headers.get('Authorization'); if (!auth?.startsWith('Bearer ')) return new Response('Unauthorized', { status: 401 }); // Gradual rollouts (deterministic user bucketing) const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(userId)); if (new Uint8Array(hash)[0] % 100 < rolloutPercent) return newFeature(request); ``` Rate limiting: See [Durable Objects](../durable-objects/README.md) ## R2 Multipart Upload ```typescript // For files > 100MB const upload = await env.MY_BUCKET.createMultipartUpload('large-file.bin'); try { const parts = []; for (let i = 0; i < chunks.length; i++) { parts.push(await upload.uploadPart(i + 1, chunks[i])); } await upload.complete(parts); } catch (err) { await upload.abort(); throw err; } ``` Parallel uploads, resume on failure, handle files > 5GB ## Workflows (Step Orchestration) ```typescript import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers'; export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent<{ userId: string }>, step: WorkflowStep) { const user = await step.do('fetch-user', async () => fetch(`/api/users/${event.payload.userId}`).then(r => r.json()) ); await step.sleep('wait', '1 hour'); await step.do('notify', async () => sendEmail(user.email)); } } ``` Multi-step jobs with automatic retries, state persistence, resume from failure ## See Also - [API](./api.md) - Runtime APIs - [Gotchas](./gotchas.md) - Common issues - [Configuration](./configuration.md) - Setup - [Frameworks](./frameworks.md) - Hono, routing, validation ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-ai/README.md ================================================ # Cloudflare Workers AI Expert guidance for Cloudflare Workers AI - serverless GPU-powered AI inference at the edge. ## Overview Workers AI provides: - 50+ pre-trained models (LLMs, embeddings, image generation, speech-to-text, translation) - Native Workers binding (no external API calls) - Pay-per-use pricing (neurons consumed per inference) - OpenAI-compatible REST API - Streaming support for text generation - Function calling with compatible models **Architecture**: Inference runs on Cloudflare's GPU network. Models load on first request (cold start 1-3s), subsequent requests are faster. ## Quick Start ```typescript interface Env { AI: Ai; } export default { async fetch(request: Request, env: Env) { const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [{ role: 'user', content: 'What is Cloudflare?' }] }); return Response.json(response); } }; ``` ```bash # Setup - add binding to wrangler.jsonc wrangler dev --remote # Must use --remote for AI wrangler deploy ``` ## Model Selection Decision Tree ### Text Generation (Chat/Completion) **Quality Priority**: - **Best quality**: `@cf/meta/llama-3.1-70b-instruct` (expensive, ~2000 neurons) - **Balanced**: `@cf/meta/llama-3.1-8b-instruct` (good quality, ~200 neurons) - **Fastest/cheapest**: `@cf/mistral/mistral-7b-instruct-v0.1` (~50 neurons) **Function Calling**: - Use `@cf/meta/llama-3.1-8b-instruct` or `@cf/meta/llama-3.1-70b-instruct` (native tool support) **Code Generation**: - Use `@cf/deepseek-ai/deepseek-coder-6.7b-instruct` (specialized for code) ### Embeddings (Semantic Search/RAG) **English text**: - **Best**: `@cf/baai/bge-large-en-v1.5` (1024 dims, highest quality) - **Balanced**: `@cf/baai/bge-base-en-v1.5` (768 dims, good quality) - **Fast**: `@cf/baai/bge-small-en-v1.5` (384 dims, lower quality but fast) **Multilingual**: - Use `@hf/sentence-transformers/paraphrase-multilingual-minilm-l12-v2` ### Image Generation - **Stable Diffusion**: `@cf/stabilityai/stable-diffusion-xl-base-1.0` (~10,000 neurons) - **Portraits**: `@cf/lykon/dreamshaper-8-lcm` (optimized for faces) ### Other Tasks - **Speech-to-text**: `@cf/openai/whisper` - **Translation**: `@cf/meta/m2m100-1.2b` (100 languages) - **Image classification**: `@cf/microsoft/resnet-50` ## SDK Approach Decision Tree ### Native Binding (Recommended) **When**: Building Workers/Pages with TypeScript **Why**: Zero external dependencies, best performance, native types ```typescript await env.AI.run(model, input); ``` ### REST API **When**: External services, non-Workers environments, testing **Why**: Standard HTTP, works anywhere ```bash curl https://api.cloudflare.com/client/v4/accounts//ai/run/@cf/meta/llama-3.1-8b-instruct \ -H "Authorization: Bearer " \ -d '{"messages":[{"role":"user","content":"Hello"}]}' ``` ### Vercel AI SDK Integration **When**: Using Vercel AI SDK features (streaming UI, tool calling abstractions) **Why**: Unified interface across providers ```typescript import { openai } from '@ai-sdk/openai'; const model = openai('model-name', { baseURL: 'https://api.cloudflare.com/client/v4/accounts//ai/v1', headers: { Authorization: 'Bearer ' } }); ``` ## RAG vs Direct Generation ### Use RAG (Vectorize + Workers AI) When: - Answering questions about specific documents/data - Need factual accuracy from known corpus - Context exceeds model's window (>4K tokens) - Building knowledge base chat ### Use Direct Generation When: - Creative writing, brainstorming - General knowledge questions - Small context fits in prompt (<4K tokens) - Cost optimization (RAG adds embedding + vector search costs) ## Platform Limits | Limit | Free Tier | Paid Plans | |-------|-----------|------------| | Neurons/day | 10,000 | Pay per use | | Rate limit | Varies by model | Higher (contact support) | | Context window | Model dependent (2K-8K) | Same | | Streaming | ✅ Supported | ✅ Supported | | Function calling | ✅ Supported (select models) | ✅ Supported | **Pricing**: Free 10K neurons/day, then pay per neuron consumed (varies by model) ## Common Tasks ```typescript // Streaming text generation const stream = await env.AI.run(model, { messages, stream: true }); for await (const chunk of stream) { console.log(chunk.response); } // Embeddings for RAG const { data } = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: ['Query text', 'Document 1', 'Document 2'] }); // Function calling const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [{ role: 'user', content: 'What is the weather?' }], tools: [{ type: 'function', function: { name: 'getWeather', parameters: { ... } } }] }); ``` ## Development Workflow ```bash # Always use --remote for AI (local doesn't have models) wrangler dev --remote # Deploy to production wrangler deploy # View model catalog # https://developers.cloudflare.com/workers-ai/models/ ``` ## Reading Order **Start here**: Quick Start above → configuration.md (setup) **Common tasks**: - First time setup: configuration.md → Add binding + deploy - Choose model: Model Selection Decision Tree (above) → api.md - Build RAG: patterns.md → Vectorize integration - Optimize costs: Model Selection + gotchas.md (rate limits) - Debugging: gotchas.md → Common errors ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc setup, TypeScript types, bindings, environment variables - [api.md](./api.md) - env.AI.run(), streaming, function calling, REST API, response types - [patterns.md](./patterns.md) - RAG with Vectorize, prompt engineering, batching, error handling, caching - [gotchas.md](./gotchas.md) - Deprecated @cloudflare/ai package, rate limits, pricing, common errors ## See Also - [vectorize](../vectorize/) - Vector database for RAG patterns - [ai-gateway](../ai-gateway/) - Caching, rate limiting, analytics for AI requests - [workers](../workers/) - Worker runtime and fetch handler patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-ai/api.md ================================================ # Workers AI API Reference ## Core Method ```typescript const response = await env.AI.run(model, input); ``` ## Text Generation ```typescript const result = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [ { role: 'system', content: 'You are helpful' }, { role: 'user', content: 'Hello' } ], temperature: 0.7, // 0-1 max_tokens: 100 }); console.log(result.response); ``` **Streaming:** ```typescript const stream = await env.AI.run(model, { messages, stream: true }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } }); ``` ## Embeddings ```typescript const result = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: ['Query', 'Doc 1', 'Doc 2'] // Batch for efficiency }); const [queryEmbed, doc1Embed, doc2Embed] = result.data; // 768-dim vectors ``` ## Function Calling ```typescript const tools = [{ type: 'function', function: { name: 'getWeather', description: 'Get weather for location', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'] } } }]; const response = await env.AI.run(model, { messages, tools }); if (response.tool_calls) { const args = JSON.parse(response.tool_calls[0].function.arguments); // Execute function, send result back } ``` ## Image Generation ```typescript const image = await env.AI.run('@cf/stabilityai/stable-diffusion-xl-base-1.0', { prompt: 'Mountain sunset', num_steps: 20, // 1-20 guidance: 7.5 // 1-20 }); return new Response(image, { headers: { 'Content-Type': 'image/png' } }); ``` ## Speech Recognition ```typescript const audioArray = Array.from(new Uint8Array(await request.arrayBuffer())); const result = await env.AI.run('@cf/openai/whisper', { audio: audioArray }); console.log(result.text); ``` ## Translation ```typescript const result = await env.AI.run('@cf/meta/m2m100-1.2b', { text: 'Hello', source_lang: 'en', target_lang: 'es' }); console.log(result.translated_text); ``` ## REST API ```bash curl https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/meta/llama-3.1-8b-instruct \ -H "Authorization: Bearer $TOKEN" \ -d '{"messages":[{"role":"user","content":"Hello"}]}' ``` ## Error Codes | Code | Meaning | Fix | |------|---------|-----| | 7502 | Model not found | Check spelling | | 7504 | Validation failed | Verify input schema | | 7505 | Rate limited | Reduce rate or upgrade | | 7506 | Context exceeded | Reduce input size | ## Performance Tips 1. **Batch embeddings** - single request for multiple texts 2. **Stream long responses** - reduce perceived latency 3. **Accept cold starts** - first request ~1-3s, subsequent ~100-500ms ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-ai/configuration.md ================================================ # Workers AI Configuration ## wrangler.jsonc ```jsonc { "name": "my-ai-worker", "main": "src/index.ts", "compatibility_date": "2024-01-01", "ai": { "binding": "AI" } } ``` ## TypeScript ```bash npm install --save-dev @cloudflare/workers-types ``` ```typescript interface Env { AI: Ai; } export default { async fetch(request: Request, env: Env) { const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [{ role: 'user', content: 'Hello' }] }); return Response.json(response); } }; ``` ## Local Development ```bash wrangler dev --remote # Required for AI - no local inference ``` ## REST API ```typescript const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/meta/llama-3.1-8b-instruct`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_TOKEN}` }, body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] }) } ); ``` Create API token at: dash.cloudflare.com/profile/api-tokens (Workers AI - Read permission) ## SDK Compatibility **OpenAI SDK:** ```typescript import OpenAI from 'openai'; const client = new OpenAI({ apiKey: env.CLOUDFLARE_API_TOKEN, baseURL: `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/ai/v1` }); ``` ## Multi-Model Setup ```typescript const MODELS = { chat: '@cf/meta/llama-3.1-8b-instruct', embed: '@cf/baai/bge-base-en-v1.5', image: '@cf/stabilityai/stable-diffusion-xl-base-1.0' }; ``` ## RAG Setup (with Vectorize) ```jsonc { "ai": { "binding": "AI" }, "vectorize": { "bindings": [{ "binding": "VECTORIZE", "index_name": "embeddings-index" }] } } ``` ## Troubleshooting | Error | Fix | |-------|-----| | `env.AI is undefined` | Check `ai` binding in wrangler.jsonc | | Local AI doesn't work | Use `wrangler dev --remote` | | Type 'Ai' not found | Install `@cloudflare/workers-types` | | @cloudflare/ai package error | Don't install - use native binding | ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-ai/gotchas.md ================================================ # Workers AI Gotchas ## Critical: @cloudflare/ai is DEPRECATED ```typescript // ❌ WRONG - Don't install @cloudflare/ai import Ai from '@cloudflare/ai'; // ✅ CORRECT - Use native binding export default { async fetch(request: Request, env: Env) { await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [...] }); } } ``` ## Development ### "AI inference doesn't work locally" ```bash # ❌ Local AI doesn't work wrangler dev # ✅ Use remote wrangler dev --remote ``` ### "env.AI is undefined" Add binding to wrangler.jsonc: ```jsonc { "ai": { "binding": "AI" } } ``` ## API Responses ### Embedding response shape varies ```typescript // @cf/baai/bge-base-en-v1.5 returns: { data: [[0.1, 0.2, ...]] } const embedding = response.data[0]; // Get first element ``` ### Stream returns ReadableStream ```typescript const stream = await env.AI.run(model, { messages: [...], stream: true }); for await (const chunk of stream) { console.log(chunk.response); } ``` ## Rate Limits & Pricing | Model Type | Neurons/Request | |------------|-----------------| | Small text (7B) | ~50-200 | | Large text (70B) | ~500-2000 | | Embeddings | ~5-20 | | Image gen | ~10,000+ | **Free tier**: 10,000 neurons/day ```typescript // ❌ EXPENSIVE - 70B model await env.AI.run('@cf/meta/llama-3.1-70b-instruct', ...); // ✅ CHEAPER - Use smallest that works await env.AI.run('@cf/meta/llama-3.1-8b-instruct', ...); ``` ## Model-Specific ### Function calling Only `@cf/meta/llama-3.1-*` and `mistral-7b-instruct-v0.2` support tools. ### Empty response Check context limits (2K-8K tokens). Validate input structure. ### Inconsistent responses Set `temperature: 0` for deterministic outputs. ### Cold start latency First request: 1-3s. Use AI Gateway caching for frequent prompts. ## TypeScript ```typescript interface Env { AI: Ai; // From @cloudflare/workers-types } interface TextGenerationResponse { response: string; } interface EmbeddingResponse { data: number[][]; shape: number[]; } ``` ## Common Errors ### 7502: Model not found Check exact model name at developers.cloudflare.com/workers-ai/models/ ### 7504: Input validation failed ```typescript // Text gen requires messages array await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [{ role: 'user', content: 'Hello' }] // ✅ }); // Embeddings require text await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: 'Hello' }); // ✅ ``` ## Vercel AI SDK Integration ```typescript import { openai } from '@ai-sdk/openai'; const model = openai('gpt-3.5-turbo', { baseURL: 'https://api.cloudflare.com/client/v4/accounts//ai/v1', headers: { Authorization: 'Bearer ' } }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-ai/patterns.md ================================================ # Workers AI Patterns ## RAG (Retrieval-Augmented Generation) ```typescript // 1. Embed query const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: query }); // 2. Search vectors const results = await env.VECTORIZE.query(embedding.data[0], { topK: 5, returnMetadata: true }); // 3. Build context const context = results.matches.map(m => m.metadata?.text).join('\n\n'); // 4. Generate with context const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [ { role: 'system', content: `Answer based on:\n\n${context}` }, { role: 'user', content: query } ] }); ``` ## Streaming (SSE) ```typescript const stream = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages, stream: true }); const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); (async () => { for await (const chunk of stream) { await writer.write(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); } await writer.write(new TextEncoder().encode('data: [DONE]\n\n')); await writer.close(); })(); return new Response(readable, { headers: { 'Content-Type': 'text/event-stream' } }); ``` ## Error Handling & Retry ```typescript async function runWithRetry(env, model, input, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await env.AI.run(model, input); } catch (error) { if (error.message?.includes('7505') && attempt < maxRetries - 1) { await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000)); continue; } throw error; } } } ``` ## Model Fallback ```typescript try { return await env.AI.run('@cf/meta/llama-3.1-70b-instruct', { messages }); } catch { return await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages }); } ``` ## Prompt Patterns ```typescript // System prompts const PROMPTS = { json: 'Respond with valid JSON only.', concise: 'Keep responses brief.', cot: 'Think step by step before answering.' }; // Few-shot messages: [ { role: 'system', content: 'Extract as JSON' }, { role: 'user', content: 'John bought 3 apples for $5' }, { role: 'assistant', content: '{"name":"John","item":"apples","qty":3}' }, { role: 'user', content: actualInput } ] ``` ## Parallel Execution ```typescript const [sentiment, summary, embedding] = await Promise.all([ env.AI.run('@cf/mistral/mistral-7b-instruct-v0.1', { messages: sentimentPrompt }), env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: summaryPrompt }), env.AI.run('@cf/baai/bge-base-en-v1.5', { text }) ]); ``` ## Cost Optimization | Task | Model | Neurons | |------|-------|---------| | Classify | `@cf/mistral/mistral-7b-instruct-v0.1` | ~50 | | Chat | `@cf/meta/llama-3.1-8b-instruct` | ~200 | | Complex | `@cf/meta/llama-3.1-70b-instruct` | ~2000 | | Embed | `@cf/baai/bge-base-en-v1.5` | ~10 | ```typescript // Batch embeddings const response = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: textsArray // Process multiple at once }); ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-for-platforms/README.md ================================================ # Cloudflare Workers for Platforms Multi-tenant platform with isolated customer code execution at scale. ## Use Cases - Multi-tenant SaaS running customer code - AI-generated code execution in secure sandboxes - Programmable platforms with isolated compute - Edge functions/serverless platforms - Website builders with static + dynamic content - Unlimited app deployment at scale **NOT for general Workers** - only for Workers for Platforms architecture. ## Quick Start **One-click deploy:** [Platform Starter Kit](https://github.com/cloudflare/workers-for-platforms-example) deploys complete WfP setup with dispatch namespace, dispatch worker, and user worker example. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/workers-for-platforms-example) **Manual setup:** See [configuration.md](./configuration.md) for namespace creation and dispatch worker configuration. ## Key Features - Unlimited Workers per namespace (no script limits) - Automatic tenant isolation - Custom CPU/subrequest limits per customer - Hostname routing (subdomains/vanity domains) - Egress/ingress control - Static assets support - Tags for bulk operations ## Architecture **4 Components:** 1. **Dispatch Namespace** - Container for unlimited customer Workers, automatic isolation (untrusted mode by default - no request.cf access, no shared cache) 2. **Dynamic Dispatch Worker** - Entry point, routes requests, enforces platform logic (auth, limits, validation) 3. **User Workers** - Customer code in isolated sandboxes, API-deployed, optional bindings (KV/D1/R2/DO) 4. **Outbound Worker** (optional) - Intercepts external fetch, controls egress, logs subrequests (blocks TCP socket connect() API) **Request Flow:** ``` Request → Dispatch Worker → Determines user Worker → env.DISPATCHER.get("customer") → User Worker executes (Outbound Worker for external fetch) → Response → Dispatch Worker → Client ``` ## Decision Trees ### When to Use Workers for Platforms ``` Need to run code? ├─ Your code only → Regular Workers ├─ Customer/AI code → Workers for Platforms └─ Untrusted code in sandbox → Workers for Platforms OR Sandbox API ``` ### Routing Strategy Selection ``` Hostname routing needed? ├─ Subdomains only (*.saas.com) → `*.saas.com/*` route + subdomain extraction ├─ Custom domains → `*/*` wildcard + Cloudflare for SaaS + KV/metadata routing └─ Path-based (/customer/app) → Any route + path parsing ``` ### Isolation Mode Selection ``` Worker mode? ├─ Running customer code → Untrusted (default) ├─ Need request.cf geolocation → Trusted mode ├─ Internal platform, controlled code → Trusted mode with cache key prefixes └─ Maximum isolation → Untrusted + unique resources per customer ``` ## In This Reference | File | Purpose | When to Read | |------|---------|--------------| | [configuration.md](./configuration.md) | Namespace setup, dispatch worker config | First-time setup, changing limits | | [api.md](./api.md) | User worker API, dispatch API, outbound worker | Deploying workers, SDK integration | | [patterns.md](./patterns.md) | Multi-tenancy, routing, egress control | Planning architecture, scaling | | [gotchas.md](./gotchas.md) | Limits, isolation issues, best practices | Debugging, production prep | ## See Also - [workers](../workers/) - Core Workers runtime documentation - [durable-objects](../durable-objects/) - Stateful multi-tenant patterns - [sandbox](../sandbox/) - Alternative for untrusted code execution - [Reference Architecture: Programmable Platforms](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/programmable-platforms/) - [Reference Architecture: AI Vibe Coding Platform](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-vibe-coding-platform/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-for-platforms/api.md ================================================ # API Operations ## Deploy User Worker ```bash curl -X PUT \ "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE/scripts/$SCRIPT_NAME" \ -H "Authorization: Bearer $API_TOKEN" \ -F 'metadata={"main_module": "worker.mjs"};type=application/json' \ -F 'worker.mjs=@worker.mjs;type=application/javascript+module' ``` ### TypeScript SDK ```typescript import Cloudflare from "cloudflare"; const client = new Cloudflare({ apiToken: process.env.API_TOKEN }); const scriptFile = new File([scriptContent], `${scriptName}.mjs`, { type: "application/javascript+module", }); await client.workersForPlatforms.dispatch.namespaces.scripts.update( namespace, scriptName, { account_id: accountId, metadata: { main_module: `${scriptName}.mjs` }, files: [scriptFile], } ); ``` ## TypeScript Types ```typescript import type { DispatchNamespace } from '@cloudflare/workers-types'; interface DispatchNamespace { get(name: string, options?: Record, dispatchOptions?: DynamicDispatchOptions): Fetcher; } interface DynamicDispatchOptions { limits?: DynamicDispatchLimits; outbound?: Record; } interface DynamicDispatchLimits { cpuMs?: number; // Max CPU milliseconds subRequests?: number; // Max fetch() calls } // Usage const userWorker = env.DISPATCHER.get('customer-123', {}, { limits: { cpuMs: 50, subRequests: 20 }, outbound: { customerId: '123', url: request.url } }); ``` ## Deploy with Bindings ```bash curl -X PUT ".../scripts/$SCRIPT_NAME" \ -F 'metadata={ "main_module": "worker.mjs", "bindings": [ {"type": "kv_namespace", "name": "MY_KV", "namespace_id": "'$KV_ID'"} ], "tags": ["customer-123", "production"], "compatibility_date": "2026-01-01" // Use current date for new projects };type=application/json' \ -F 'worker.mjs=@worker.mjs;type=application/javascript+module' ``` ## List/Delete Workers ```bash # List curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE/scripts" \ -H "Authorization: Bearer $API_TOKEN" # Delete by name curl -X DELETE ".../scripts/$SCRIPT_NAME" -H "Authorization: Bearer $API_TOKEN" # Delete by tag curl -X DELETE ".../scripts?tags=customer-123%3Ayes" -H "Authorization: Bearer $API_TOKEN" ``` **Pagination:** SDK supports async iteration. Manual: add `?per_page=100&page=1` query params. ## Static Assets **3-step process:** Create session → Upload files → Deploy Worker ### 1. Create Upload Session ```bash curl -X POST ".../scripts/$SCRIPT_NAME/assets-upload-session" \ -H "Authorization: Bearer $API_TOKEN" \ -d '{ "manifest": { "/index.html": {"hash": "08f1dfda4574284ab3c21666d1ee8c7d4", "size": 1234} } }' # Returns: jwt, buckets ``` **Hash:** SHA-256 truncated to first 16 bytes (32 hex characters) ### 2. Upload Files ```bash curl -X POST ".../workers/assets/upload?base64=true" \ -H "Authorization: Bearer $UPLOAD_JWT" \ -F '08f1dfda4574284ab3c21666d1ee8c7d4=' # Returns: completion jwt ``` **Multiple buckets:** Upload to all returned bucket URLs (typically 2 for redundancy) using same JWT and hash. ### 3. Deploy with Assets ```bash curl -X PUT ".../scripts/$SCRIPT_NAME" \ -F 'metadata={ "main_module": "index.js", "assets": {"jwt": ""}, "bindings": [{"type": "assets", "name": "ASSETS"}] };type=application/json' \ -F 'index.js=export default {...};type=application/javascript+module' ``` **Asset Isolation:** Assets shared across namespace by default. For customer isolation, salt hash: `sha256(customerId + fileContents).slice(0, 32)` ## Dispatch Workers ### Subdomain Routing ```typescript export default { async fetch(request: Request, env: Env): Promise { const userWorkerName = new URL(request.url).hostname.split(".")[0]; const userWorker = env.DISPATCHER.get(userWorkerName); return await userWorker.fetch(request); }, }; ``` ### Path Routing ```typescript const pathParts = new URL(request.url).pathname.split("/").filter(Boolean); const userWorker = env.DISPATCHER.get(pathParts[0]); return await userWorker.fetch(request); ``` ### KV Routing ```typescript const hostname = new URL(request.url).hostname; const userWorkerName = await env.ROUTING_KV.get(hostname); const userWorker = env.DISPATCHER.get(userWorkerName); return await userWorker.fetch(request); ``` ## Outbound Workers Control external fetch from user Workers: ### Configure ```typescript const userWorker = env.DISPATCHER.get( workerName, {}, { outbound: { customer_context: { customer_name: workerName, url: request.url } } } ); ``` ### Implement ```typescript export default { async fetch(request: Request, env: Env): Promise { const customerName = env.customer_name; const url = new URL(request.url); // Block domains if (["malicious.com"].some(d => url.hostname.includes(d))) { return new Response("Blocked", { status: 403 }); } // Inject auth if (url.hostname === "api.example.com") { const headers = new Headers(request.headers); headers.set("Authorization", `Bearer ${generateJWT(customerName)}`); return fetch(new Request(request, { headers })); } return fetch(request); }, }; ``` **Note:** Doesn't intercept DO/mTLS fetch. See [README.md](./README.md), [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-for-platforms/configuration.md ================================================ # Configuration ## Dispatch Namespace Binding ### wrangler.jsonc ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "dispatch_namespaces": [{ "binding": "DISPATCHER", "namespace": "production" }] } ``` ## Worker Isolation Mode Workers in a namespace run in **untrusted mode** by default for security: - No access to `request.cf` object - Isolated cache per Worker (no shared cache) - `caches.default` disabled ### Enable Trusted Mode For internal platforms where you control all code: ```bash curl -X PUT \ "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE" \ -H "Authorization: Bearer $API_TOKEN" \ -d '{"name": "'$NAMESPACE'", "trusted_workers": true}' ``` **Caveats:** - Workers share cache within namespace (use cache key prefixes: `customer-${id}:${key}`) - `request.cf` object accessible - Redeploy existing Workers after enabling trusted mode **When to use:** Internal platforms, A/B testing platforms, need geolocation data ### With Outbound Worker ```jsonc { "dispatch_namespaces": [{ "binding": "DISPATCHER", "namespace": "production", "outbound": { "service": "outbound-worker", "parameters": ["customer_context"] } }] } ``` ## Wrangler Commands ```bash wrangler dispatch-namespace list wrangler dispatch-namespace get production wrangler dispatch-namespace create production wrangler dispatch-namespace delete staging wrangler dispatch-namespace rename old new ``` ## Custom Limits Set CPU time and subrequest limits per invocation: ```typescript const userWorker = env.DISPATCHER.get( workerName, {}, { limits: { cpuMs: 10, // Max CPU ms subRequests: 5 // Max fetch() calls } } ); ``` Handle limit violations: ```typescript try { return await userWorker.fetch(request); } catch (e) { if (e.message.includes("CPU time limit")) { return new Response("CPU limit exceeded", { status: 429 }); } throw e; } ``` ## Static Assets Deploy HTML/CSS/images with Workers. See [api.md](./api.md#static-assets) for upload process. ### Wrangler ```jsonc { "name": "customer-site", "main": "./src/index.js", "assets": { "directory": "./public", "binding": "ASSETS" } } ``` ```bash npx wrangler deploy --name customer-site --dispatch-namespace production ``` ### Dashboard Deployment Alternative to CLI: 1. Upload Worker file in dashboard 2. Add `--dispatch-namespace` flag: `wrangler deploy --dispatch-namespace production` 3. Or configure in wrangler.jsonc under `dispatch_namespaces` See [api.md](./api.md) for programmatic deployment via REST API or SDK. ## Tags Organize/search Workers (max 8/script): ```bash # Set tags curl -X PUT ".../tags" -d '["customer-123", "pro", "production"]' # Filter by tag curl ".../scripts?tags=production%3Ayes" # Delete by tag curl -X DELETE ".../scripts?tags=customer-123%3Ayes" ``` Common patterns: `customer-123`, `free|pro|enterprise`, `production|staging` ## Bindings **Supported binding types:** 29 total including KV, D1, R2, Durable Objects, Analytics Engine, Service, Assets, Queue, Vectorize, Hyperdrive, Workflow, AI, Browser, and more. Add via API metadata (see [api.md](./api.md#deploy-with-bindings)): ```json { "bindings": [ {"type": "kv_namespace", "name": "USER_KV", "namespace_id": "..."}, {"type": "r2_bucket", "name": "STORAGE", "bucket_name": "..."}, {"type": "d1", "name": "DB", "id": "..."} ] } ``` Preserve existing bindings: ```json { "bindings": [{"type": "r2_bucket", "name": "STORAGE", "bucket_name": "new"}], "keep_bindings": ["kv_namespace", "d1"] // Preserves existing bindings of these types } ``` For complete binding type reference, see [bindings](../bindings/) documentation See [README.md](./README.md), [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-for-platforms/gotchas.md ================================================ # Gotchas & Limits ## Common Errors ### "Worker not found" **Cause:** Attempting to get Worker that doesn't exist in namespace **Solution:** Catch error and return 404: ```typescript try { const userWorker = env.DISPATCHER.get(workerName); return userWorker.fetch(request); } catch (e) { if (e.message.startsWith("Worker not found")) { return new Response("Worker not found", { status: 404 }); } throw e; // Re-throw unexpected errors } ``` ### "CPU time limit exceeded" **Cause:** User Worker exceeded configured CPU time limit **Solution:** Track violations in Analytics Engine and return 429 response; consider adjusting limits per customer tier ### "Hostname Routing Issues" **Cause:** DNS proxy settings causing routing problems **Solution:** Use `*/*` wildcard route which works regardless of proxy settings for orange-to-orange routing ### "Bindings Lost on Update" **Cause:** Not using `keep_bindings` flag when updating Worker **Solution:** Use `keep_bindings: true` in API requests to preserve existing bindings during updates ### "Tag Filtering Not Working" **Cause:** Special characters not URL encoded in tag filters **Solution:** URL encode tags (e.g., `tags=production%3Ayes`) and avoid special chars like `,` and `&` ### "Deploy Failures with ES Modules" **Cause:** Incorrect upload format for ES modules **Solution:** Use multipart form upload, specify `main_module` in metadata, and set file type to `application/javascript+module` ### "Static Asset Upload Failed" **Cause:** Invalid hash format, expired token, or incorrect encoding **Solution:** Hash must be first 16 bytes (32 hex chars) of SHA-256, upload within 1 hour of session creation, deploy within 1 hour of upload completion, and Base64 encode file contents ### "Outbound Worker Not Intercepting Calls" **Cause:** Outbound Workers don't intercept Durable Object or mTLS binding fetch **Solution:** Plan egress control accordingly; not all fetch calls are intercepted ### "TCP Socket Connection Failed" **Cause:** Outbound Worker enabled blocks `connect()` API for TCP sockets **Solution:** Outbound Workers only intercept `fetch()` calls; TCP socket connections unavailable when outbound configured. Remove outbound if TCP needed, or use proxy pattern. ### "API Rate Limit Exceeded" **Cause:** Exceeded Cloudflare API rate limits (1200 requests per 5 minutes per account, 200 requests per second per IP) **Solution:** Implement exponential backoff: ```typescript async function deployWithBackoff(deploy: () => Promise, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await deploy(); } catch (e) { if (e.status === 429 && i < maxRetries - 1) { await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); continue; } throw e; } } } ``` ### "Gradual Deployment Not Supported" **Cause:** Attempted to use gradual deployments with user Workers **Solution:** Gradual deployments not supported for Workers in dispatch namespaces. Use all-at-once deployment with staged rollout via dispatch worker logic (feature flags, percentage-based routing). ### "Asset Session Expired" **Cause:** Upload JWT expired (1 hour validity) or completion token expired (1 hour after upload) **Solution:** Complete asset upload within 1 hour of session creation, and deploy Worker within 1 hour of upload completion. For large uploads, batch files or increase upload parallelism. ## Platform Limits | Limit | Value | Notes | |-------|-------|-------| | Workers per namespace | Unlimited | Unlike regular Workers (500 per account) | | Namespaces per account | Unlimited | Best practice: 1 production + 1 staging | | Max tags per Worker | 8 | For filtering and organization | | Worker mode | Untrusted (default) | No `request.cf` access unless trusted mode | | Cache isolation | Per-Worker (untrusted) | Shared in trusted mode with key prefixes | | Durable Object namespaces | Unlimited | No per-account limit for WfP | | Gradual Deployments | Not supported | All-at-once only | | `caches.default` | Disabled (untrusted) | Use Cache API with custom keys | ## Asset Upload Limits | Limit | Value | Notes | |-------|-------|-------| | Upload session JWT validity | 1 hour | Must complete upload within this time | | Completion token validity | 1 hour | Must deploy within this time after upload | | Asset hash format | First 16 bytes SHA-256 | 32 hex characters | | Base64 encoding | Required | For binary files | ## API Rate Limits | Limit Type | Value | Scope | |------------|-------|-------| | Client API | 1200 requests / 5 min | Per account | | Client API | 200 requests / sec | Per IP address | | GraphQL | Varies by query cost | Query complexity | See [Cloudflare API Rate Limits](https://developers.cloudflare.com/fundamentals/api/reference/limits/) for details. ## Operational Limits | Operation | Limit | Notes | |-----------|-------|-------| | CPU time (custom limits) | Up to Workers plan limit | Set per-invocation in dispatch worker | | Subrequests (custom limits) | Up to Workers plan limit | Set per-invocation in dispatch worker | | Outbound Worker subrequests | Not intercepted for DO/mTLS | Only regular fetch() calls | | TCP sockets with outbound | Disabled | `connect()` API unavailable | See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-for-platforms/patterns.md ================================================ # Multi-Tenant Patterns ## Billing by Plan ```typescript interface Env { DISPATCHER: DispatchNamespace; CUSTOMERS_KV: KVNamespace; } export default { async fetch(request: Request, env: Env): Promise { const userWorkerName = new URL(request.url).hostname.split(".")[0]; const customerPlan = await env.CUSTOMERS_KV.get(userWorkerName); const plans = { enterprise: { cpuMs: 50, subRequests: 50 }, pro: { cpuMs: 20, subRequests: 20 }, free: { cpuMs: 10, subRequests: 5 }, }; const limits = plans[customerPlan as keyof typeof plans] || plans.free; const userWorker = env.DISPATCHER.get(userWorkerName, {}, { limits }); return await userWorker.fetch(request); }, }; ``` ## Resource Isolation **Complete isolation:** Create unique resources per customer - KV namespace per customer - D1 database per customer - R2 bucket per customer ```typescript const bindings = [{ type: "kv_namespace", name: "USER_KV", namespace_id: `customer-${customerId}-kv` }]; ``` ## Hostname Routing ### Wildcard Route (Recommended) Configure `*/*` route on SaaS domain → dispatch Worker **Benefits:** - Supports subdomains + custom vanity domains - No per-route limits (regular Workers limited to 100 routes) - Programmatic control - Works with any DNS proxy settings **Setup:** 1. Cloudflare for SaaS custom hostnames 2. Fallback origin (dummy `A 192.0.2.0` if Worker is origin) 3. DNS CNAME to SaaS domain 4. `*/*` route → dispatch Worker 5. Routing logic in dispatch Worker ```typescript export default { async fetch(request: Request, env: Env): Promise { const hostname = new URL(request.url).hostname; const hostnameData = await env.ROUTING_KV.get(`hostname:${hostname}`, { type: "json" }); if (!hostnameData?.workerName) { return new Response("Hostname not configured", { status: 404 }); } const userWorker = env.DISPATCHER.get(hostnameData.workerName); return await userWorker.fetch(request); }, }; ``` ### Subdomain-Only 1. Wildcard DNS: `*.saas.com` → origin 2. Route: `*.saas.com/*` → dispatch Worker 3. Extract subdomain for routing ### Orange-to-Orange (O2O) Behavior When customers use Cloudflare and CNAME to your Workers domain: | Scenario | Behavior | Route Pattern | |----------|----------|---------------| | Customer not on Cloudflare | Standard routing | `*/*` or `*.domain.com/*` | | Customer on Cloudflare (proxied CNAME) | Invokes Worker at edge | `*/*` required | | Customer on Cloudflare (DNS-only CNAME) | Standard routing | Any route works | **Recommendation:** Always use `*/*` wildcard for consistent O2O behavior. ### Custom Metadata Routing For Cloudflare for SaaS: Store worker name in custom hostname `custom_metadata`, retrieve in dispatch worker to route requests. Requires custom hostnames as subdomains of your domain. ## Observability ### Logpush - Enable on dispatch Worker → captures all user Worker logs - Filter by `Outcome` or `Script Name` ### Tail Workers - Real-time logs with custom formatting - Receives HTTP status, `console.log()`, exceptions, diagnostics ### Analytics Engine ```typescript // Track violations env.ANALYTICS.writeDataPoint({ indexes: [customerName], blobs: ["cpu_limit_exceeded"], }); ``` ### GraphQL ```graphql query { viewer { accounts(filter: {accountTag: $accountId}) { workersInvocationsAdaptive(filter: {dispatchNamespaceName: "production"}) { sum { requests errors cpuTime } } } } } ``` ## Use Case Implementations ### AI Code Execution ```typescript async function deployGeneratedCode(name: string, code: string) { const file = new File([code], `${name}.mjs`, { type: "application/javascript+module" }); await client.workersForPlatforms.dispatch.namespaces.scripts.update("production", name, { account_id: accountId, metadata: { main_module: `${name}.mjs`, tags: [name, "ai-generated"] }, files: [file], }); } // Short limits for untrusted code const userWorker = env.DISPATCHER.get(sessionId, {}, { limits: { cpuMs: 5, subRequests: 3 } }); ``` **VibeSDK:** For AI-powered code generation + deployment platforms, see [VibeSDK](https://github.com/cloudflare/vibesdk) - handles AI generation, sandbox execution, live preview, and deployment. Reference: [AI Vibe Coding Platform Architecture](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-vibe-coding-platform/) ### Edge Functions Platform ```typescript // Route: /customer-id/function-name const [customerId, functionName] = new URL(request.url).pathname.split("/").filter(Boolean); const workerName = `${customerId}-${functionName}`; const userWorker = env.DISPATCHER.get(workerName); ``` ### Website Builder - Deploy static assets + Worker code - See [api.md](./api.md#static-assets) for full implementation - Salt hashes for asset isolation ## Best Practices ### Architecture - One namespace per environment (production, staging) - Platform logic in dispatch Worker (auth, rate limiting, validation) - Isolation automatic (no shared cache, untrusted mode) ### Routing - Use `*/*` wildcard routes - Store mappings in KV - Handle missing Workers gracefully ### Limits & Security - Set custom limits by plan - Track violations with Analytics Engine - Use outbound Workers for egress control - Sanitize responses ### Tags - Tag all Workers: customer ID, plan, environment - Enable bulk operations - Filter efficiently See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-playground/README.md ================================================ # Cloudflare Workers Playground Skill Reference ## Overview Cloudflare Workers Playground is a browser-based sandbox for instantly experimenting with, testing, and deploying Cloudflare Workers without authentication or setup. This skill provides patterns, APIs, and best practices specifically for Workers Playground development. **URL:** [workers.cloudflare.com/playground](https://workers.cloudflare.com/playground) ## ⚠️ Playground Constraints **Playground is NOT production-equivalent:** - ✅ Real Workers runtime, instant testing, shareable URLs - ❌ No TypeScript (JavaScript only) - ❌ No bindings (KV, D1, R2, Durable Objects) - ❌ No environment variables or secrets - ❌ ES modules only (no Service Worker format) - ⚠️ Safari broken (use Chrome/Firefox) **For production:** Use `wrangler` CLI. Playground is for rapid prototyping. ## Quick Start Minimal Worker: ```javascript export default { async fetch(request, env, ctx) { return new Response('Hello World'); } }; ``` JSON API: ```javascript export default { async fetch(request, env, ctx) { const data = { message: 'Hello', timestamp: Date.now() }; return Response.json(data); } }; ``` Proxy with modification: ```javascript export default { async fetch(request, env, ctx) { const response = await fetch('https://example.com'); const modified = new Response(response.body, response); modified.headers.set('X-Custom-Header', 'added-by-worker'); return modified; } }; ``` Import from CDN: ```javascript import { Hono } from 'https://esm.sh/hono@3'; export default { async fetch(request) { const app = new Hono(); app.get('/', (c) => c.text('Hello Hono!')); return app.fetch(request); } }; ``` ## Reading Order 1. **[configuration.md](configuration.md)** - Start here: playground setup, constraints, deployment 2. **[api.md](api.md)** - Core APIs: Request, Response, ExecutionContext, fetch, Cache 3. **[patterns.md](patterns.md)** - Common use cases: routing, proxying, A/B testing, multi-module code 4. **[gotchas.md](gotchas.md)** - Troubleshooting: errors, browser issues, limits, best practices ## 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 ## Key Features **No Setup Required:** - Open URL and start coding - No CLI, no account, no config files - Code executes in real Cloudflare Workers runtime **Instant Preview:** - Live preview pane with browser tab or HTTP tester - Auto-reload on code changes - DevTools integration (right-click → Inspect) **Share & Deploy:** - Copy Link generates permanent shareable URL - Deploy button publishes to production in ~30 seconds - Get `*.workers.dev` subdomain immediately ## Common Use Cases - **API development:** Test endpoints before wrangler setup - **Learning Workers:** Experiment with APIs without local environment - **Prototyping:** Quick POCs for edge logic - **Sharing examples:** Generate shareable links for bug reports or demos - **Framework testing:** Import from CDN (Hono, itty-router, etc.) ## Limitations vs Production | Feature | Playground | Production (wrangler) | |---------|------------|----------------------| | Language | JavaScript only | JS + TypeScript | | Bindings | None | KV, D1, R2, DO, AI, etc. | | Environment vars | None | Full support | | Module format | ES only | ES + Service Worker | | CPU time | 10ms (Free plan) | 10ms Free / 50ms Paid | | Custom domains | No | Yes | | Analytics | No | Yes | ## See Also - [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) - [Workers Examples](https://developers.cloudflare.com/workers/examples/) - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) - [Workers API Reference](https://developers.cloudflare.com/workers/runtime-apis/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-playground/api.md ================================================ # Workers Playground API ## Handler ```javascript export default { async fetch(request, env, ctx) { // request: Request, env: {} (empty in playground), ctx: ExecutionContext return new Response('Hello'); } }; ``` ## Request ```javascript const method = request.method; // "GET", "POST" const url = new URL(request.url); // Parse URL const headers = request.headers; // Headers object const body = await request.json(); // Read body (consumes stream) const clone = request.clone(); // Clone before reading body // Query params url.searchParams.get('page'); // Single value url.searchParams.getAll('tag'); // Array // Cloudflare metadata request.cf.country; // "US" request.cf.colo; // "SFO" ``` ## Response ```javascript // Text return new Response('Hello', { status: 200 }); // JSON return Response.json({ data }, { status: 200, headers: {...} }); // Redirect return Response.redirect('/new-path', 301); // Modify existing const modified = new Response(response.body, response); modified.headers.set('X-Custom', 'value'); ``` ## ExecutionContext ```javascript // Background work (after response sent) ctx.waitUntil(fetch('https://logs.example.com', { method: 'POST', body: '...' })); return new Response('OK'); // Returns immediately ``` ## Fetch ```javascript const response = await fetch('https://api.example.com'); const data = await response.json(); // With options await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Alice' }) }); ``` ## Cache ```javascript const cache = caches.default; // Check cache let response = await cache.match(request); if (!response) { response = await fetch(origin); await cache.put(request, response.clone()); // Clone before put! } return response; ``` ## Crypto ```javascript crypto.randomUUID(); // UUID v4 crypto.getRandomValues(new Uint8Array(16)); // SHA-256 hash const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data)); ``` ## Limits (Playground = Free Plan) | Resource | Limit | |----------|-------| | CPU time | 10ms | | Subrequests | 50 | | Memory | 128 MB | ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-playground/configuration.md ================================================ # Configuration ## Getting Started Navigate to [workers.cloudflare.com/playground](https://workers.cloudflare.com/playground) - **No account required** for testing - **No CLI or local setup** needed - Code executes in real Cloudflare Workers runtime - Share code via URL (never expires) ## Playground Constraints ⚠️ **Important Limitations** | Constraint | Playground | Production Workers | |------------|------------|-------------------| | **Module Format** | ES modules only | ES modules or Service Worker | | **TypeScript** | Not supported (JS only) | Supported via build step | | **Bindings** | Not available | KV, D1, R2, Durable Objects, etc. | | **wrangler.toml** | Not used | Required for config | | **Environment Variables** | Not available | Full support | | **Secrets** | Not available | Full support | | **Custom Domains** | Not available | Full support | **Playground is for rapid prototyping only.** For production apps, use `wrangler` CLI. ## Code Editor ### Syntax Requirements Must export default object with `fetch` handler: ```javascript export default { async fetch(request, env, ctx) { return new Response('Hello World'); } }; ``` **Key Points:** - Must use ES modules (`export default`) - `fetch` method receives `(request, env, ctx)` - Must return `Response` object - TypeScript not supported (use plain JavaScript) ### Multi-Module Code Import from external URLs or inline modules: ```javascript // Import from CDN import { Hono } from 'https://esm.sh/hono@3'; // Or paste library code and import relatively // (See patterns.md for multi-module examples) export default { async fetch(request) { const app = new Hono(); app.get('/', (c) => c.text('Hello')); return app.fetch(request); } }; ``` ## Preview Panel ### Browser Tab Default interactive preview with address bar: - Enter custom URL paths - Automatic reload on code changes - DevTools available (right-click → Inspect) ### HTTP Test Panel Switch to **HTTP** tab for raw HTTP testing: - Change HTTP method (GET, POST, PUT, DELETE, PATCH, etc.) - Add/edit request headers - Modify request body (JSON, form data, text) - View response headers and body - Test different content types Example HTTP test: ``` Method: POST URL: /api/users Headers: Content-Type: application/json Authorization: Bearer token123 Body: { "name": "Alice", "email": "alice@example.com" } ``` ## Sharing Code **Copy Link** button generates shareable URL: - Code embedded in URL fragment - Links never expire - No account required - Can be bookmarked for later Example: `https://workers.cloudflare.com/playground#abc123...` ## Deploying from Playground Click **Deploy** button to move code to production: 1. **Log in** to Cloudflare account (creates free account if needed) 2. **Review** Worker name and code 3. **Deploy** to global network (takes ~30 seconds) 4. **Get URL**: Deployed to `.workers.dev` subdomain 5. **Manage** from dashboard: add bindings, custom domains, analytics **After deploy:** - Code runs on Cloudflare's global network (300+ cities) - Can add KV, D1, R2, Durable Objects bindings - Configure custom domains and routes - View analytics and logs - Set environment variables and secrets **Note:** Deployed Workers are production-ready but start on Free plan (100k requests/day). ## Browser Compatibility | Browser | Status | Notes | |---------|--------|-------| | Chrome/Edge | ✅ Full support | Recommended | | Firefox | ✅ Full support | Works well | | Safari | ⚠️ Broken | Preview fails with "PreviewRequestFailed" | **Safari users:** Use Chrome, Firefox, or Edge for Workers Playground. ## DevTools Integration 1. **Open preview** in browser tab 2. **Right-click** → Inspect Element 3. **Console tab** shows Worker logs: - `console.log()` output - Uncaught errors - Network requests (subrequests) **Note:** DevTools show client-side console, not Worker execution logs. For production logging, use Logpush or Tail Workers. ## Limits in Playground Same as production Free plan: | Resource | Limit | Notes | |----------|-------|-------| | CPU time | 10ms | Per request | | Memory | 128 MB | Per request | | Script size | 1 MB | After compression | | Subrequests | 50 | Outbound fetch calls | | Request size | 100 MB | Incoming | | Response size | Unlimited | Outgoing (streamed) | **Exceeding CPU time** throws error immediately. Optimize hot paths or upgrade to Paid plan (50ms CPU). ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-playground/gotchas.md ================================================ # Workers Playground Gotchas ## Platform Limitations | Limitation | Impact | Workaround | |------------|--------|------------| | Safari broken | Preview fails | Use Chrome/Firefox/Edge | | TypeScript unsupported | TS syntax errors | Write plain JS or use JSDoc | | No bindings | `env` always `{}` | Mock data or use external APIs | | No env vars | Can't access secrets | Hardcode for testing | ## Common Runtime Errors ### "Response body already read" ```javascript // ❌ Body consumed twice const body = await request.text(); await fetch(url, { body: request.body }); // Error! // ✅ Clone first const clone = request.clone(); const body = await request.text(); await fetch(url, { body: clone.body }); ``` ### "Worker exceeded CPU time" **Limit:** 10ms (free), 50ms (paid) ```javascript // ✅ Move slow work to background ctx.waitUntil(fetch('https://analytics.example.com', {...})); return new Response('OK'); // Return immediately ``` ### "Too many subrequests" **Limit:** 50 (free), 1000 (paid) ```javascript // ❌ 100 individual fetches // ✅ Batch into single API call await fetch('https://api.example.com/batch', { body: JSON.stringify({ ids: [...] }) }); ``` ## Best Practices ```javascript // Clone before caching await cache.put(request, response.clone()); return response; // Validate input early if (request.method !== 'POST') return new Response('', { status: 405 }); // Handle errors try { ... } catch (e) { return Response.json({ error: e.message }, { status: 500 }); } ``` ## Limits | Resource | Free | Paid | |----------|------|------| | CPU time | 10ms | 50ms | | Memory | 128 MB | 128 MB | | Subrequests | 50 | 1000 | ## Browser Support | Browser | Status | |---------|--------| | Chrome | ✅ Recommended | | Firefox | ✅ Works | | Edge | ✅ Works | | Safari | ❌ Broken | ## Debugging ```javascript console.log('URL:', request.url); // View in browser DevTools Console ``` **Note:** `console.log` works in playground. For production, use Logpush or Tail Workers. ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-playground/patterns.md ================================================ # Workers Playground Patterns ## JSON API ```javascript export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === '/api/hello') return Response.json({ message: 'Hello' }); if (url.pathname === '/api/echo' && request.method === 'POST') { return Response.json({ received: await request.json() }); } return Response.json({ error: 'Not found' }, { status: 404 }); } }; ``` ## Router Pattern ```javascript const routes = { '/': () => new Response('Home'), '/api/users': () => Response.json([{ id: 1, name: 'Alice' }]) }; export default { async fetch(request) { const handler = routes[new URL(request.url).pathname]; return handler ? handler() : new Response('Not Found', { status: 404 }); } }; ``` ## Proxy Pattern ```javascript export default { async fetch(request) { const url = new URL(request.url); url.hostname = 'api.example.com'; return fetch(url.toString(), { method: request.method, headers: request.headers, body: request.body }); } }; ``` ## CORS Handling ```javascript export default { async fetch(request) { if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE', 'Access-Control-Allow-Headers': 'Content-Type, Authorization' } }); } const response = await fetch('https://api.example.com', request); const modified = new Response(response.body, response); modified.headers.set('Access-Control-Allow-Origin', '*'); return modified; } }; ``` ## Caching ```javascript export default { async fetch(request) { if (request.method !== 'GET') return fetch(request); const cache = caches.default; let response = await cache.match(request); if (!response) { response = await fetch('https://api.example.com'); if (response.status === 200) await cache.put(request, response.clone()); } return response; } }; ``` ## Hono Framework ```javascript import { Hono } from 'https://esm.sh/hono@3'; const app = new Hono(); app.get('/', (c) => c.text('Hello')); app.get('/api/users/:id', (c) => c.json({ id: c.req.param('id') })); app.notFound((c) => c.json({ error: 'Not found' }, 404)); export default app; ``` ## Authentication ```javascript export default { async fetch(request) { const auth = request.headers.get('Authorization'); if (!auth?.startsWith('Bearer ')) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } const token = auth.substring(7); if (token !== 'secret-token') { return Response.json({ error: 'Invalid token' }, { status: 403 }); } return Response.json({ message: 'Authenticated' }); } }; ``` ## Error Handling ```javascript export default { async fetch(request) { try { const response = await fetch('https://api.example.com'); if (!response.ok) throw new Error(`API returned ${response.status}`); return response; } catch (error) { return Response.json({ error: error.message }, { status: 500 }); } } }; ``` **Note:** In-memory state (Maps, variables) resets on Worker cold start. Use Durable Objects or KV for persistence. ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-vpc/README.md ================================================ # Workers VPC Connectivity Connect Cloudflare Workers to private networks and internal infrastructure using TCP Sockets. ## Overview Workers VPC connectivity enables outbound TCP connections from Workers to private resources in AWS, Azure, GCP, on-premises datacenters, or any private network. This is achieved through the **TCP Sockets API** (`cloudflare:sockets`), which provides low-level network access for custom protocols and services. **Key capabilities:** - Direct TCP connections to private IPs and hostnames - TLS/StartTLS support for encrypted connections - Integration with Cloudflare Tunnel for secure private network access - Full control over wire protocols (database protocols, SSH, MQTT, custom TCP) **Note:** This reference documents the TCP Sockets API. For the newer Workers VPC Services product (HTTP-only service bindings with built-in SSRF protection), refer to separate documentation when available. VPC Services is currently in beta (2025+). ## Quick Decision: Which Technology? Need private network connectivity from Workers? | Requirement | Use | Why | |------------|-----|-----| | HTTP/HTTPS APIs in private network | VPC Services (beta, separate docs) | SSRF-safe, declarative bindings | | PostgreSQL/MySQL databases | [Hyperdrive](../hyperdrive/) | Connection pooling, caching, optimized | | Custom TCP protocols (SSH, MQTT, proprietary) | **TCP Sockets (this doc)** | Full protocol control | | Simple HTTP with lowest latency | TCP Sockets + [Smart Placement](../smart-placement/) | Manual optimization | | Expose on-prem to internet (inbound) | [Cloudflare Tunnel](../tunnel/) | Not Worker-specific | ## When to Use TCP Sockets **Use TCP Sockets when you need:** - ✅ Direct control over wire protocols (e.g., Postgres wire protocol, SSH, Redis RESP) - ✅ Non-HTTP protocols (MQTT, SMTP, custom binary protocols) - ✅ StartTLS or custom TLS negotiation - ✅ Streaming binary data over TCP **Don't use TCP Sockets when:** - ❌ You just need HTTP/HTTPS (use `fetch()` or VPC Services) - ❌ You need PostgreSQL/MySQL (use Hyperdrive for pooling) - ❌ You need WebSocket (use native Workers WebSocket) ## Quick Start ```typescript import { connect } from 'cloudflare:sockets'; export default { async fetch(req: Request): Promise { // Connect to private service const socket = connect( { hostname: "db.internal.company.net", port: 5432 }, { secureTransport: "on" } ); try { await socket.opened; // Wait for connection const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("QUERY\r\n")); await writer.close(); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new Response(value); } finally { await socket.close(); } } }; ``` ## Architecture Pattern: Workers + Tunnel Most private network connectivity combines TCP Sockets with Cloudflare Tunnel: ``` ┌─────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Worker │────▶│ TCP Socket │────▶│ Tunnel │────▶│ Private │ │ │ │ (this API) │ │ (cloudflared)│ │ Network │ └─────────┘ └─────────────┘ └──────────────┘ └─────────────┘ ``` 1. Worker opens TCP socket to Tunnel hostname 2. Tunnel endpoint routes to private IP 3. Response flows back through Tunnel to Worker See [configuration.md](./configuration.md) for Tunnel setup details. ## Reading Order 1. **Start here (README.md)** - Overview and decision guide 2. **[api.md](./api.md)** - Socket interface, types, methods 3. **[configuration.md](./configuration.md)** - Wrangler setup, Tunnel integration 4. **[patterns.md](./patterns.md)** - Real-world examples (databases, protocols, error handling) 5. **[gotchas.md](./gotchas.md)** - Limits, blocked ports, common errors ## Key Limits | Limit | Value | |-------|-------| | Max concurrent sockets per request | 6 | | Blocked destinations | Cloudflare IPs, localhost, port 25 | | Scope requirement | Must create in handler (not global) | See [gotchas.md](./gotchas.md) for complete limits and troubleshooting. ## Best Practices 1. **Always close sockets** - Use try/finally blocks 2. **Validate destinations** - Prevent SSRF by allowlisting hosts 3. **Use Hyperdrive for databases** - Better performance than raw TCP 4. **Prefer fetch() for HTTP** - Only use TCP when necessary 5. **Combine with Smart Placement** - Reduce latency to private networks ## Related Technologies - **[Hyperdrive](../hyperdrive/)** - PostgreSQL/MySQL with connection pooling - **[Cloudflare Tunnel](../tunnel/)** - Secure private network access - **[Smart Placement](../smart-placement/)** - Auto-locate Workers near backends - **VPC Services (beta)** - HTTP-only service bindings with SSRF protection (separate docs) ## Reference - [TCP Sockets API Documentation](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) - [Connect to databases guide](https://developers.cloudflare.com/workers/tutorials/connect-to-postgres/) - [Cloudflare Tunnel setup](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-vpc/api.md ================================================ # TCP Sockets API Reference Complete API reference for the Cloudflare Workers TCP Sockets API (`cloudflare:sockets`). ## Core Function: `connect()` ```typescript function connect( address: SocketAddress, options?: SocketOptions ): Socket ``` Creates an outbound TCP connection to the specified address. ### Parameters #### `SocketAddress` ```typescript interface SocketAddress { hostname: string; // DNS hostname or IP address port: number; // TCP port (1-65535, excluding blocked ports) } ``` | Field | Type | Description | Example | |-------|------|-------------|---------| | `hostname` | `string` | Target hostname or IP | `"db.internal.net"`, `"10.0.1.50"` | | `port` | `number` | TCP port number | `5432`, `443`, `22` | DNS names are resolved at connection time. IPv4, IPv6, and private IPs (10.x, 172.16.x, 192.168.x) supported. #### `SocketOptions` ```typescript interface SocketOptions { secureTransport?: "off" | "on" | "starttls"; allowHalfOpen?: boolean; } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | `secureTransport` | `"off" \| "on" \| "starttls"` | `"off"` | TLS mode | | `allowHalfOpen` | `boolean` | `false` | Allow half-closed connections | **`secureTransport` modes:** | Mode | Behavior | Use Case | |------|----------|----------| | `"off"` | Plain TCP, no encryption | Testing, internal trusted networks | | `"on"` | Immediate TLS handshake | HTTPS, secure databases, SSH | | `"starttls"` | Start plain, upgrade later with `startTls()` | Postgres, SMTP, IMAP | **`allowHalfOpen`:** When `false` (default), closing read stream auto-closes write stream. When `true`, streams are independent. ### Returns A `Socket` object with readable/writable streams. ## Socket Interface ```typescript interface Socket { // Streams readable: ReadableStream; writable: WritableStream; // Connection state opened: Promise; closed: Promise; // Methods close(): Promise; startTls(): Socket; } ``` ### Properties #### `readable: ReadableStream` Stream for reading data from the socket. Use `getReader()` to consume data. ```typescript const reader = socket.readable.getReader(); const { done, value } = await reader.read(); // Read one chunk ``` #### `writable: WritableStream` Stream for writing data to the socket. Use `getWriter()` to send data. ```typescript const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("HELLO\r\n")); await writer.close(); ``` #### `opened: Promise` Promise that resolves when connection succeeds, rejects on failure. ```typescript interface SocketInfo { remoteAddress?: string; // May be undefined localAddress?: string; // May be undefined } try { const info = await socket.opened; } catch (error) { // Connection failed } ``` #### `closed: Promise` Promise that resolves when socket is fully closed (both directions). ### Methods #### `close(): Promise` Closes the socket gracefully, waiting for pending writes to complete. ```typescript const socket = connect({ hostname: "api.internal", port: 443 }); try { // Use socket } finally { await socket.close(); // Always call in finally block } ``` #### `startTls(): Socket` Upgrades connection to TLS. Only available when `secureTransport: "starttls"` was specified. ```typescript const socket = connect( { hostname: "db.internal", port: 5432 }, { secureTransport: "starttls" } ); // Send protocol-specific StartTLS command const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("STARTTLS\r\n")); // Upgrade to TLS - use returned socket, not original const secureSocket = socket.startTls(); const secureWriter = secureSocket.writable.getWriter(); ``` ## Complete Example ```typescript import { connect } from 'cloudflare:sockets'; export default { async fetch(req: Request): Promise { const socket = connect({ hostname: "echo.example.com", port: 7 }, { secureTransport: "on" }); try { await socket.opened; const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("Hello, TCP!\n")); await writer.close(); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new Response(value); } finally { await socket.close(); } } }; ``` See [patterns.md](./patterns.md) for multi-chunk reading, error handling, and protocol implementations. ## Quick Reference | Task | Code | |------|------| | Import | `import { connect } from 'cloudflare:sockets';` | | Connect | `connect({ hostname: "host", port: 443 })` | | With TLS | `connect(addr, { secureTransport: "on" })` | | StartTLS | `socket.startTls()` after handshake | | Write | `await writer.write(data); await writer.close();` | | Read | `const { value } = await reader.read();` | | Error handling | `try { await socket.opened; } catch { }` | | Always close | `try { } finally { await socket.close(); }` | ## See Also - [patterns.md](./patterns.md) - Real-world protocol implementations - [configuration.md](./configuration.md) - Wrangler setup and environment variables - [gotchas.md](./gotchas.md) - Limits and error handling ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-vpc/configuration.md ================================================ # Configuration Setup and configuration for TCP Sockets in Cloudflare Workers. ## Wrangler Configuration ### Basic Setup TCP Sockets are available by default in Workers runtime. No special configuration required in `wrangler.jsonc`: ```jsonc { "name": "private-network-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01" } ``` ### Environment Variables Store connection details as env vars: ```jsonc { "vars": { "DB_HOST": "10.0.1.50", "DB_PORT": "5432" } } ``` ```typescript interface Env { DB_HOST: string; DB_PORT: string; } export default { async fetch(req: Request, env: Env): Promise { const socket = connect({ hostname: env.DB_HOST, port: parseInt(env.DB_PORT) }); } }; ``` ### Per-Environment Configuration ```jsonc { "vars": { "DB_HOST": "localhost" }, "env": { "staging": { "vars": { "DB_HOST": "staging-db.internal.net" } }, "production": { "vars": { "DB_HOST": "prod-db.internal.net" } } } } ``` Deploy: `wrangler deploy --env staging` or `wrangler deploy --env production` ## Integration with Cloudflare Tunnel To connect Workers to private networks, combine TCP Sockets with Cloudflare Tunnel: ``` Worker (TCP Socket) → Tunnel hostname → cloudflared → Private Network ``` ### Quick Setup 1. **Install cloudflared** on a server inside your private network 2. **Create tunnel**: `cloudflared tunnel create my-private-network` 3. **Configure routing** in `config.yml`: ```yaml tunnel: credentials-file: /path/to/.json ingress: - hostname: db.internal.example.com service: tcp://10.0.1.50:5432 - service: http_status:404 # Required catch-all ``` 4. **Run tunnel**: `cloudflared tunnel run my-private-network` 5. **Connect from Worker**: ```typescript const socket = connect( { hostname: "db.internal.example.com", port: 5432 }, // Tunnel hostname { secureTransport: "on" } ); ``` For detailed Tunnel setup, see [Tunnel configuration reference](../tunnel/configuration.md). ## Smart Placement Integration Reduce latency by auto-placing Workers near backends: ```jsonc { "placement": { "mode": "smart" } } ``` Workers automatically relocate closer to TCP socket destinations after observing connection latency. See [Smart Placement reference](../smart-placement/). ## Secrets Management Store sensitive credentials as secrets (not in wrangler.jsonc): ```bash wrangler secret put DB_PASSWORD # Enter value when prompted ``` Access in Worker via `env.DB_PASSWORD`. Use in protocol handshake or authentication. ## Local Development Test with `wrangler dev`. Note: Local mode may not access private networks. Use public endpoints or mock servers for development: ```typescript const config = process.env.NODE_ENV === 'dev' ? { hostname: 'localhost', port: 5432 } // Mock : { hostname: 'db.internal.example.com', port: 5432 }; // Production ``` ## Connection String Patterns Parse connection strings to extract host and port: ```typescript function parseConnectionString(connStr: string): SocketAddress { const url = new URL(connStr); // e.g., "postgres://10.0.1.50:5432/mydb" return { hostname: url.hostname, port: parseInt(url.port) || 5432 }; } ``` ## Hyperdrive Integration For PostgreSQL/MySQL, prefer Hyperdrive over raw TCP sockets (includes connection pooling): ```jsonc { "hyperdrive": [{ "binding": "DB", "id": "" }] } ``` See [Hyperdrive reference](../hyperdrive/) for complete setup. ## Compatibility TCP Sockets available in all modern Workers. Use current date: `"compatibility_date": "2025-01-01"`. No special flags required. ## Related Configuration - **[Tunnel Configuration](../tunnel/configuration.md)** - Detailed cloudflared setup - **[Smart Placement](../smart-placement/configuration.md)** - Placement mode options - **[Hyperdrive](../hyperdrive/configuration.md)** - Database connection pooling setup ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-vpc/gotchas.md ================================================ # Gotchas and Troubleshooting Common pitfalls, limitations, and solutions for TCP Sockets in Cloudflare Workers. ## Platform Limits ### Connection Limits | Limit | Value | |-------|-------| | Max concurrent sockets per request | 6 (hard limit) | | Socket lifetime | Request duration | | Connection timeout | Platform-dependent, no setting | **Problem:** Exceeding 6 connections throws error **Solution:** Process in batches of 6 ```typescript for (let i = 0; i < hosts.length; i += 6) { const batch = hosts.slice(i, i + 6).map(h => connect({ hostname: h, port: 443 })); await Promise.all(batch.map(async s => { /* use */ await s.close(); })); } ``` ### Blocked Destinations Cloudflare IPs (1.1.1.1), localhost (127.0.0.1), port 25 (SMTP), Worker's own URL blocked for security. **Solution:** Use public IPs or Tunnel hostnames: `connect({ hostname: "db.internal.company.net", port: 5432 })` ### Scope Requirements **Problem:** Sockets created in global scope fail **Cause:** Sockets tied to request lifecycle **Solution:** Create inside handler: `export default { async fetch() { const socket = connect(...); } }` ## Common Errors ### Error: "proxy request failed" **Causes:** Blocked destination (Cloudflare IP, localhost, port 25), DNS failure, network unreachable **Solution:** Validate destinations, use Tunnel hostnames, catch errors with try/catch ### Error: "TCP Loop detected" **Cause:** Worker connecting to itself **Solution:** Connect to external service, not Worker's own hostname ### Error: "Port 25 prohibited" **Cause:** SMTP port blocked **Solution:** Use Email Workers API for email ### Error: "socket is not open" **Cause:** Read/write after close **Solution:** Always use try/finally to ensure proper closure order ### Error: Connection timeout **Cause:** No built-in timeout **Solution:** Use `Promise.race()`: ```typescript const socket = connect(addr, opts); const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)); await Promise.race([socket.opened, timeout]); ``` ## TLS/SSL Issues ### StartTLS Timing **Problem:** Calling `startTls()` too early **Solution:** Send protocol-specific STARTTLS command, wait for server OK, then call `socket.startTls()` ### Certificate Validation **Problem:** Self-signed certs fail **Solution:** Use proper certs or Tunnel (handles TLS termination) ## Performance Issues ### Not Using Connection Pooling **Problem:** New connection overhead per request **Solution:** Use [Hyperdrive](../hyperdrive/) for databases (built-in pooling) ### Not Using Smart Placement **Problem:** High latency to backend **Solution:** Enable: `{ "placement": { "mode": "smart" } }` in wrangler.jsonc ### Forgetting to Close Sockets **Problem:** Resource leaks **Solution:** Always use try/finally: ```typescript const socket = connect({ hostname: "api.internal", port: 443 }); try { // Use socket } finally { await socket.close(); } ``` ## Data Handling Issues ### Assuming Single Read Gets All Data **Problem:** Only reading once may miss chunked data **Solution:** Loop `reader.read()` until `done === true` (see patterns.md) ### Text Encoding Issues **Problem:** Using wrong encoding **Solution:** Specify encoding: `new TextDecoder('iso-8859-1').decode(data)` ## Security Issues ### SSRF Vulnerability **Problem:** User-controlled destinations allow access to internal services **Solution:** Validate against strict allowlist: ```typescript const ALLOWED = ['api1.internal.net', 'api2.internal.net']; const host = new URL(req.url).searchParams.get('host'); if (!host || !ALLOWED.includes(host)) return new Response('Forbidden', { status: 403 }); ``` ## When to Use Alternatives | Use Case | Alternative | Reason | |----------|-------------|--------| | PostgreSQL/MySQL | [Hyperdrive](../hyperdrive/) | Connection pooling, caching | | HTTP/HTTPS | `fetch()` | Simpler, built-in | | HTTP with SSRF protection | VPC Services (beta 2025+) | Declarative bindings | ## Debugging Tips 1. **Log connection details:** `const info = await socket.opened; console.log(info.remoteAddress);` 2. **Test with public services first:** Use tcpbin.com:4242 echo server 3. **Verify Tunnel:** `cloudflared tunnel info ` and `cloudflared tunnel route ip list` ## Related - [Hyperdrive](../hyperdrive/) - Database connections - [Smart Placement](../smart-placement/) - Latency optimization - [Tunnel Troubleshooting](../tunnel/gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workers-vpc/patterns.md ================================================ # Common Patterns Real-world patterns and examples for TCP Sockets in Cloudflare Workers. ```typescript import { connect } from 'cloudflare:sockets'; ``` ## Basic Patterns ### Simple Request-Response ```typescript const socket = connect({ hostname: "echo.example.com", port: 7 }, { secureTransport: "on" }); try { await socket.opened; const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("Hello\n")); await writer.close(); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new Response(value); } finally { await socket.close(); } ``` ### Reading All Data ```typescript async function readAll(socket: Socket): Promise { const reader = socket.readable.getReader(); const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const total = chunks.reduce((sum, c) => sum + c.length, 0); const result = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; } ``` ### Streaming Response ```typescript // Stream socket data directly to HTTP response const socket = connect({ hostname: "stream.internal", port: 9000 }, { secureTransport: "on" }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("STREAM\n")); await writer.close(); return new Response(socket.readable); ``` ## Protocol Examples ### Redis RESP ```typescript // Send: *2\r\n$3\r\nGET\r\n$\r\n\r\n // Recv: $\r\n\r\n or $-1\r\n for null const socket = connect({ hostname: "redis.internal", port: 6379 }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode(`*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n`)); ``` ### PostgreSQL **Use [Hyperdrive](../hyperdrive/) for production.** Raw Postgres protocol is complex (startup, auth, query messages). ### MQTT ```typescript const socket = connect({ hostname: "mqtt.broker", port: 1883 }); const writer = socket.writable.getWriter(); // CONNECT: 0x10 0x00 0x04 "MQTT" 0x04 ... // PUBLISH: 0x30 ``` ## Error Handling Patterns ### Retry with Backoff ```typescript async function connectWithRetry(addr: SocketAddress, opts: SocketOptions, maxRetries = 3): Promise { for (let i = 1; i <= maxRetries; i++) { try { const socket = connect(addr, opts); await socket.opened; return socket; } catch (error) { if (i === maxRetries) throw error; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i - 1))); // Exponential backoff } } throw new Error('Unreachable'); } ``` ### Timeout ```typescript async function connectWithTimeout(addr: SocketAddress, opts: SocketOptions, ms = 5000): Promise { const socket = connect(addr, opts); const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms)); await Promise.race([socket.opened, timeout]); return socket; } ``` ### Fallback ```typescript async function connectWithFallback(primary: string, fallback: string, port: number): Promise { try { const socket = connect({ hostname: primary, port }, { secureTransport: "on" }); await socket.opened; return socket; } catch { return connect({ hostname: fallback, port }, { secureTransport: "on" }); } } ``` ## Security Patterns ### Destination Allowlist (Prevent SSRF) ```typescript const ALLOWED_HOSTS = ['db.internal.company.net', 'api.internal.company.net', /^10\.0\.1\.\d+$/]; function isAllowed(hostname: string): boolean { return ALLOWED_HOSTS.some(p => p instanceof RegExp ? p.test(hostname) : p === hostname); } export default { async fetch(req: Request): Promise { const target = new URL(req.url).searchParams.get('host'); if (!target || !isAllowed(target)) return new Response('Forbidden', { status: 403 }); const socket = connect({ hostname: target, port: 443 }); // Use socket... } }; ``` ### Connection Pooling ```typescript class SocketPool { private pool = new Map(); async acquire(hostname: string, port: number): Promise { const key = `${hostname}:${port}`; const sockets = this.pool.get(key) || []; if (sockets.length > 0) return sockets.pop()!; const socket = connect({ hostname, port }, { secureTransport: "on" }); await socket.opened; return socket; } release(hostname: string, port: number, socket: Socket): void { const key = `${hostname}:${port}`; const sockets = this.pool.get(key) || []; if (sockets.length < 3) { sockets.push(socket); this.pool.set(key, sockets); } else socket.close(); } } ``` ## Multi-Protocol Gateway ```typescript interface Protocol { name: string; defaultPort: number; test(host: string, port: number): Promise; } const PROTOCOLS: Record = { redis: { name: 'redis', defaultPort: 6379, async test(host, port) { const socket = connect({ hostname: host, port }); try { const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode('*1\r\n$4\r\nPING\r\n')); writer.releaseLock(); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new TextDecoder().decode(value || new Uint8Array()); } finally { await socket.close(); } } } }; export default { async fetch(req: Request): Promise { const url = new URL(req.url); const proto = url.pathname.slice(1); // /redis const host = url.searchParams.get('host'); if (!host || !PROTOCOLS[proto]) return new Response('Invalid', { status: 400 }); const result = await PROTOCOLS[proto].test(host, parseInt(url.searchParams.get('port') || '') || PROTOCOLS[proto].defaultPort); return new Response(result); } }; ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/workflows/README.md ================================================ # Cloudflare Workflows Durable multi-step applications with automatic retries, state persistence, and long-running execution. ## What It Does - Chain steps with automatic retry logic - Persist state between steps (minutes → weeks) - Handle failures without losing progress - Wait for external events/approvals - Sleep without consuming resources **Available:** Free & Paid Workers plans ## Core Concepts **Workflow**: Class extending `WorkflowEntrypoint` with `run` method **Instance**: Single execution with unique ID & independent state **Steps**: Independently retriable units via `step.do()` - API calls, DB queries, AI invocations **State**: Persisted from step returns; step name = cache key ## Quick Start ```typescript import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers'; type Env = { MY_WORKFLOW: Workflow; DB: D1Database }; type Params = { userId: string }; export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { const user = await step.do('fetch user', async () => { return await this.env.DB.prepare('SELECT * FROM users WHERE id = ?') .bind(event.params.userId).first(); }); await step.sleep('wait 7 days', '7 days'); await step.do('send reminder', async () => { await sendEmail(user.email, 'Reminder!'); }); } } ``` ## Key Features - **Durability**: Failed steps don't re-run successful ones - **Retries**: Configurable backoff (constant/linear/exponential) - **Events**: `waitForEvent()` for webhooks/approvals (timeout: 1h → 365d) - **Sleep**: `sleep()` / `sleepUntil()` for scheduling (max 365d) - **Parallel**: `Promise.all()` for concurrent steps - **Idempotency**: Check-then-execute patterns ## Reading Order **Getting Started:** configuration.md → api.md → patterns.md **Troubleshooting:** gotchas.md ## In This Reference - [configuration.md](./configuration.md) - wrangler.jsonc setup, step config, bindings - [api.md](./api.md) - Step APIs, instance management, sleep/parameters - [patterns.md](./patterns.md) - Common workflows, testing, orchestration - [gotchas.md](./gotchas.md) - Timeouts, limits, debugging strategies ## See Also - [durable-objects](../durable-objects/) - Alternative stateful approach - [queues](../queues/) - Message-driven workflows - [workers](../workers/) - Entry point for workflow instances ================================================ FILE: skills/.curated/cloudflare-deploy/references/workflows/api.md ================================================ # Workflow APIs ## Step APIs ```typescript // step.do() const result = await step.do('step name', async () => { /* logic */ }); const result = await step.do('step name', { retries, timeout }, async () => {}); // step.sleep() await step.sleep('description', '1 hour'); await step.sleep('description', 5000); // ms // step.sleepUntil() await step.sleepUntil('description', Date.parse('2024-12-31')); // step.waitForEvent() const data = await step.waitForEvent('wait', {event: 'webhook-type', timeout: '24h'}); // Default 24h, max 365d try { const event = await step.waitForEvent('wait', { event: 'approval', timeout: '1h' }); } catch (e) { /* Timeout */ } ``` ## Instance Management ```typescript // Create single const instance = await env.MY_WORKFLOW.create({id: crypto.randomUUID(), params: { userId: 'user123' }}); // id optional, auto-generated if omitted // Create with custom retention (default: 3 days free, 30 days paid) const instance = await env.MY_WORKFLOW.create({ id: crypto.randomUUID(), params: { userId: 'user123' }, retention: '30 days' // Override default retention period }); // Batch (max 100, idempotent: skips existing IDs) const instances = await env.MY_WORKFLOW.createBatch([{id: 'user1', params: {name: 'John'}}, {id: 'user2', params: {name: 'Jane'}}]); // Get & Status const instance = await env.MY_WORKFLOW.get('instance-id'); const status = await instance.status(); // {status: 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'waitingForPause' | 'unknown', error?, output?} // Control await instance.pause(); await instance.resume(); await instance.terminate(); await instance.restart(); // Send Events await instance.sendEvent({type: 'approval', payload: { approved: true }}); // Must match waitForEvent type ``` ## Triggering Workflows ```typescript // From Worker export default { async fetch(req, env) { const instance = await env.MY_WORKFLOW.create({id: crypto.randomUUID(), params: { userId: 'user123' }}); return Response.json({ id: instance.id }); }}; // From Queue export default { async queue(batch, env) { for (const msg of batch.messages) { await env.MY_WORKFLOW.create({id: `job-${msg.id}`, params: msg.body}); } }}; // From Cron export default { async scheduled(event, env) { await env.CLEANUP_WORKFLOW.create({id: `cleanup-${Date.now()}`, params: { timestamp: event.scheduledTime }}); }}; // From Another Workflow (non-blocking) export class ParentWorkflow extends WorkflowEntrypoint { async run(event, step) { const child = await step.do('start child', async () => await this.env.CHILD_WORKFLOW.create({id: `child-${event.instanceId}`, params: {}})); } } ``` ## Error Handling ```typescript import { NonRetryableError } from 'cloudflare:workers'; // NonRetryableError await step.do('validate', async () => { if (!event.params.paymentMethod) throw new NonRetryableError('Payment method required'); const res = await fetch('https://api.example.com/charge', { method: 'POST' }); if (res.status === 401) throw new NonRetryableError('Invalid credentials'); // Don't retry if (!res.ok) throw new Error('Retryable failure'); // Will retry return res.json(); }); // Catching Errors try { await step.do('risky op', async () => { throw new NonRetryableError('Failed'); }); } catch (e) { await step.do('cleanup', async () => {}); } // Idempotency await step.do('charge', async () => { const sub = await fetch(`https://api/subscriptions/${id}`).then(r => r.json()); if (sub.charged) return sub; // Already done return await fetch(`https://api/subscriptions/${id}`, {method: 'POST', body: JSON.stringify({ amount: 10.0 })}).then(r => r.json()); }); ``` ## Type Constraints Params and step returns must be `Rpc.Serializable`: ```typescript // ✅ Valid types type ValidParams = { userId: string; count: number; tags: string[]; metadata: Record; }; // ❌ Invalid types type InvalidParams = { callback: () => void; // Functions not serializable symbol: symbol; // Symbols not serializable circular: any; // Circular references not allowed }; // Step returns follow same rules const result = await step.do('fetch', async () => { return { userId: '123', data: [1, 2, 3] }; // ✅ Plain object }); ``` ## Sleep & Scheduling ```typescript // Relative await step.sleep('wait 1 hour', '1 hour'); await step.sleep('wait 30 days', '30 days'); await step.sleep('wait 5s', 5000); // ms // Absolute await step.sleepUntil('launch date', Date.parse('24 Oct 2024 13:00:00 UTC')); await step.sleepUntil('deadline', new Date('2024-12-31T23:59:59Z')); ``` Units: second, minute, hour, day, week, month, year. Max: 365 days. Sleeping instances don't count toward concurrency. ## Parameters **Pass from Worker:** ```typescript const instance = await env.MY_WORKFLOW.create({ id: crypto.randomUUID(), params: { userId: 'user123', email: 'user@example.com' } }); ``` **Access in Workflow:** ```typescript async run(event: WorkflowEvent, step: WorkflowStep) { const userId = event.params.userId; const instanceId = event.instanceId; const createdAt = event.timestamp; } ``` **CLI Trigger:** ```bash npx wrangler workflows trigger my-workflow '{"userId":"user123"}' ``` ## Wrangler CLI ```bash npm create cloudflare@latest my-workflow -- --template "cloudflare/workflows-starter" npx wrangler deploy npx wrangler workflows list npx wrangler workflows trigger my-workflow '{"userId":"user123"}' npx wrangler workflows instances list my-workflow npx wrangler workflows instances describe my-workflow instance-id npx wrangler workflows instances pause/resume/terminate my-workflow instance-id ``` ## REST API ```bash # Create curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances" -H "Authorization: Bearer {token}" -d '{"id":"custom-id","params":{"userId":"user123"}}' # Status curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances/{instance_id}/status" -H "Authorization: Bearer {token}" # Send Event curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances/{instance_id}/events" -H "Authorization: Bearer {token}" -d '{"type":"approval","payload":{"approved":true}}' ``` See: [configuration.md](./configuration.md), [patterns.md](./patterns.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workflows/configuration.md ================================================ # Workflow Configuration ## wrangler.jsonc Setup ```jsonc { "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date for new projects "observability": { "enabled": true // Enables Workflows dashboard + structured logs }, "workflows": [ { "name": "my-workflow", // Workflow name "binding": "MY_WORKFLOW", // Env binding "class_name": "MyWorkflow" // TS class name // "script_name": "other-worker" // For cross-script calls } ], "limits": { "cpu_ms": 300000 // 5 min max (default 30s) } } ``` ## Step Configuration ```typescript // Basic step const data = await step.do('step name', async () => ({ result: 'value' })); // With retry config await step.do('api call', { retries: { limit: 10, // Default: 5, or Infinity delay: '10 seconds', // Default: 10000ms backoff: 'exponential' // constant | linear | exponential }, timeout: '30 minutes' // Per-attempt timeout (default: 10min) }, async () => { const res = await fetch('https://api.example.com/data'); if (!res.ok) throw new Error('Failed'); return res.json(); }); ``` ### Parallel Steps ```typescript const [user, settings] = await Promise.all([ step.do('fetch user', async () => this.env.KV.get(`user:${id}`)), step.do('fetch settings', async () => this.env.KV.get(`settings:${id}`)) ]); ``` ### Conditional Steps ```typescript const config = await step.do('fetch config', async () => this.env.KV.get('flags', { type: 'json' }) ); // ✅ Deterministic (based on step output) if (config.enableEmail) { await step.do('send email', async () => sendEmail()); } // ❌ Non-deterministic (Date.now outside step) if (Date.now() > deadline) { /* BAD */ } ``` ### Dynamic Steps (Loops) ```typescript const files = await step.do('list files', async () => this.env.BUCKET.list() ); for (const file of files.objects) { await step.do(`process ${file.key}`, async () => { const obj = await this.env.BUCKET.get(file.key); return processData(await obj.arrayBuffer()); }); } ``` ## Multiple Workflows ```jsonc { "workflows": [ {"name": "user-onboarding", "binding": "USER_ONBOARDING", "class_name": "UserOnboarding"}, {"name": "data-processing", "binding": "DATA_PROCESSING", "class_name": "DataProcessing"} ] } ``` Each class extends `WorkflowEntrypoint` with its own `Params` type. ## Cross-Script Bindings Worker A defines workflow. Worker B calls it by adding `script_name`: ```jsonc // Worker B (caller) { "workflows": [{ "name": "billing-workflow", "binding": "BILLING", "script_name": "billing-worker" // Points to Worker A }] } ``` ## Bindings Workflows access Cloudflare bindings via `this.env`: ```typescript type Env = { MY_WORKFLOW: Workflow; KV: KVNamespace; DB: D1Database; BUCKET: R2Bucket; AI: Ai; VECTORIZE: VectorizeIndex; }; await step.do('use bindings', async () => { const kv = await this.env.KV.get('key'); const db = await this.env.DB.prepare('SELECT * FROM users').first(); const file = await this.env.BUCKET.get('file.txt'); const ai = await this.env.AI.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hi' }); }); ``` ## Pages Functions Binding Pages Functions can trigger Workflows via service bindings: ```typescript // functions/_middleware.ts export const onRequest: PagesFunction = async ({ env, request }) => { const instance = await env.MY_WORKFLOW.create({ params: { url: request.url } }); return new Response(`Started ${instance.id}`); }; ``` Configure in wrangler.jsonc under `service_bindings`. See: [api.md](./api.md), [patterns.md](./patterns.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workflows/gotchas.md ================================================ # Gotchas & Debugging ## Common Errors ### "Step Timeout" **Cause:** Step execution exceeding 10 minute default timeout or configured timeout **Solution:** Set custom timeout with `step.do('long operation', {timeout: '30 minutes'}, async () => {...})` or increase CPU limit in wrangler.jsonc (max 5min CPU time) ### "waitForEvent Timeout" **Cause:** Event not received within timeout period (default 24h, max 365d) **Solution:** Wrap in try-catch to handle timeout gracefully and proceed with default behavior ### "Non-Deterministic Step Names" **Cause:** Using dynamic values like `Date.now()` in step names causes replay issues **Solution:** Use deterministic values like `event.instanceId` for step names ### "State Lost in Variables" **Cause:** Using module-level or local variables to store state which is lost on hibernation **Solution:** Return values from `step.do()` which are automatically persisted: `const total = await step.do('step 1', async () => 10)` ### "Non-Deterministic Conditionals" **Cause:** Using non-deterministic logic (like `Date.now()`) outside steps in conditionals **Solution:** Move non-deterministic operations inside steps: `const isLate = await step.do('check', async () => Date.now() > deadline)` ### "Large Step Returns Exceeding Limit" **Cause:** Returning data >1 MiB from step **Solution:** Store large data in R2 and return only reference: `{ key: 'r2-object-key' }` ### "Step Exceeded CPU Limit But Ran for < 30s" **Cause:** Confusion between CPU time (active compute) and wall-clock time (includes I/O waits) **Solution:** Network requests, database queries, and sleeps don't count toward CPU. 30s limit = 30s of active processing ### "Idempotency Violation" **Cause:** Step operations not idempotent, causing duplicate charges or actions on retry **Solution:** Check if operation already completed before executing (e.g., check if customer already charged) ### "Instance ID Collision" **Cause:** Reusing instance IDs causing conflicts **Solution:** Use unique IDs with timestamp: `await env.MY_WORKFLOW.create({ id: \`${userId}-${Date.now()}\`, params: {} })` ### "Instance Data Disappeared After Completion" **Cause:** Completed/errored instances are automatically deleted after retention period (3 days free / 30 days paid) **Solution:** Export critical data to KV/R2/D1 before workflow completes ### "Missing await on step.do" **Cause:** Forgetting to await step.do() causing fire-and-forget behavior **Solution:** Always await step operations: `await step.do('task', ...)` ## Limits | Limit | Free | Paid | Notes | |-------|------|------|-------| | CPU per step | 10ms | 30s (default), 5min (max) | Set via `limits.cpu_ms` in wrangler.jsonc | | Step state | 1 MiB | 1 MiB | Per step return value | | Instance state | 100 MB | 1 GB | Total state per workflow instance | | Steps per workflow | 1,024 | 1,024 | `step.sleep()` doesn't count | | Executions per day | 100k | Unlimited | Daily execution limit | | Concurrent instances | 25 | 10k | Maximum concurrent workflows; waiting state excluded | | Queued instances | 100k | 1M | Maximum queued workflow instances | | Subrequests per step | 50 | 1,000 | Maximum outbound requests per step | | State retention | 3 days | 30 days | How long completed instances kept | | Step timeout default | 10 min | 10 min | Per attempt | | waitForEvent timeout default | 24h | 24h | Maximum 365 days | | waitForEvent timeout max | 365 days | 365 days | Maximum wait time | **Note:** Instances in `waiting` state (from `step.sleep` or `step.waitForEvent`) don't count toward concurrent instance limit, allowing millions of sleeping workflows. ## Pricing | Metric | Free | Paid | Notes | |--------|------|------|-------| | Requests | 100k/day | 10M/mo + $0.30/M | Workflow invocations | | CPU time | 10ms/invoke | 30M CPU-ms/mo + $0.02/M CPU-ms | Actual CPU usage | | Storage | 1 GB | 1 GB/mo + $0.20/GB-mo | All instances (running/errored/sleeping/completed) | ## References - [Official Docs](https://developers.cloudflare.com/workflows/) - [Get Started Guide](https://developers.cloudflare.com/workflows/get-started/guide/) - [Workers API](https://developers.cloudflare.com/workflows/build/workers-api/) - [REST API](https://developers.cloudflare.com/api/resources/workflows/) - [Examples](https://developers.cloudflare.com/workflows/examples/) - [Limits](https://developers.cloudflare.com/workflows/reference/limits/) - [Pricing](https://developers.cloudflare.com/workflows/reference/pricing/) See: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/workflows/patterns.md ================================================ # Workflow Patterns ## Image Processing Pipeline ```typescript export class ImageProcessingWorkflow extends WorkflowEntrypoint { async run(event, step) { const imageData = await step.do('fetch', async () => (await this.env.BUCKET.get(event.params.imageKey)).arrayBuffer()); const description = await step.do('generate description', async () => await this.env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', {image: Array.from(new Uint8Array(imageData)), prompt: 'Describe this image', max_tokens: 50}) ); await step.waitForEvent('await approval', { event: 'approved', timeout: '24h' }); await step.do('publish', async () => await this.env.BUCKET.put(`public/${event.params.imageKey}`, imageData)); } } ``` ## User Lifecycle ```typescript export class UserLifecycleWorkflow extends WorkflowEntrypoint { async run(event, step) { await step.do('welcome email', async () => await sendEmail(event.params.email, 'Welcome!')); await step.sleep('trial period', '7 days'); const hasConverted = await step.do('check conversion', async () => { const user = await this.env.DB.prepare('SELECT subscription_status FROM users WHERE id = ?').bind(event.params.userId).first(); return user.subscription_status === 'active'; }); if (!hasConverted) await step.do('trial expiration email', async () => await sendEmail(event.params.email, 'Trial ending')); } } ``` ## Data Pipeline ```typescript export class DataPipelineWorkflow extends WorkflowEntrypoint { async run(event, step) { const rawData = await step.do('extract', {retries: { limit: 10, delay: '30s', backoff: 'exponential' }}, async () => { const res = await fetch(event.params.sourceUrl); if (!res.ok) throw new Error('Fetch failed'); return res.json(); }); const transformed = await step.do('transform', async () => rawData.map(item => ({ id: item.id, normalized: normalizeData(item) })) ); const dataRef = await step.do('store', async () => { const key = `processed/${Date.now()}.json`; await this.env.BUCKET.put(key, JSON.stringify(transformed)); return { key }; }); await step.do('load', async () => { const data = await (await this.env.BUCKET.get(dataRef.key)).json(); for (let i = 0; i < data.length; i += 100) { await this.env.DB.batch(data.slice(i, i + 100).map(item => this.env.DB.prepare('INSERT INTO records VALUES (?, ?)').bind(item.id, item.normalized) )); } }); } } ``` ## Human-in-the-Loop Approval ```typescript export class ApprovalWorkflow extends WorkflowEntrypoint { async run(event, step) { await step.do('create approval', async () => await this.env.DB.prepare('INSERT INTO approvals (id, user_id, status) VALUES (?, ?, ?)').bind(event.instanceId, event.params.userId, 'pending').run()); try { const approval = await step.waitForEvent<{ approved: boolean }>('wait for approval', { event: 'approval-response', timeout: '48h' }); if (approval.approved) { await step.do('process approval', async () => {}); } else { await step.do('handle rejection', async () => {}); } } catch (e) { await step.do('auto reject', async () => await this.env.DB.prepare('UPDATE approvals SET status = ? WHERE id = ?').bind('auto-rejected', event.instanceId).run()); } } } ``` ## Testing Workflows ### Setup ```typescript // vitest.config.ts import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: './wrangler.jsonc' } } } } }); ``` ### Introspection API ```typescript import { introspectWorkflowInstance } from 'cloudflare:test'; const instance = await env.MY_WORKFLOW.create({ params: { userId: '123' } }); const introspector = await introspectWorkflowInstance(env.MY_WORKFLOW, instance.id); // Wait for step completion const result = await introspector.waitForStepResult({ name: 'fetch user', index: 0 }); // Mock step behavior await introspector.modify(async (m) => { await m.mockStepResult({ name: 'api call' }, { mocked: true }); }); ``` ## Best Practices ### ✅ DO 1. **Granular steps**: One API call per step (unless proving idempotency) 2. **Idempotency**: Check-then-execute; use idempotency keys 3. **Deterministic names**: Use static or step-output-based names 4. **Return state**: Persist via step returns, not variables 5. **Always await**: `await step.do()`, avoid dangling promises 6. **Deterministic conditionals**: Base on `event.payload` or step outputs 7. **Store large data externally**: R2/KV for >1 MiB, return refs 8. **Batch creation**: `createBatch()` for multiple instances ### ❌ DON'T 1. **One giant step**: Breaks durability & retry control 2. **State outside steps**: Lost on hibernation 3. **Mutate events**: Events immutable, return new state 4. **Non-deterministic logic outside steps**: `Math.random()`, `Date.now()` must be in steps 5. **Side effects outside steps**: May duplicate on restart 6. **Non-deterministic step names**: Prevents caching 7. **Ignore timeouts**: `waitForEvent` throws, use try-catch 8. **Reuse instance IDs**: Must be unique within retention ## Orchestration Patterns ### Fan-Out (Parallel Processing) ```typescript const files = await step.do('list', async () => this.env.BUCKET.list()); await Promise.all(files.objects.map((file, i) => step.do(`process ${i}`, async () => processFile(await (await this.env.BUCKET.get(file.key)).arrayBuffer())))); ``` ### Parent-Child Workflows ```typescript const child = await step.do('start child', async () => await this.env.CHILD_WORKFLOW.create({id: `child-${event.instanceId}`, params: { data: result.data }})); await step.do('other work', async () => console.log(`Child started: ${child.id}`)); ``` ### Race Pattern ```typescript const winner = await Promise.race([ step.do('option A', async () => slowOperation()), step.do('option B', async () => fastOperation()) ]); ``` ### Scheduled Workflow Chain ```typescript export default { async scheduled(event, env) { await env.DAILY_WORKFLOW.create({id: `daily-${event.scheduledTime}`, params: { timestamp: event.scheduledTime }}); }}; export class DailyWorkflow extends WorkflowEntrypoint { async run(event, step) { await step.do('daily task', async () => {}); await step.sleep('wait 7 days', '7 days'); await step.do('weekly followup', async () => {}); } } ``` See: [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md) ================================================ FILE: skills/.curated/cloudflare-deploy/references/wrangler/README.md ================================================ # Cloudflare Wrangler Official CLI for Cloudflare Workers - develop, manage, and deploy Workers from the command line. ## What is Wrangler? Wrangler is the Cloudflare Developer Platform CLI that allows you to: - Create, develop, and deploy Workers - Manage bindings (KV, D1, R2, Durable Objects, etc.) - Configure routing and environments - Run local development servers - Execute migrations and manage resources - Perform integration testing ## Installation ```bash npm install wrangler --save-dev # or globally npm install -g wrangler ``` Run commands: `npx wrangler ` (or `pnpm`/`yarn wrangler`) ## Reading Order | If you want to... | Start here | |-------------------|------------| | Create/deploy Worker quickly | Essential Commands below → [patterns.md](./patterns.md) §New Worker | | Configure bindings (KV, D1, R2) | [configuration.md](./configuration.md) §Bindings | | Write integration tests | [api.md](./api.md) §startWorker | | Debug production issues | [gotchas.md](./gotchas.md) + Essential Commands §Monitoring | | Set up multi-environment workflow | [configuration.md](./configuration.md) §Environments | ## Essential Commands ### Project & Development ```bash wrangler init [name] # Create new project wrangler dev # Local dev server (fast, simulated) wrangler dev --remote # Dev with remote resources (production-like) wrangler deploy # Deploy to production wrangler deploy --env staging # Deploy to environment wrangler versions list # List versions wrangler rollback [id] # Rollback deployment wrangler login # OAuth login wrangler whoami # Check auth status ``` ## Resource Management ### KV ```bash wrangler kv namespace create NAME wrangler kv key put "key" "value" --namespace-id= wrangler kv key get "key" --namespace-id= ``` ### D1 ```bash wrangler d1 create NAME wrangler d1 execute NAME --command "SQL" wrangler d1 migrations create NAME "description" wrangler d1 migrations apply NAME ``` ### R2 ```bash wrangler r2 bucket create NAME wrangler r2 object put BUCKET/key --file path wrangler r2 object get BUCKET/key ``` ### Other Resources ```bash wrangler queues create NAME wrangler vectorize create NAME --dimensions N --metric cosine wrangler hyperdrive create NAME --connection-string "..." wrangler workflows create NAME wrangler constellation create NAME wrangler pages project create NAME wrangler pages deployment create --project NAME --branch main ``` ### Secrets ```bash wrangler secret put NAME # Set Worker secret wrangler secret list # List Worker secrets wrangler secret delete NAME # Delete Worker secret wrangler secret bulk FILE.json # Bulk upload from JSON # Secrets Store (centralized, reusable across Workers) wrangler secret-store:secret put STORE_NAME SECRET_NAME wrangler secret-store:secret list STORE_NAME ``` ### Monitoring ```bash wrangler tail # Real-time logs wrangler tail --env production # Tail specific env wrangler tail --status error # Filter by status ``` ## In This Reference - [auth.md](./auth.md) - Authentication setup (`wrangler login`, API tokens) - [configuration.md](./configuration.md) - wrangler.jsonc setup, environments, bindings - [api.md](./api.md) - Programmatic API (`startWorker`, `getPlatformProxy`, events) - [patterns.md](./patterns.md) - Common workflows and development patterns - [gotchas.md](./gotchas.md) - Common pitfalls, limits, and troubleshooting ## Quick Decision Tree ``` Need to test your Worker? ├─ Testing full Worker with bindings → api.md §startWorker ├─ Testing individual functions → api.md §getPlatformProxy └─ Testing with Vitest → patterns.md §Testing with Vitest Need to configure something? ├─ Bindings (KV, D1, R2, etc.) → configuration.md §Bindings ├─ Multiple environments → configuration.md §Environments ├─ Static files → configuration.md §Workers Assets └─ Routing → configuration.md §Routing Development not working? ├─ Local differs from production → Use `wrangler dev --remote` ├─ Bindings not available → gotchas.md §Binding Not Available └─ Auth issues → auth.md Authentication issues? ├─ "Not logged in" / "Unauthorized" → auth.md ├─ First time deploying → `wrangler login` (one-time OAuth) └─ CI/CD setup → auth.md §API Token ``` ## See Also - [workers](../workers/) - Workers runtime API reference - [miniflare](../miniflare/) - Local testing with Miniflare - [workerd](../workerd/) - Runtime that powers `wrangler dev` ================================================ FILE: skills/.curated/cloudflare-deploy/references/wrangler/api.md ================================================ # Wrangler Programmatic API Node.js APIs for testing and development. ## startWorker (Testing) Starts Worker with real local bindings for integration tests. Stable API (replaces `unstable_startWorker`). ```typescript import { startWorker } from "wrangler"; import { describe, it, before, after } from "node:test"; import assert from "node:assert"; describe("worker", () => { let worker; before(async () => { worker = await startWorker({ config: "wrangler.jsonc", environment: "development" }); }); after(async () => { await worker.dispose(); }); it("responds with 200", async () => { const response = await worker.fetch("http://example.com"); assert.strictEqual(response.status, 200); }); }); ``` ### Options | Option | Type | Description | |--------|------|-------------| | `config` | `string` | Path to wrangler.jsonc | | `environment` | `string` | Environment name from config | | `persist` | `boolean \| { path: string }` | Enable persistent state | | `bundle` | `boolean` | Enable bundling (default: true) | | `remote` | `false \| true \| "minimal"` | Remote mode: `false` (local), `true` (full remote), `"minimal"` (remote bindings only) | ### Remote Mode ```typescript // Local mode (default) - fast, simulated const worker = await startWorker({ config: "wrangler.jsonc" }); // Full remote mode - production-like, slower const worker = await startWorker({ config: "wrangler.jsonc", remote: true }); // Minimal remote mode - remote bindings, local Worker const worker = await startWorker({ config: "wrangler.jsonc", remote: "minimal" }); ``` ## getPlatformProxy Emulate bindings in Node.js without starting Worker. ```typescript import { getPlatformProxy } from "wrangler"; const { env, dispose, caches } = await getPlatformProxy({ configPath: "wrangler.jsonc", environment: "production", persist: { path: ".wrangler/state" } }); // Use bindings const value = await env.MY_KV.get("key"); await env.DB.prepare("SELECT * FROM users").all(); await env.ASSETS.put("file.txt", "content"); // Platform APIs await caches.default.put("https://example.com", new Response("cached")); await dispose(); ``` Use for unit tests (test functions, not full Worker) or scripts that need bindings. ## Type Generation Generate types from config: `wrangler types` → creates `worker-configuration.d.ts` ## Event System Listen to Worker lifecycle events for advanced workflows. ```typescript import { startWorker } from "wrangler"; const worker = await startWorker({ config: "wrangler.jsonc", bundle: true }); // Bundle events worker.on("bundleStart", (details) => { console.log("Bundling started:", details.config); }); worker.on("bundleComplete", (details) => { console.log("Bundle ready:", details.duration); }); // Reconfiguration events worker.on("reloadStart", () => { console.log("Worker reloading..."); }); worker.on("reloadComplete", () => { console.log("Worker reloaded"); }); await worker.dispose(); ``` ### Dynamic Reconfiguration ```typescript import { startWorker } from "wrangler"; const worker = await startWorker({ config: "wrangler.jsonc" }); // Replace entire config await worker.setConfig({ config: "wrangler.staging.jsonc", environment: "staging" }); // Patch specific fields await worker.patchConfig({ vars: { DEBUG: "true" } }); await worker.dispose(); ``` ## unstable_dev (Deprecated) Use `startWorker` instead. ## Multi-Worker Registry Test multiple Workers with service bindings. ```typescript import { startWorker } from "wrangler"; const auth = await startWorker({ config: "./auth/wrangler.jsonc" }); const api = await startWorker({ config: "./api/wrangler.jsonc", bindings: { AUTH: auth } // Service binding }); const response = await api.fetch("http://example.com/api/login"); // API Worker calls AUTH Worker via env.AUTH.fetch() await api.dispose(); await auth.dispose(); ``` ## Best Practices - Use `startWorker` for integration tests (tests full Worker) - Use `getPlatformProxy` for unit tests (tests individual functions) - Use `remote: true` when debugging production-specific issues - Use `remote: "minimal"` for faster tests with real bindings - Enable `persist: true` for debugging (state survives runs) - Run `wrangler types` after config changes - Always `dispose()` to prevent resource leaks - Listen to bundle events for build monitoring - Use multi-worker registry for testing service bindings ## See Also - [README.md](./README.md) - CLI commands - [configuration.md](./configuration.md) - Config - [patterns.md](./patterns.md) - Testing patterns ================================================ FILE: skills/.curated/cloudflare-deploy/references/wrangler/auth.md ================================================ # Authentication Authenticate with Cloudflare before deploying Workers or Pages. ## Quick Decision Tree ``` Need to authenticate? ├─ Interactive/local dev → wrangler login (recommended) ├─ CI/CD or headless → CLOUDFLARE_API_TOKEN env var └─ Terraform/Pulumi → See respective references ``` ## wrangler login (Recommended) One-time OAuth flow for local development: ```bash npx wrangler login # Opens browser, completes OAuth npx wrangler whoami # Verify: shows email + account ID ``` Credentials stored locally. Works for all subsequent commands. ## API Token (CI/CD) For automated pipelines or environments without browser access: 1. Go to: **https://dash.cloudflare.com/profile/api-tokens** 2. Click **Create Token** 3. Use template: **"Edit Cloudflare Workers"** (covers Workers, Pages, KV, D1, R2) 4. Copy the token (shown only once) 5. Set environment variable: ```bash export CLOUDFLARE_API_TOKEN="your-token-here" ``` ### Minimal Permissions by Task | Task | Template / Permissions | |------|------------------------| | Deploy Workers/Pages | "Edit Cloudflare Workers" template | | Read-only access | "Read All Resources" template | | Custom scope | Account:Read + Workers Scripts:Edit + specific resources | ## Troubleshooting | Error | Cause | Fix | |-------|-------|-----| | "Not logged in" | No credentials | `wrangler login` or set `CLOUDFLARE_API_TOKEN` | | "Authentication error" | Invalid/expired token | Regenerate token in dashboard | | "Missing account" | Wrong account selected | `wrangler whoami` to check, add `account_id` to wrangler.jsonc | | Token works locally, fails CI | Token scoped to wrong account | Verify account ID matches in both places | | "Insufficient permissions" | Token lacks required scope | Create new token with correct permissions | ## Verifying Authentication ```bash npx wrangler whoami ``` Output shows: - Email (if OAuth login) - Account ID and name - Token scopes (if API token) Non-zero exit code means not authenticated. ## See Also - [terraform/README.md](../terraform/README.md) - Terraform provider auth - [pulumi/README.md](../pulumi/README.md) - Pulumi provider auth ================================================ FILE: skills/.curated/cloudflare-deploy/references/wrangler/configuration.md ================================================ # Wrangler Configuration Configuration reference for wrangler.jsonc (recommended). ## Config Format **wrangler.jsonc recommended** (v3.91.0+) - provides schema validation. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01", // Use current date "vars": { "API_KEY": "dev-key" }, "kv_namespaces": [{ "binding": "MY_KV", "id": "abc123" }] } ``` ## Field Inheritance Inheritable: `name`, `main`, `compatibility_date`, `routes`, `triggers` Non-inheritable (define per env): `vars`, bindings (KV, D1, R2, etc.) ## Environments ```jsonc { "name": "my-worker", "vars": { "ENV": "dev" }, "env": { "production": { "name": "my-worker-prod", "vars": { "ENV": "prod" }, "route": { "pattern": "example.com/*", "zone_name": "example.com" } } } } ``` Deploy: `wrangler deploy --env production` ## Routing ```jsonc // Custom domain (recommended) { "routes": [{ "pattern": "api.example.com", "custom_domain": true }] } // Zone-based { "routes": [{ "pattern": "api.example.com/*", "zone_name": "example.com" }] } // workers.dev { "workers_dev": true } ``` ## Bindings ```jsonc // Variables { "vars": { "API_URL": "https://api.example.com" } } // KV { "kv_namespaces": [{ "binding": "CACHE", "id": "abc123" }] } // D1 { "d1_databases": [{ "binding": "DB", "database_id": "abc-123" }] } // R2 { "r2_buckets": [{ "binding": "ASSETS", "bucket_name": "my-assets" }] } // Durable Objects { "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter", "script_name": "my-worker" // Required for external DOs }] } } { "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }] } // Service Bindings { "services": [{ "binding": "AUTH", "service": "auth-worker" }] } // Queues { "queues": { "producers": [{ "binding": "TASKS", "queue": "task-queue" }], "consumers": [{ "queue": "task-queue", "max_batch_size": 10 }] } } // Vectorize { "vectorize": [{ "binding": "VECTORS", "index_name": "embeddings" }] } // Hyperdrive (requires nodejs_compat_v2 for pg/postgres) { "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "hyper-id" }] } { "compatibility_flags": ["nodejs_compat_v2"] } // For pg/postgres // Workers AI { "ai": { "binding": "AI" } } // Workflows { "workflows": [{ "binding": "WORKFLOW", "name": "my-workflow", "class_name": "MyWorkflow" }] } // Secrets Store (centralized secrets) { "secrets_store": [{ "binding": "SECRETS", "id": "store-id" }] } // Constellation (AI inference) { "constellation": [{ "binding": "MODEL", "project_id": "proj-id" }] } ``` ## Workers Assets (Static Files) Recommended for serving static files (replaces old `site` config). ```jsonc { "assets": { "directory": "./public", "binding": "ASSETS", "html_handling": "auto-trailing-slash", // or "none", "force-trailing-slash" "not_found_handling": "single-page-application" // or "404-page", "none" } } ``` Access in Worker: ```typescript export default { async fetch(request, env) { // Try serving static asset first const asset = await env.ASSETS.fetch(request); if (asset.status !== 404) return asset; // Custom logic for non-assets return new Response("API response"); } } ``` ## Placement Control where Workers run geographically. ```jsonc { "placement": { "mode": "smart" // or "off" } } ``` - `"smart"`: Run Worker near data sources (D1, Durable Objects) to reduce latency - `"off"`: Default distribution (run everywhere) ## Auto-Provisioning (Beta) Omit resource IDs - Wrangler creates them and writes back to config on deploy. ```jsonc { "kv_namespaces": [{ "binding": "MY_KV" }] } // No id - auto-provisioned ``` After deploy, ID is added to config automatically. ## Advanced ```jsonc // Cron Triggers { "triggers": { "crons": ["0 0 * * *"] } } // Observability (tracing) { "observability": { "enabled": true, "head_sampling_rate": 0.1 } } // Runtime Limits { "limits": { "cpu_ms": 100 } } // Browser Rendering { "browser": { "binding": "BROWSER" } } // mTLS Certificates { "mtls_certificates": [{ "binding": "CERT", "certificate_id": "cert-uuid" }] } // Logpush (stream logs to R2/S3) { "logpush": true } // Tail Consumers (process logs with another Worker) { "tail_consumers": [{ "service": "log-worker" }] } // Unsafe bindings (access to arbitrary bindings) { "unsafe": { "bindings": [{ "name": "MY_BINDING", "type": "plain_text", "text": "value" }] } } ``` ## See Also - [README.md](./README.md) - Overview and commands - [api.md](./api.md) - Programmatic API - [patterns.md](./patterns.md) - Workflows - [gotchas.md](./gotchas.md) - Common issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/wrangler/gotchas.md ================================================ # Wrangler Common Issues ## Common Errors ### "Binding ID vs name mismatch" **Cause:** Confusion between binding name (code) and resource ID **Solution:** Bindings use `binding` (code name) and `id`/`database_id`/`bucket_name` (resource ID). Preview bindings need separate IDs: `preview_id`, `preview_database_id` ### "Environment not inheriting config" **Cause:** Non-inheritable keys not redefined per environment **Solution:** Non-inheritable keys (bindings, vars) must be redefined per environment. Inheritable keys (routes, compatibility_date) can be overridden ### "Local dev behavior differs from production" **Cause:** Using local simulation instead of remote execution **Solution:** Choose appropriate remote mode: - `wrangler dev` (default): Local simulation, fast, limited accuracy - `wrangler dev --remote`: Full remote execution, production-accurate, slower - Use `remote: "minimal"` in tests for fast tests with real remote bindings ### "startWorker doesn't match production" **Cause:** Using local mode when remote resources needed **Solution:** Use `remote` option: ```typescript const worker = await startWorker({ config: "wrangler.jsonc", remote: true // or "minimal" for faster tests }); ``` ### "Unexpected runtime changes" **Cause:** Missing compatibility_date **Solution:** Always set `compatibility_date`: ```jsonc { "compatibility_date": "2025-01-01" } ``` ### "Durable Object binding not working" **Cause:** Missing script_name for external DOs **Solution:** Always specify `script_name` for external Durable Objects: ```jsonc { "durable_objects": { "bindings": [ { "name": "MY_DO", "class_name": "MyDO", "script_name": "my-worker" } ] } } ``` For local DOs in same Worker, `script_name` is optional. ### "Auto-provisioned resources not appearing" **Cause:** IDs written back to config on first deploy, but config not reloaded **Solution:** After first deploy with auto-provisioning, config file is updated with IDs. Commit the updated config. On subsequent deploys, existing resources are reused. ### "Secrets not available in local dev" **Cause:** Secrets set with `wrangler secret put` only work in deployed Workers **Solution:** For local dev, use `.dev.vars` ### "Node.js compatibility error" **Cause:** Missing Node.js compatibility flag **Solution:** Some bindings (Hyperdrive with `pg`) require: ```jsonc { "compatibility_flags": ["nodejs_compat_v2"] } ``` ### "Workers Assets 404 errors" **Cause:** Asset path mismatch or incorrect `html_handling` **Solution:** - Check `assets.directory` points to correct build output - Set `html_handling: "auto-trailing-slash"` for SPAs - Use `not_found_handling: "single-page-application"` to serve index.html for 404s ```jsonc { "assets": { "directory": "./dist", "html_handling": "auto-trailing-slash", "not_found_handling": "single-page-application" } } ``` ### "Placement not reducing latency" **Cause:** Misunderstanding of Smart Placement **Solution:** Smart Placement only helps when Worker accesses D1 or Durable Objects. It doesn't affect KV, R2, or external API latency. ```jsonc { "placement": { "mode": "smart" } } // Only beneficial with D1/DOs ``` ### "unstable_startWorker not found" **Cause:** Using outdated API **Solution:** Use stable `startWorker` instead: ```typescript import { startWorker } from "wrangler"; // Not unstable_startWorker ``` ### "outboundService not mocking fetch" **Cause:** Mock function not returning Response **Solution:** Always return Response, use `fetch(req)` for passthrough: ```typescript const worker = await startWorker({ outboundService: (req) => { if (shouldMock(req)) { return new Response("mocked"); } return fetch(req); // Required for non-mocked requests } }); ``` ## Limits | Resource/Limit | Value | Notes | |----------------|-------|-------| | Bindings per Worker | 64 | Total across all types | | Environments | Unlimited | Named envs in config | | Config file size | ~1MB | Keep reasonable | | Workers Assets size | 25 MB | Per deployment | | Workers Assets files | 20,000 | Max number of files | | Script size (compressed) | 1 MB | Free, 10 MB paid | | CPU time | 10-50ms | Free, 50-500ms paid | | Subrequest limit | 50 | Free, 1000 paid | ## Troubleshooting ### Authentication Issues ```bash wrangler logout wrangler login wrangler whoami ``` ### Configuration Errors ```bash wrangler check # Validate config ``` Use wrangler.jsonc with `$schema` for validation. ### Binding Not Available - Check binding exists in config - For environments, ensure binding defined for that env - Local dev: some bindings need `--remote` ### Deployment Failures ```bash wrangler tail # Check logs wrangler deploy --dry-run # Validate wrangler whoami # Check account limits ``` ### Local Development Issues ```bash rm -rf .wrangler/state # Clear local state wrangler dev --remote # Use remote bindings wrangler dev --persist-to ./local-state # Custom persist location wrangler dev --inspector-port 9229 # Enable debugging ``` ### Testing Issues ```bash # If tests hang, ensure dispose() is called worker.dispose() // Always cleanup # If bindings don't work in tests const worker = await startWorker({ config: "wrangler.jsonc", remote: "minimal" // Use remote bindings }); ``` ## Resources - Docs: https://developers.cloudflare.com/workers/wrangler/ - Config: https://developers.cloudflare.com/workers/wrangler/configuration/ - Commands: https://developers.cloudflare.com/workers/wrangler/commands/ - Examples: https://github.com/cloudflare/workers-sdk/tree/main/templates - Discord: https://discord.gg/cloudflaredev ## See Also - [README.md](./README.md) - Commands - [configuration.md](./configuration.md) - Config - [api.md](./api.md) - Programmatic API - [patterns.md](./patterns.md) - Workflows ================================================ FILE: skills/.curated/cloudflare-deploy/references/wrangler/patterns.md ================================================ # Wrangler Development Patterns Common workflows and best practices. ## New Worker Project ```bash wrangler init my-worker && cd my-worker wrangler dev # Develop locally wrangler deploy # Deploy ``` ## Local Development ```bash wrangler dev # Local mode (fast, simulated) wrangler dev --remote # Remote mode (production-accurate) wrangler dev --env staging --port 8787 wrangler dev --inspector-port 9229 # Enable debugging ``` Debug: chrome://inspect → Configure → localhost:9229 ## Secrets ```bash # Production echo "secret-value" | wrangler secret put SECRET_KEY # Local: use .dev.vars (gitignored) # SECRET_KEY=local-dev-key ``` ## Adding KV ```bash wrangler kv namespace create MY_KV wrangler kv namespace create MY_KV --preview # Add to wrangler.jsonc: { "binding": "MY_KV", "id": "abc123" } wrangler deploy ``` ## Adding D1 ```bash wrangler d1 create my-db wrangler d1 migrations create my-db "initial_schema" # Edit migration file in migrations/, then: wrangler d1 migrations apply my-db --local wrangler deploy wrangler d1 migrations apply my-db --remote # Time Travel (restore to point in time) wrangler d1 time-travel restore my-db --timestamp 2025-01-01T12:00:00Z ``` ## Multi-Environment ```bash wrangler deploy --env staging wrangler deploy --env production ``` ```jsonc { "env": { "staging": { "vars": { "ENV": "staging" } } } } ``` ## Testing ### Integration Tests with Node.js Test Runner ```typescript import { startWorker } from "wrangler"; import { describe, it, before, after } from "node:test"; import assert from "node:assert"; describe("API", () => { let worker; before(async () => { worker = await startWorker({ config: "wrangler.jsonc", remote: "minimal" // Fast tests with real bindings }); }); after(async () => await worker.dispose()); it("creates user", async () => { const response = await worker.fetch("http://example.com/api/users", { method: "POST", body: JSON.stringify({ name: "Alice" }) }); assert.strictEqual(response.status, 201); }); }); ``` ### Testing with Vitest Install: `npm install -D vitest @cloudflare/vitest-pool-workers` **vitest.config.ts:** ```typescript import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.jsonc" } } } } }); ``` **tests/api.test.ts:** ```typescript import { env, SELF } from "cloudflare:test"; import { describe, it, expect } from "vitest"; it("fetches users", async () => { const response = await SELF.fetch("https://example.com/api/users"); expect(response.status).toBe(200); }); it("uses bindings", async () => { await env.MY_KV.put("key", "value"); expect(await env.MY_KV.get("key")).toBe("value"); }); ``` ### Multi-Worker Development (Service Bindings) ```typescript const authWorker = await startWorker({ config: "./auth/wrangler.jsonc" }); const apiWorker = await startWorker({ config: "./api/wrangler.jsonc", bindings: { AUTH: authWorker } // Service binding }); // Test API calling AUTH const response = await apiWorker.fetch("http://example.com/api/protected"); await authWorker.dispose(); await apiWorker.dispose(); ``` ### Mock External APIs ```typescript const worker = await startWorker({ config: "wrangler.jsonc", outboundService: (req) => { const url = new URL(req.url); if (url.hostname === "api.external.com") { return new Response(JSON.stringify({ mocked: true }), { headers: { "content-type": "application/json" } }); } return fetch(req); // Pass through other requests } }); // Test Worker that calls external API const response = await worker.fetch("http://example.com/proxy"); // Worker internally fetches api.external.com - gets mocked response ``` ## Monitoring & Versions ```bash wrangler tail # Real-time logs wrangler tail --status error # Filter errors wrangler versions list wrangler rollback [id] ``` ## TypeScript ```bash wrangler types # Generate types from config ``` ```typescript export default { async fetch(request: Request, env: Env): Promise { return Response.json({ value: await env.MY_KV.get("key") }); } } satisfies ExportedHandler; ``` ## Workers Assets ```jsonc { "assets": { "directory": "./dist", "binding": "ASSETS" } } ``` ```typescript export default { async fetch(request, env) { // API routes first if (new URL(request.url).pathname.startsWith("/api/")) { return Response.json({ data: "from API" }); } return env.ASSETS.fetch(request); // Static assets } } ``` ## See Also - [README.md](./README.md) - Commands - [configuration.md](./configuration.md) - Config - [api.md](./api.md) - Programmatic API - [gotchas.md](./gotchas.md) - Issues ================================================ FILE: skills/.curated/cloudflare-deploy/references/zaraz/IMPLEMENTATION_SUMMARY.md ================================================ # Zaraz Reference Implementation Summary ## Files Created | File | Lines | Purpose | |------|-------|---------| | README.md | 111 | Navigation, decision tree, quick start | | api.md | 287 | Web API reference, Zaraz Context | | configuration.md | 307 | Dashboard setup, triggers, tools, consent | | patterns.md | 430 | SPA, e-commerce, Worker integration | | gotchas.md | 317 | Troubleshooting, limits, tool-specific issues | | **Total** | **1,452** | **vs 366 original** | ## Key Improvements Applied ### Structure - ✅ Created 5-file progressive disclosure system - ✅ Added navigation table in README - ✅ Added decision tree for routing - ✅ Added "Reading Order by Task" guide - ✅ Cross-referenced files throughout ### New Content Added - ✅ Zaraz Context (system/client properties) - ✅ History Change trigger for SPA tracking - ✅ Context Enrichers pattern - ✅ Worker Variables pattern - ✅ Consent management deep dive - ✅ Tool-specific quirks (GA4, Facebook, Google Ads) - ✅ GTM migration guide - ✅ Comprehensive troubleshooting - ✅ "When NOT to use Zaraz" section - ✅ TypeScript type definitions ### Preserved Content - ✅ All original API methods - ✅ E-commerce tracking examples - ✅ Consent management - ✅ Workers integration (expanded) - ✅ Common patterns (expanded) - ✅ Debugging tools - ✅ Reference links ## Progressive Disclosure Impact ### Before (Monolithic) All tasks loaded 366 lines regardless of need. ### After (Progressive) - **Track event task**: README (111) + api.md (287) = 398 lines - **Debug issue**: gotchas.md (317) = 317 lines (13% reduction) - **Configure tool**: configuration.md (307) = 307 lines (16% reduction) - **SPA tracking**: README + patterns.md (SPA section) ~180 lines (51% reduction) **Net effect:** Task-specific loading reduces unnecessary content by 13-51% depending on use case. ## File Summary ### README.md (111 lines) - Overview and core concepts - Quick start guide - When to use Zaraz vs Workers - Navigation table - Reading order by task - Decision tree ### api.md (287 lines) - zaraz.track() - zaraz.set() - zaraz.ecommerce() - Zaraz Context (system/client properties) - zaraz.consent API - zaraz.debug - Cookie methods - TypeScript definitions ### configuration.md (307 lines) - Dashboard setup flow - Trigger types (including History Change) - Tool configuration (GA4, Facebook, Google Ads) - Actions and action rules - Selective loading - Consent management setup - Privacy features - Testing workflow ### patterns.md (430 lines) - SPA tracking (React, Vue, Next.js) - User identification flows - Complete e-commerce funnel - A/B testing - Worker integration (Context Enrichers, Worker Variables, HTML injection) - Multi-tool coordination - GTM migration - Best practices ### gotchas.md (317 lines) - Events not firing (5-step debug process) - Consent issues - SPA tracking pitfalls - Performance issues - Tool-specific quirks - Data layer issues - Limits table - When NOT to use Zaraz - Debug checklist ## Quality Metrics - ✅ All files use consistent markdown formatting - ✅ Code examples include language tags - ✅ Tables for structured data (limits, parameters, comparisons) - ✅ Problem → Cause → Solution format in gotchas - ✅ Cross-references between files - ✅ No "see documentation" placeholders - ✅ Real, actionable examples throughout - ✅ Verified API syntax for Workers ## Original Backup Original SKILL.md preserved as `_SKILL_old.md` for reference. ================================================ FILE: skills/.curated/cloudflare-deploy/references/zaraz/README.md ================================================ # Cloudflare Zaraz Expert guidance for Cloudflare Zaraz - server-side tag manager for loading third-party tools at the edge. ## What is Zaraz? Zaraz offloads third-party scripts (analytics, ads, chat, marketing) to Cloudflare's edge, improving site speed, privacy, and security. Zero client-side performance impact. **Core Concepts:** - **Server-side execution** - Scripts run on Cloudflare, not user's browser - **Single HTTP request** - All tools loaded via one endpoint - **Privacy-first** - Control data sent to third parties - **No client-side JS overhead** - Minimal browser impact ## Quick Start 1. Navigate to domain > Zaraz in Cloudflare dashboard 2. Click "Start setup" 3. Add tools (Google Analytics, Facebook Pixel, etc.) 4. Configure triggers (when tools fire) 5. Add tracking code to your site: ```javascript // Track page view zaraz.track('page_view'); // Track custom event zaraz.track('button_click', { button_id: 'cta' }); // Set user properties zaraz.set('userId', 'user_123'); ``` ## When to Use Zaraz **Use Zaraz when:** - Adding multiple third-party tools (analytics, ads, marketing) - Site performance is critical (no client-side JS overhead) - Privacy compliance required (GDPR, CCPA) - Non-technical teams need to manage tools **Use Workers directly when:** - Building custom server-side tracking logic - Need full control over data processing - Integrating with complex backend systems - Zaraz's tool library doesn't meet needs ## In This Reference | File | Purpose | When to Read | |------|---------|--------------| | [api.md](./api.md) | Web API, zaraz object, consent methods | Implementing tracking calls | | [configuration.md](./configuration.md) | Dashboard setup, triggers, tools | Initial setup, adding tools | | [patterns.md](./patterns.md) | SPA, e-commerce, Worker integration | Best practices, common scenarios | | [gotchas.md](./gotchas.md) | Troubleshooting, limits, pitfalls | Debugging issues | ## Reading Order by Task | Task | Files to Read | |------|---------------| | Add analytics to site | README → configuration.md | | Track custom events | README → api.md | | Debug tracking issues | gotchas.md | | SPA tracking | api.md → patterns.md (SPA section) | | E-commerce tracking | api.md#ecommerce → patterns.md#ecommerce | | Worker integration | patterns.md#worker-integration | | GDPR compliance | api.md#consent → configuration.md#consent | ## Decision Tree ``` What do you need? ├─ Track events in browser → api.md │ ├─ Page views, clicks → zaraz.track() │ ├─ User properties → zaraz.set() │ └─ E-commerce → zaraz.ecommerce() │ ├─ Configure Zaraz → configuration.md │ ├─ Add GA4/Facebook → tools setup │ ├─ When tools fire → triggers │ └─ GDPR consent → consent purposes │ ├─ Integrate with Workers → patterns.md#worker-integration │ ├─ Enrich context → Context Enrichers │ └─ Inject tracking → HTML rewriting │ └─ Debug issues → gotchas.md ├─ Events not firing → troubleshooting ├─ Consent issues → consent debugging └─ Performance → debugging tools ``` ## Key Features - **100+ Pre-built Tools** - GA4, Facebook, Google Ads, TikTok, etc. - **Zero Client Impact** - Runs at Cloudflare's edge, not browser - **Privacy Controls** - Consent management, data filtering - **Custom Tools** - Build Managed Components for proprietary systems - **Worker Integration** - Enrich context, compute dynamic values - **Debug Mode** - Real-time event inspection ## Reference - [Zaraz Docs](https://developers.cloudflare.com/zaraz/) - [Web API](https://developers.cloudflare.com/zaraz/web-api/) - [Managed Components](https://developers.cloudflare.com/zaraz/advanced/load-custom-managed-component/) --- This skill focuses exclusively on Zaraz. For Workers development, see `cloudflare-workers` skill. ================================================ FILE: skills/.curated/cloudflare-deploy/references/zaraz/api.md ================================================ # Zaraz Web API Client-side JavaScript API for tracking events, setting properties, and managing consent. ## zaraz.track() ```javascript zaraz.track('button_click'); zaraz.track('purchase', { value: 99.99, currency: 'USD', item_id: '12345' }); zaraz.track('pageview', { page_path: '/products', page_title: 'Products' }); // SPA ``` **Params:** `eventName` (string), `properties` (object, optional). Fire-and-forget. ## zaraz.set() ```javascript zaraz.set('userId', 'user_12345'); zaraz.set({ email: '[email protected]', plan: 'premium', country: 'US' }); ``` Properties persist for page session. Use for user identification and segmentation. ## zaraz.ecommerce() ```javascript zaraz.ecommerce('Product Viewed', { product_id: 'SKU123', name: 'Widget', price: 49.99 }); zaraz.ecommerce('Product Added', { product_id: 'SKU123', quantity: 2, price: 49.99 }); zaraz.ecommerce('Order Completed', { order_id: 'ORD-789', total: 149.98, currency: 'USD', products: [{ product_id: 'SKU123', quantity: 2, price: 49.99 }] }); ``` **Events:** `Product Viewed`, `Product Added`, `Product Removed`, `Cart Viewed`, `Checkout Started`, `Order Completed` Tools auto-map to GA4, Facebook CAPI, etc. ## System Properties (Triggers) ``` {{system.page.url}} {{system.page.title}} {{system.page.referrer}} {{system.device.ip}} {{system.device.userAgent}} {{system.device.language}} {{system.cookies.name}} {{client.__zarazTrack.userId}} ``` ## zaraz.consent ```javascript // Check const purposes = zaraz.consent.getAll(); // { analytics: true, marketing: false } // Set zaraz.consent.modal = true; // Show modal zaraz.consent.setAll({ analytics: true, marketing: false }); zaraz.consent.set('marketing', true); // Listen zaraz.consent.addEventListener('consentChanged', () => { if (zaraz.consent.getAll().marketing) zaraz.track('marketing_consent_granted'); }); ``` **Flow:** Configure purposes in dashboard → Map tools to purposes → Show modal/set programmatically → Tools fire when allowed ## zaraz.debug ```javascript zaraz.debug = true; zaraz.track('test_event'); console.log(zaraz.tools); // View loaded tools ``` ## Cookie Methods ```javascript zaraz.getCookie('session_id'); // Zaraz namespace zaraz.readCookie('_ga'); // Any cookie ``` ## Async Behavior All methods fire-and-forget. Events batched and sent asynchronously: ```javascript zaraz.track('event1'); zaraz.set('prop', 'value'); zaraz.track('event2'); // All batched ``` ## TypeScript Types ```typescript interface Zaraz { track(event: string, properties?: Record): void; set(key: string, value: unknown): void; set(properties: Record): void; ecommerce(event: string, properties: Record): void; consent: { getAll(): Record; setAll(purposes: Record): void; set(purpose: string, value: boolean): void; addEventListener(event: 'consentChanged', callback: () => void): void; modal: boolean; }; debug: boolean; tools?: string[]; getCookie(name: string): string | undefined; readCookie(name: string): string | undefined; } declare global { interface Window { zaraz: Zaraz; } } ``` ================================================ FILE: skills/.curated/cloudflare-deploy/references/zaraz/configuration.md ================================================ # Zaraz Configuration ## Dashboard Setup 1. Domain → Zaraz → Start setup 2. Add tool (e.g., Google Analytics 4) 3. Enter credentials (GA4: `G-XXXXXXXXXX`) 4. Configure triggers 5. Save and Publish ## Triggers | Type | When | Use Case | |------|------|----------| | Pageview | Page load | Track page views | | Click | Element clicked | Button tracking | | Form Submission | Form submitted | Lead capture | | History Change | URL changes (SPA) | React/Vue routing | | Variable Match | Custom condition | Conditional firing | ### History Change (SPA) ``` Type: History Change Event: pageview ``` Fires on `pushState`, `replaceState`, hash changes. **No manual tracking needed.** ### Click Trigger ``` Type: Click CSS Selector: .buy-button Event: purchase_intent Properties: button_text: {{system.clickElement.text}} ``` ## Tool Configuration **GA4:** ``` Measurement ID: G-XXXXXXXXXX Events: page_view, purchase, user_engagement ``` **Facebook Pixel:** ``` Pixel ID: 1234567890123456 Events: PageView, Purchase, AddToCart ``` **Google Ads:** ``` Conversion ID: AW-XXXXXXXXX Conversion Label: YYYYYYYYYY ``` ## Consent Management 1. Settings → Consent → Create purposes (analytics, marketing) 2. Map tools to purposes 3. Set behavior: "Do not load until consent granted" **Programmatic consent:** ```javascript zaraz.consent.setAll({ analytics: true, marketing: true }); ``` ## Privacy Features | Feature | Default | |---------|---------| | IP Anonymization | Enabled | | Cookie Control | Via consent purposes | | GDPR/CCPA | Consent modal | ## Testing 1. **Preview Mode** - test without publishing 2. **Debug Mode** - `zaraz.debug = true` 3. **Network tab** - filter "zaraz" ## Limits | Resource | Limit | |----------|-------| | Event properties | 100KB | | Consent purposes | 20 | ================================================ FILE: skills/.curated/cloudflare-deploy/references/zaraz/gotchas.md ================================================ # Zaraz Gotchas ## Events Not Firing **Check:** 1. Tool enabled in dashboard (green dot) 2. Trigger conditions met 3. Consent granted for tool's purpose 4. Tool credentials correct (GA4: `G-XXXXXXXXXX`, FB: numeric only) **Debug:** ```javascript zaraz.debug = true; console.log('Tools:', zaraz.tools); console.log('Consent:', zaraz.consent.getAll()); ``` ## Consent Issues **Modal not showing:** ```javascript // Clear consent cookie document.cookie = 'zaraz-consent=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; location.reload(); ``` **Tools firing before consent:** Map tool to consent purpose with "Do not load until consent granted". ## SPA Tracking **Route changes not tracked:** 1. Configure History Change trigger in dashboard 2. Hash routing (`#/path`) requires manual tracking: ```javascript window.addEventListener('hashchange', () => { zaraz.track('pageview', { page_path: location.pathname + location.hash }); }); ``` **React fix:** ```javascript const location = useLocation(); useEffect(() => { zaraz.track('pageview', { page_path: location.pathname }); }, [location]); // Include dependency ``` ## Performance **Slow page load:** - Audit tool count (50+ degrades performance) - Disable blocking triggers unless required - Reduce event payload size (<100KB) ## Tool-Specific Issues | Tool | Issue | Fix | |------|-------|-----| | GA4 | Events not in real-time | Wait 5-10 min, use DebugView | | Facebook | Invalid Pixel ID | Use numeric only (no `fbpx_` prefix) | | Google Ads | Conversions not attributed | Include `send_to: 'AW-XXX/LABEL'` | ## Data Layer - Properties persist per page only - set on each page load - Nested access: `{{client.__zarazTrack.user.plan}}` ## Limits | Resource | Limit | |----------|-------| | Request size | 100KB | | Consent purposes | 20 | | API rate | 1000 req/sec | ## When NOT to Use Zaraz - Server-to-server tracking (use Workers) - Real-time bidirectional communication - Binary data transmission - Authentication flows ================================================ FILE: skills/.curated/cloudflare-deploy/references/zaraz/patterns.md ================================================ # Zaraz Patterns ## SPA Tracking **History Change Trigger (Recommended):** Configure in dashboard - no code needed, Zaraz auto-detects route changes. **Manual tracking (React/Vue/Next.js):** ```javascript // On route change zaraz.track('pageview', { page_path: pathname, page_title: document.title }); ``` ## User Identification ```javascript // Login zaraz.set({ userId: user.id, email: user.email, plan: user.plan }); zaraz.track('login', { method: 'oauth' }); // Logout - set to null (cannot clear) zaraz.set('userId', null); ``` ## E-commerce Funnel | Event | Method | |-------|--------| | View | `zaraz.ecommerce('Product Viewed', { product_id, name, price })` | | Add to cart | `zaraz.ecommerce('Product Added', { product_id, quantity })` | | Checkout | `zaraz.ecommerce('Checkout Started', { cart_id, products: [...] })` | | Purchase | `zaraz.ecommerce('Order Completed', { order_id, total, products })` | ## A/B Testing ```javascript zaraz.set('experiment_checkout', variant); zaraz.track('experiment_viewed', { experiment_id: 'checkout', variant }); // On conversion zaraz.track('experiment_conversion', { experiment_id, variant, value }); ``` ## Worker Integration **Context Enricher** - Modify context before tools execute: ```typescript export default { async fetch(request, env) { const body = await request.json(); body.system.userRegion = request.cf?.region; return Response.json(body); } }; ``` Configure: Zaraz > Settings > Context Enrichers **Worker Variables** - Compute dynamic values server-side, use as `{{worker.variable_name}}`. ## GTM Migration | GTM | Zaraz | |-----|-------| | `dataLayer.push({event: 'purchase'})` | `zaraz.ecommerce('Order Completed', {...})` | | `{{Page URL}}` | `{{system.page.url}}` | | `{{Page Title}}` | `{{system.page.title}}` | | Page View trigger | Pageview trigger | | Click trigger | Click (selector: `*`) | ## Best Practices 1. Use dashboard triggers over inline code 2. Enable History Change for SPAs (no manual code) 3. Debug with `zaraz.debug = true` 4. Implement consent early (GDPR/CCPA) 5. Use Context Enrichers for sensitive/server data ================================================ FILE: skills/.curated/develop-web-game/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don\'t include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/develop-web-game/SKILL.md ================================================ --- name: "develop-web-game" description: "Use when Codex is building or iterating on a web game (HTML/JS) and needs a reliable development + testing loop: implement small changes, run a Playwright-based test script with short input bursts and intentional pauses, inspect screenshots/text, and review console errors with render_game_to_text." --- # Develop Web Game Build games in small steps and validate every change. Treat each iteration as: implement → act → pause → observe → adjust. ## Skill paths (set once) ```bash export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" export WEB_GAME_CLIENT="$CODEX_HOME/skills/develop-web-game/scripts/web_game_playwright_client.js" export WEB_GAME_ACTIONS="$CODEX_HOME/skills/develop-web-game/references/action_payloads.json" ``` User-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`). ## Workflow 1. **Pick a goal.** Define a single feature or behavior to implement. 2. **Implement small.** Make the smallest change that moves the game forward. 3. **Ensure integration points.** Provide a single canvas and `window.render_game_to_text` so the test loop can read state. 4. **Add `window.advanceTime(ms)`.** Strongly prefer a deterministic step hook so the Playwright script can advance frames reliably; without it, automated tests can be flaky. 5. **Initialize progress.md.** If `progress.md` exists, read it first and confirm the original user prompt is recorded at the top (prefix with `Original prompt:`). Also note any TODOs and suggestions left by the previous agent. If missing, create it and write `Original prompt: ` at the top before appending updates. 6. **Verify Playwright availability.** Ensure `playwright` is available (local dependency or global install). If unsure, check `npx` first. 7. **Run the Playwright test script.** You must run `$WEB_GAME_CLIENT` after each meaningful change; do not invent a new client unless required. 8. **Use the payload reference.** Base actions on `$WEB_GAME_ACTIONS` to avoid guessing keys. 9. **Inspect state.** Capture screenshots and text state after each burst. 10. **Inspect screenshots.** Open the latest screenshot, verify expected visuals, fix any issues, and rerun the script. Repeat until correct. 11. **Verify controls and state (multi-step focus).** Exhaustively exercise all important interactions. For each, think through the full multi-step sequence it implies (cause → intermediate states → outcome) and verify the entire chain works end-to-end. Confirm `render_game_to_text` reflects the same state shown on screen. If anything is off, fix and rerun. Examples of important interactions: move, jump, shoot/attack, interact/use, select/confirm/cancel in menus, pause/resume, restart, and any special abilities or puzzle actions defined by the request. Multi-step examples: shooting an enemy should reduce its health; when health reaches 0 it should disappear and update the score; collecting a key should unlock a door and allow level progression. 12. **Check errors.** Review console errors and fix the first new issue before continuing. 13. **Reset between scenarios.** Avoid cross-test state when validating distinct features. 14. **Iterate with small deltas.** Change one variable at a time (frames, inputs, timing, positions), then repeat steps 7–13 until stable. Example command (actions required): ``` node "$WEB_GAME_CLIENT" --url http://localhost:5173 --actions-file "$WEB_GAME_ACTIONS" --click-selector "#start-btn" --iterations 3 --pause-ms 250 ``` Example actions (inline JSON): ```json { "steps": [ { "buttons": ["left_mouse_button"], "frames": 2, "mouse_x": 120, "mouse_y": 80 }, { "buttons": [], "frames": 6 }, { "buttons": ["right"], "frames": 8 }, { "buttons": ["space"], "frames": 4 } ] } ``` ## Test Checklist Test any new features added for the request and any areas your logic changes could affect. Identify issues, fix them, and re-run the tests to confirm they’re resolved. Examples of things to test: - Primary movement/interaction inputs (e.g., move, jump, shoot, confirm/select). - Win/lose or success/fail transitions. - Score/health/resource changes. - Boundary conditions (collisions, walls, screen edges). - Menu/pause/start flow if present. - Any special actions tied to the request (powerups, combos, abilities, puzzles, timers). ## Test Artifacts to Review - Latest screenshots from the Playwright run. - Latest `render_game_to_text` JSON output. - Console error logs (fix the first new error before continuing). You must actually open and visually inspect the latest screenshots after running the Playwright script, not just generate them. Ensure everything that should be visible on screen is actually visible. Go beyond the start screen and capture gameplay screenshots that cover all newly added features. Treat the screenshots as the source of truth; if something is missing, it is missing in the build. If you suspect a headless/WebGL capture issue, rerun the Playwright script in headed mode and re-check. Fix and rerun in a tight loop until the screenshots and text state look correct. Once fixes are verified, re-test all important interactions and controls, confirm they work, and ensure your changes did not introduce regressions. If they did, fix them and rerun everything in a loop until interactions, text state, and controls all work as expected. Be exhaustive in testing controls; broken games are not acceptable. ## Core Game Guidelines ### Canvas + Layout - Prefer a single canvas centered in the window. ### Visuals - Keep on-screen text minimal; show controls on a start/menu screen rather than overlaying them during play. - Avoid overly dark scenes unless the design calls for it. Make key elements easy to see. - Draw the background on the canvas itself instead of relying on CSS backgrounds. ### Text State Output (render_game_to_text) Expose a `window.render_game_to_text` function that returns a concise JSON string representing the current game state. The text should include enough information to play the game without visuals. Minimal pattern: ```js function renderGameToText() { const payload = { mode: state.mode, player: { x: state.player.x, y: state.player.y, r: state.player.r }, entities: state.entities.map((e) => ({ x: e.x, y: e.y, r: e.r })), score: state.score, }; return JSON.stringify(payload); } window.render_game_to_text = renderGameToText; ``` Keep the payload succinct and biased toward on-screen/interactive elements. Prefer current, visible entities over full history. Include a clear coordinate system note (origin and axis directions), and encode all player-relevant state: player position/velocity, active obstacles/enemies, collectibles, timers/cooldowns, score, and any mode/state flags needed to make correct decisions. Avoid large histories; only include what's currently relevant and visible. ### Time Stepping Hook Provide a deterministic time-stepping hook so the Playwright client can advance the game in controlled increments. Expose `window.advanceTime(ms)` (or a thin wrapper that forwards to your game update loop) and have the game loop use it when present. The Playwright test script uses this hook to step frames deterministically during automated testing. Minimal pattern: ```js window.advanceTime = (ms) => { const steps = Math.max(1, Math.round(ms / (1000 / 60))); for (let i = 0; i < steps; i++) update(1 / 60); render(); }; ``` ### Fullscreen Toggle - Use a single key (prefer `f`) to toggle fullscreen on/off. - Allow `Esc` to exit fullscreen. - When fullscreen toggles, resize the canvas/rendering so visuals and input mapping stay correct. ## Progress Tracking Create a `progress.md` file if it doesn't exist, and append TODOs, notes, gotchas, and loose ends as you go so another agent can pick up seamlessly. If a `progress.md` file already exists, read it first, including the original user prompt at the top (you may be continuing another agent's work). Do not overwrite the original prompt; preserve it. Update `progress.md` after each meaningful chunk of work (feature added, bug found, test run, or decision made). At the end of your work, leave TODOs and suggestions for the next agent in `progress.md`. ## Playwright Prerequisites - Prefer a local `playwright` dependency if the project already has it. - If unsure whether Playwright is available, check for `npx`: ``` command -v npx >/dev/null 2>&1 ``` - If `npx` is missing, install Node/npm and then install Playwright globally: ``` npm install -g @playwright/mcp@latest ``` - Do not switch to `@playwright/test` unless explicitly asked; stick to the client script. ## Scripts - `$WEB_GAME_CLIENT` (installed default: `$CODEX_HOME/skills/develop-web-game/scripts/web_game_playwright_client.js`) — Playwright-based action loop with virtual-time stepping, screenshot capture, and console error buffering. You must pass an action burst via `--actions-file`, `--actions-json`, or `--click`. ## References - `$WEB_GAME_ACTIONS` (installed default: `$CODEX_HOME/skills/develop-web-game/references/action_payloads.json`) — example action payloads (keyboard + mouse, per-frame capture). Use these to build your burst. ================================================ FILE: skills/.curated/develop-web-game/agents/openai.yaml ================================================ interface: display_name: "Develop Web Game" short_description: "Web game dev + Playwright test loop" icon_small: "./assets/game-small.svg" icon_large: "./assets/game.png" default_prompt: "Build and iterate a playable web game in this workspace, validating changes with a Playwright loop." ================================================ FILE: skills/.curated/develop-web-game/references/action_payloads.json ================================================ { "steps": [ { "buttons": ["left"], "frames": 6 }, { "buttons": [], "frames": 4 }, { "buttons": ["space"], "frames": 3 } ] } ================================================ FILE: skills/.curated/develop-web-game/scripts/web_game_playwright_client.js ================================================ import fs from "node:fs"; import path from "node:path"; import { chromium } from "playwright"; function parseArgs(argv) { const args = { url: null, iterations: 3, pauseMs: 250, headless: true, screenshotDir: "output/web-game", actionsFile: null, actionsJson: null, click: null, clickSelector: null, }; for (let i = 2; i < argv.length; i++) { const arg = argv[i]; const next = argv[i + 1]; if (arg === "--url" && next) { args.url = next; i++; } else if (arg === "--iterations" && next) { args.iterations = parseInt(next, 10); i++; } else if (arg === "--pause-ms" && next) { args.pauseMs = parseInt(next, 10); i++; } else if (arg === "--headless" && next) { args.headless = next !== "0" && next !== "false"; i++; } else if (arg === "--screenshot-dir" && next) { args.screenshotDir = next; i++; } else if (arg === "--actions-file" && next) { args.actionsFile = next; i++; } else if (arg === "--actions-json" && next) { args.actionsJson = next; i++; } else if (arg === "--click" && next) { const parts = next.split(",").map((v) => parseFloat(v.trim())); if (parts.length === 2 && parts.every((v) => Number.isFinite(v))) { args.click = { x: parts[0], y: parts[1] }; } i++; } else if (arg === "--click-selector" && next) { args.clickSelector = next; i++; } } if (!args.url) { throw new Error("--url is required"); } return args; } const buttonNameToKey = { up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight", enter: "Enter", space: "Space", a: "KeyA", b: "KeyB", }; async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function ensureDir(p) { fs.mkdirSync(p, { recursive: true }); } function makeVirtualTimeShim() { return `(() => { const pending = new Set(); const origSetTimeout = window.setTimeout.bind(window); const origSetInterval = window.setInterval.bind(window); const origRequestAnimationFrame = window.requestAnimationFrame.bind(window); window.__vt_pending = pending; window.setTimeout = (fn, t, ...rest) => { const task = {}; pending.add(task); return origSetTimeout(() => { pending.delete(task); fn(...rest); }, t); }; window.setInterval = (fn, t, ...rest) => { const task = {}; pending.add(task); return origSetInterval(() => { fn(...rest); }, t); }; window.requestAnimationFrame = (fn) => { const task = {}; pending.add(task); return origRequestAnimationFrame((ts) => { pending.delete(task); fn(ts); }); }; window.advanceTime = (ms) => { return new Promise((resolve) => { const start = performance.now(); function step(now) { if (now - start >= ms) return resolve(); origRequestAnimationFrame(step); } origRequestAnimationFrame(step); }); }; window.__drainVirtualTimePending = () => pending.size; })();`; } async function getCanvasHandle(page) { const handle = await page.evaluateHandle(() => { let best = null; let bestArea = 0; for (const canvas of document.querySelectorAll("canvas")) { const area = (canvas.width || canvas.clientWidth || 0) * (canvas.height || canvas.clientHeight || 0); if (area > bestArea) { bestArea = area; best = canvas; } } return best; }); return handle.asElement(); } async function captureCanvasPngBase64(canvas) { return canvas.evaluate((c) => { if (!c || typeof c.toDataURL !== "function") return ""; const data = c.toDataURL("image/png"); const idx = data.indexOf(","); return idx === -1 ? "" : data.slice(idx + 1); }); } async function isCanvasTransparent(canvas) { if (!canvas) return true; return canvas.evaluate((c) => { try { const w = c.width || c.clientWidth || 0; const h = c.height || c.clientHeight || 0; if (!w || !h) return true; const size = Math.max(1, Math.min(16, w, h)); const probe = document.createElement("canvas"); probe.width = size; probe.height = size; const ctx = probe.getContext("2d"); if (!ctx) return true; ctx.drawImage(c, 0, 0, size, size); const data = ctx.getImageData(0, 0, size, size).data; for (let i = 3; i < data.length; i += 4) { if (data[i] !== 0) return false; } return true; } catch { return false; } }); } async function captureScreenshot(page, canvas, outPath) { let buffer = null; let base64 = canvas ? await captureCanvasPngBase64(canvas) : ""; if (base64) { buffer = Buffer.from(base64, "base64"); const transparent = canvas ? await isCanvasTransparent(canvas) : false; if (transparent) buffer = null; } if (!buffer && canvas) { try { buffer = await canvas.screenshot({ type: "png" }); } catch { buffer = null; } } if (!buffer) { const bbox = canvas ? await canvas.boundingBox() : null; if (bbox) { buffer = await page.screenshot({ type: "png", omitBackground: false, clip: bbox, }); } else { buffer = await page.screenshot({ type: "png", omitBackground: false }); } } fs.writeFileSync(outPath, buffer); } class ConsoleErrorTracker { constructor() { this._seen = new Set(); this._errors = []; } ingest(err) { const key = JSON.stringify(err); if (this._seen.has(key)) return; this._seen.add(key); this._errors.push(err); } drain() { const next = [...this._errors]; this._errors = []; return next; } } async function doChoreography(page, canvas, steps) { for (const step of steps) { const buttons = new Set(step.buttons || []); for (const button of buttons) { if (button === "left_mouse_button" || button === "right_mouse_button") { const bbox = canvas ? await canvas.boundingBox() : null; if (!bbox) continue; const x = typeof step.mouse_x === "number" ? step.mouse_x : bbox.width / 2; const y = typeof step.mouse_y === "number" ? step.mouse_y : bbox.height / 2; await page.mouse.move(bbox.x + x, bbox.y + y); await page.mouse.down({ button: button === "left_mouse_button" ? "left" : "right" }); } else if (buttonNameToKey[button]) { await page.keyboard.down(buttonNameToKey[button]); } } const frames = step.frames || 1; for (let i = 0; i < frames; i++) { await page.evaluate(async () => { if (typeof window.advanceTime === "function") { await window.advanceTime(1000 / 60); } }); } for (const button of buttons) { if (button === "left_mouse_button" || button === "right_mouse_button") { await page.mouse.up({ button: button === "left_mouse_button" ? "left" : "right" }); } else if (buttonNameToKey[button]) { await page.keyboard.up(buttonNameToKey[button]); } } } } async function main() { const args = parseArgs(process.argv); ensureDir(args.screenshotDir); const browser = await chromium.launch({ headless: args.headless, args: ["--use-gl=angle", "--use-angle=swiftshader"], }); const page = await browser.newPage(); const consoleErrors = new ConsoleErrorTracker(); page.on("console", (msg) => { if (msg.type() !== "error") return; consoleErrors.ingest({ type: "console.error", text: msg.text() }); }); page.on("pageerror", (err) => { consoleErrors.ingest({ type: "pageerror", text: String(err) }); }); await page.addInitScript({ content: makeVirtualTimeShim() }); await page.goto(args.url, { waitUntil: "domcontentloaded" }); await page.waitForTimeout(500); await page.evaluate(() => { window.dispatchEvent(new Event("resize")); }); let canvas = await getCanvasHandle(page); if (args.clickSelector) { try { await page.click(args.clickSelector, { timeout: 5000 }); await page.waitForTimeout(250); } catch (err) { console.warn("Failed to click selector", args.clickSelector, err); } } let steps = null; if (args.actionsFile) { const raw = fs.readFileSync(args.actionsFile, "utf-8"); const parsed = JSON.parse(raw); if (Array.isArray(parsed)) steps = parsed; if (parsed && Array.isArray(parsed.steps)) steps = parsed.steps; } else if (args.actionsJson) { const parsed = JSON.parse(args.actionsJson); if (Array.isArray(parsed)) steps = parsed; if (parsed && Array.isArray(parsed.steps)) steps = parsed.steps; } else if (args.click) { steps = [ { buttons: ["left_mouse_button"], frames: 2, mouse_x: args.click.x, mouse_y: args.click.y, }, ]; } if (!steps) { throw new Error("Actions are required. Use --actions-file, --actions-json, or --click."); } for (let i = 0; i < args.iterations; i++) { if (!canvas) canvas = await getCanvasHandle(page); await doChoreography(page, canvas, steps); await sleep(args.pauseMs); const shotPath = path.join(args.screenshotDir, `shot-${i}.png`); await captureScreenshot(page, canvas, shotPath); const text = await page.evaluate(() => { if (typeof window.render_game_to_text === "function") { return window.render_game_to_text(); } return null; }); if (text) { fs.writeFileSync(path.join(args.screenshotDir, `state-${i}.json`), text); } const freshErrors = consoleErrors.drain(); if (freshErrors.length) { fs.writeFileSync( path.join(args.screenshotDir, `errors-${i}.json`), JSON.stringify(freshErrors, null, 2) ); break; } } await browser.close(); } main().catch((err) => { console.error(err); process.exit(1); }); ================================================ FILE: skills/.curated/doc/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don\'t include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/doc/SKILL.md ================================================ --- name: "doc" description: "Use when the task involves reading, creating, or editing `.docx` documents, especially when formatting or layout fidelity matters; prefer `python-docx` plus the bundled `scripts/render_docx.py` for visual checks." --- # DOCX Skill ## When to use - Read or review DOCX content where layout matters (tables, diagrams, pagination). - Create or edit DOCX files with professional formatting. - Validate visual layout before delivery. ## Workflow 1. Prefer visual review (layout, tables, diagrams). - If `soffice` and `pdftoppm` are available, convert DOCX -> PDF -> PNGs. - Or use `scripts/render_docx.py` (requires `pdf2image` and Poppler). - If these tools are missing, install them or ask the user to review rendered pages locally. 2. Use `python-docx` for edits and structured creation (headings, styles, tables, lists). 3. After each meaningful change, re-render and inspect the pages. 4. If visual review is not possible, extract text with `python-docx` as a fallback and call out layout risk. 5. Keep intermediate outputs organized and clean up after final approval. ## Temp and output conventions - Use `tmp/docs/` for intermediate files; delete when done. - Write final artifacts under `output/doc/` when working in this repo. - Keep filenames stable and descriptive. ## Dependencies (install if missing) Prefer `uv` for dependency management. Python packages: ``` uv pip install python-docx pdf2image ``` If `uv` is unavailable: ``` python3 -m pip install python-docx pdf2image ``` System tools (for rendering): ``` # macOS (Homebrew) brew install libreoffice poppler # Ubuntu/Debian sudo apt-get install -y libreoffice poppler-utils ``` If installation isn't possible in this environment, tell the user which dependency is missing and how to install it locally. ## Environment No required environment variables. ## Rendering commands DOCX -> PDF: ``` soffice -env:UserInstallation=file:///tmp/lo_profile_$$ --headless --convert-to pdf --outdir $OUTDIR $INPUT_DOCX ``` PDF -> PNGs: ``` pdftoppm -png $OUTDIR/$BASENAME.pdf $OUTDIR/$BASENAME ``` Bundled helper: ``` python3 scripts/render_docx.py /path/to/file.docx --output_dir /tmp/docx_pages ``` ## Quality expectations - Deliver a client-ready document: consistent typography, spacing, margins, and clear hierarchy. - Avoid formatting defects: clipped/overlapping text, broken tables, unreadable characters, or default-template styling. - Charts, tables, and visuals must be legible in rendered pages with correct alignment. - Use ASCII hyphens only. Avoid U+2011 (non-breaking hyphen) and other Unicode dashes. - Citations and references must be human-readable; never leave tool tokens or placeholder strings. ## Final checks - Re-render and inspect every page at 100% zoom before final delivery. - Fix any spacing, alignment, or pagination issues and repeat the render loop. - Confirm there are no leftovers (temp files, duplicate renders) unless the user asks to keep them. ================================================ FILE: skills/.curated/doc/agents/openai.yaml ================================================ interface: display_name: "Word Docs" short_description: "Edit and review docx files" icon_small: "./assets/doc-small.svg" icon_large: "./assets/doc.png" default_prompt: "Edit or review this .docx file and return the updated file plus a concise change summary." ================================================ FILE: skills/.curated/doc/scripts/render_docx.py ================================================ import argparse import os import re import subprocess import tempfile import xml.etree.ElementTree as ET from os import makedirs, replace from os.path import abspath, basename, exists, expanduser, join, splitext from shutil import which import sys from typing import Sequence, cast from zipfile import ZipFile from pdf2image import convert_from_path, pdfinfo_from_path TWIPS_PER_INCH: int = 1440 def ensure_system_tools() -> None: missing: list[str] = [] for tool in ("soffice", "pdftoppm"): if which(tool) is None: missing.append(tool) if missing: tools = ", ".join(missing) raise RuntimeError( f"Missing required system tool(s): {tools}. Install LibreOffice and Poppler, then retry." ) def calc_dpi_via_ooxml_docx(input_path: str, max_w_px: int, max_h_px: int) -> int: """Calculate DPI from OOXML `word/document.xml` page size (w:pgSz in twips). DOCX stores page dimensions in section properties as twips (1/1440 inch). We read the first encountered section's page size and compute an isotropic DPI that fits within the target max pixel dimensions. """ with ZipFile(input_path, "r") as zf: xml = zf.read("word/document.xml") root = ET.fromstring(xml) ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} # Common placements: w:body/w:sectPr or w:body/w:p/w:pPr/w:sectPr sect_pr = root.find(".//w:sectPr", ns) if sect_pr is None: raise RuntimeError("Section properties not found in document.xml") pg_sz = sect_pr.find("w:pgSz", ns) if pg_sz is None: raise RuntimeError("Page size not found in section properties") # Values are in twips w_twips_str = pg_sz.get( "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w" ) or pg_sz.get("w") h_twips_str = pg_sz.get( "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}h" ) or pg_sz.get("h") if not w_twips_str or not h_twips_str: raise RuntimeError("Page size attributes missing in pgSz") width_in = int(w_twips_str) / TWIPS_PER_INCH height_in = int(h_twips_str) / TWIPS_PER_INCH if width_in <= 0 or height_in <= 0: raise RuntimeError("Invalid page size values in document.xml") return round(min(max_w_px / width_in, max_h_px / height_in)) def calc_dpi_via_pdf(input_path: str, max_w_px: int, max_h_px: int) -> int: """Convert input to PDF and compute DPI from its page size.""" with tempfile.TemporaryDirectory(prefix="soffice_profile_") as user_profile: with tempfile.TemporaryDirectory(prefix="soffice_convert_") as convert_tmp_dir: stem = splitext(basename(input_path))[0] pdf_path = convert_to_pdf(input_path, user_profile, convert_tmp_dir, stem) if not (pdf_path and exists(pdf_path)): raise RuntimeError("Failed to convert input to PDF for DPI computation.") info = pdfinfo_from_path(pdf_path) size_val = info.get("Page size") if not size_val: for k, v in info.items(): if isinstance(v, str) and "size" in k.lower() and "pts" in v: size_val = v break if not isinstance(size_val, str): raise RuntimeError("Failed to read PDF page size for DPI computation.") m = re.search(r"(\d+)\s*x\s*(\d+)\s*pts", size_val) if not m: raise RuntimeError("Unrecognized PDF page size format.") width_pts = int(m.group(1)) height_pts = int(m.group(2)) width_in = width_pts / 72.0 height_in = height_pts / 72.0 if width_in <= 0 or height_in <= 0: raise RuntimeError("Invalid PDF page size values.") return round(min(max_w_px / width_in, max_h_px / height_in)) def run_cmd_no_check(cmd: list[str]) -> None: subprocess.run( cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=os.environ.copy(), ) def convert_to_pdf( doc_path: str, user_profile: str, convert_tmp_dir: str, stem: str, ) -> str: # Try direct DOC(X) -> PDF cmd_pdf = [ "soffice", "-env:UserInstallation=file://" + user_profile, "--invisible", "--headless", "--norestore", "--convert-to", "pdf", "--outdir", convert_tmp_dir, doc_path, ] run_cmd_no_check(cmd_pdf) pdf_path = join(convert_tmp_dir, f"{stem}.pdf") if exists(pdf_path): return pdf_path # Fallback: DOCX -> ODT, then ODT -> PDF cmd_odt = [ "soffice", "-env:UserInstallation=file://" + user_profile, "--invisible", "--headless", "--norestore", "--convert-to", "odt", "--outdir", convert_tmp_dir, doc_path, ] run_cmd_no_check(cmd_odt) odt_path = join(convert_tmp_dir, f"{stem}.odt") if exists(odt_path): cmd_odt_pdf = [ "soffice", "-env:UserInstallation=file://" + user_profile, "--invisible", "--headless", "--norestore", "--convert-to", "pdf", "--outdir", convert_tmp_dir, odt_path, ] run_cmd_no_check(cmd_odt_pdf) if exists(pdf_path): return pdf_path return "" def rasterize( doc_path: str, out_dir: str, dpi: int, ) -> Sequence[str]: """Rasterise DOCX (or similar) to images placed in out_dir and return their paths. Images are named as page-. with pages starting at 1. """ makedirs(out_dir, exist_ok=True) doc_path = abspath(doc_path) stem = splitext(basename(doc_path))[0] # Use a unique user profile to avoid LibreOffice profile lock when running concurrently with tempfile.TemporaryDirectory(prefix="soffice_profile_") as user_profile: # Write conversion outputs into a temp directory to avoid any IO oddities with tempfile.TemporaryDirectory(prefix="soffice_convert_") as convert_tmp_dir: pdf_path = convert_to_pdf( doc_path, user_profile, convert_tmp_dir, stem, ) if not pdf_path or not exists(pdf_path): raise RuntimeError( "Failed to produce PDF for rasterization (direct and ODT fallback)." ) paths_raw = cast( list[str], convert_from_path( pdf_path, dpi=dpi, fmt="png", thread_count=8, output_folder=out_dir, paths_only=True, output_file="page", ), ) # Rename convert_from_path's output format f'page{thread_id:04d}-{page_num:02d}.' to 'page-.' pages: list[tuple[int, str]] = [] for src_path in paths_raw: base = splitext(basename(src_path))[0] page_num_str = base.split("-")[-1] page_num = int(page_num_str) dst_path = join(out_dir, f"page-{page_num}.png") replace(src_path, dst_path) pages.append((page_num, dst_path)) pages.sort(key=lambda t: t[0]) final_paths = [path for _, path in pages] return final_paths def main() -> None: parser = argparse.ArgumentParser(description="Render DOCX-like file to PNG images.") parser.add_argument( "input_path", type=str, help="Path to the input DOCX file (or compatible).", ) parser.add_argument( "--output_dir", type=str, default=None, help=( "Output directory for the rendered images. " "Defaults to a folder next to the input named after the input file (without extension)." ), ) parser.add_argument( "--width", type=int, default=1600, help=( "Approximate maximum width in pixels after isotropic scaling (default 1600). " "The actual value may exceed slightly." ), ) parser.add_argument( "--height", type=int, default=2000, help=( "Approximate maximum height in pixels after isotropic scaling (default 2000). " "The actual value may exceed slightly." ), ) parser.add_argument( "--dpi", type=int, default=None, help=("Override computed DPI. If provided, skips DOCX/PDF-based DPI calculation."), ) args = parser.parse_args() try: ensure_system_tools() input_path = abspath(expanduser(args.input_path)) out_dir = ( abspath(expanduser(args.output_dir)) if args.output_dir else splitext(input_path)[0] ) if args.dpi is not None: dpi = int(args.dpi) else: try: if input_path.lower().endswith((".docx", ".docm", ".dotx", ".dotm")): dpi = calc_dpi_via_ooxml_docx(input_path, args.width, args.height) else: raise RuntimeError("Skip OOXML DPI; not a DOCX container") except Exception: dpi = calc_dpi_via_pdf(input_path, args.width, args.height) rasterize(input_path, out_dir, dpi) print("Pages rendered to " + out_dir) except RuntimeError as exc: print(f"Error: {exc}", file=sys.stderr) raise SystemExit(1) if __name__ == "__main__": main() ================================================ FILE: skills/.curated/figma/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/figma/SKILL.md ================================================ --- name: figma description: Use the Figma MCP server to fetch design context, screenshots, variables, and assets from Figma, and to translate Figma nodes into production code. Trigger when a task involves Figma URLs, node IDs, design-to-code implementation, or Figma MCP setup and troubleshooting. --- # Figma MCP Use the Figma MCP server for Figma-driven implementation. For setup and debugging details (env vars, config, verification), see `references/figma-mcp-config.md`. ## Figma MCP Integration Rules These rules define how to translate Figma inputs into code for this project and must be followed for every Figma-driven change. ### Required flow (do not skip) 1. Run get_design_context first to fetch the structured representation for the exact node(s). 2. If the response is too large or truncated, run get_metadata to get the high-level node map and then re-fetch only the required node(s) with get_design_context. 3. Run get_screenshot for a visual reference of the node variant being implemented. 4. Only after you have both get_design_context and get_screenshot, download any assets needed and start implementation. 5. Translate the output (usually React + Tailwind) into this project's conventions, styles and framework. Reuse the project's color tokens, components, and typography wherever possible. 6. Validate against Figma for 1:1 look and behavior before marking complete. ### Implementation rules - Treat the Figma MCP output (React + Tailwind) as a representation of design and behavior, not as final code style. - Replace Tailwind utility classes with the project's preferred utilities/design-system tokens when applicable. - Reuse existing components (e.g., buttons, inputs, typography, icon wrappers) instead of duplicating functionality. - Use the project's color system, typography scale, and spacing tokens consistently. - Respect existing routing, state management, and data-fetch patterns already adopted in the repo. - Strive for 1:1 visual parity with the Figma design. When conflicts arise, prefer design-system tokens and adjust spacing or sizes minimally to match visuals. - Validate the final UI against the Figma screenshot for both look and behavior. ### Asset handling - The Figma MCP Server provides an assets endpoint which can serve image and SVG assets. - IMPORTANT: If the Figma MCP Server returns a localhost source for an image or an SVG, use that image or SVG source directly. - IMPORTANT: DO NOT import/add new icon packages, all the assets should be in the Figma payload. - IMPORTANT: do NOT use or create placeholders if a localhost source is provided. ### Link-based prompting - The server is link-based: copy the Figma frame/layer link and give that URL to the MCP client when asking for implementation help. - The client cannot browse the URL but extracts the node ID from the link; always ensure the link points to the exact node/variant you want. ## References - `references/figma-mcp-config.md` — setup, verification, troubleshooting, and link-based usage reminders. - `references/figma-tools-and-prompts.md` — tool catalog and prompt patterns for selecting frameworks/components and fetching metadata. ================================================ FILE: skills/.curated/figma/agents/openai.yaml ================================================ interface: display_name: "Figma" short_description: "Use Figma MCP for design-to-code work" icon_small: "./assets/figma-small.svg" icon_large: "./assets/figma.png" default_prompt: "Use Figma MCP to inspect the target design and translate it into implementable UI decisions." dependencies: tools: - type: "mcp" value: "figma" description: "Figma MCP server" transport: "streamable_http" url: "https://mcp.figma.com/mcp" ================================================ FILE: skills/.curated/figma/references/figma-mcp-config.md ================================================ # Figma MCP config reference Use this snippet to register the Figma MCP server in `~/.codex/config.toml` as a streamable HTTP server with bearer auth pulled from your env. ```toml [mcp_servers.figma] url = "https://mcp.figma.com/mcp" bearer_token_env_var = "FIGMA_OAUTH_TOKEN" http_headers = { "X-Figma-Region" = "us-east-1" } ``` ## Notes and options - The bearer token must be available as `FIGMA_OAUTH_TOKEN` in the environment that launches Codex. - Keep the region header aligned with your Figma region. If your org uses another region, update `X-Figma-Region` consistently. - OAuth on streamable HTTP requires the RMCP client: set `[features].rmcp_client = true` (or `experimental_use_rmcp_client = true` on older builds) at the top level of `config.toml`. - Optional per-server timeouts: `startup_timeout_sec` (default 10) and `tool_timeout_sec` (default 60) can be set inside `[mcp_servers.figma]` if needed. ## Env var setup (if missing) - One-time set for current shell: `export FIGMA_OAUTH_TOKEN=""` - Persist for future sessions: add the export line to your shell profile (e.g., `~/.zshrc` or `~/.bashrc`), then restart the shell or your IDE. - Verify before launching Codex: `echo $FIGMA_OAUTH_TOKEN` should print a non-empty token. ## Setup + verification checklist - Add the snippet above to `~/.codex/config.toml` under `[mcp_servers.figma]`, and enable `[features].rmcp_client = true` (or `experimental_use_rmcp_client = true` on older releases). - Restart Codex (CLI/IDE) after updating config and env vars. - Ask Codex to list Figma tools or run a simple call to confirm the server is reachable. ## Troubleshooting - Token not picked up: Export `FIGMA_OAUTH_TOKEN` in the same shell that launches Codex, or add it to your shell profile and restart. - OAuth errors: Verify `rmcp_client` is enabled and the bearer token is valid. Tokens copied from Figma should not include surrounding quotes. - Network/headers: Keep the `X-Figma-Region` header; if your org uses another region, update the header consistently across config and requests. ## Usage reminders - The server is link-based: copy the Figma frame or layer link, then ask the MCP client to implement that URL. The client will extract the node ID from the link (it does not browse the page). - If output feels generic, restate the project-specific rules from the main skill and ensure you follow the required flow (get_design_context → get_metadata if needed → get_screenshot). ================================================ FILE: skills/.curated/figma/references/figma-tools-and-prompts.md ================================================ # Figma MCP tools and prompt patterns Quick reference for the Figma MCP toolset, when to use each tool, and prompt examples to steer output toward your stack. ## Core tools - `get_design_context` (Figma Design, Figma Make): Primary tool. Returns structured design data and default React + Tailwind code. Selection-based prompting works on desktop; the remote server uses a frame/layer link to extract the node ID. - `get_variable_defs` (Figma Design): Lists variables/styles (colors, spacing, typography) used in the selection. Useful to align with tokens. - `get_metadata` (Figma Design): Sparse XML outline of layer IDs/names/types/positions/sizes. Use before re-calling `get_design_context` on large nodes to avoid truncation. - `get_screenshot` (Figma Design, FigJam): Screenshot of the selection for visual fidelity checks. - `get_figjam` (FigJam): XML + screenshots for FigJam diagrams (architecture, flows). - `create_design_system_rules` (no file context): Generates a rule file with design-to-code guidance for your stack. Save it where the agent can read it. - `get_code_connect_map` (Figma Design): Returns mapping of Figma node IDs to code components (`codeConnectSrc`, `codeConnectName`). Use to reuse existing components. - `add_code_connect_map` (Figma Design): Adds/updates a mapping between a Figma node and a code component to improve reuse. - `get_strategy_for_mapping` (alpha, local only): Figma-prompted tool to decide mapping strategy for connecting a node to a code component. - `send_get_strategy_response` (alpha, local only): Sends the response after `get_strategy_for_mapping`. - `whoami` (remote only): Returns the authenticated Figma user identity (email, plans, seat types). ## Prompt patterns (design context) - Change framework: “generate my Figma selection in Vue” or “in plain HTML + CSS” or “for iOS”. - Use my components: “generate my Figma selection using components from `src/components/ui`”. - Combine: “generate my Figma selection using components from `src/ui` and style with Tailwind”. - Note: On the remote server, selection-based prompting requires a frame/layer link; the server extracts the node ID from the URL. ## Prompt patterns (variables/styles) - “get the variables used in my Figma selection” - “what color and spacing variables are used in my Figma selection?” - “list the variable names and their values used in my Figma selection” ## Prompt patterns (code connect) - “show the code connect map for this selection” - “map this node to `src/components/ui/Button.tsx` with name `Button`” ## Best-practice flow reminder Use `get_design_context` → (optionally `get_metadata` for large nodes) → `get_screenshot`, and keep project rules from `SKILL.md` in mind when applying the generated output. ================================================ FILE: skills/.curated/figma-implement-design/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/figma-implement-design/SKILL.md ================================================ --- name: "figma-implement-design" description: "Translate Figma nodes into production-ready code with 1:1 visual fidelity using the Figma MCP workflow (design context, screenshots, assets, and project-convention translation). Trigger when the user provides Figma URLs or node IDs, or asks to implement designs or components that must match Figma specs. Requires a working Figma MCP server connection." --- # Implement Design ## Overview This skill provides a structured workflow for translating Figma designs into production-ready code with pixel-perfect accuracy. It ensures consistent integration with the Figma MCP server, proper use of design tokens, and 1:1 visual parity with designs. ## Prerequisites - Figma MCP server must be connected and accessible - User must provide a Figma URL in the format: `https://figma.com/design/:fileKey/:fileName?node-id=1-2` - `:fileKey` is the file key - `1-2` is the node ID (the specific component or frame to implement) - **OR** when using `figma-desktop` MCP: User can select a node directly in the Figma desktop app (no URL required) - Project should have an established design system or component library (preferred) ## Required Workflow **Follow these steps in order. Do not skip steps.** ### Step 0: Set up Figma MCP (if not already configured) If any MCP call fails because Figma MCP is not connected, pause and set it up: 1. Add the Figma MCP: - `codex mcp add figma --url https://mcp.figma.com/mcp` 2. Enable remote MCP client: - Set `[features].rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client` 3. Log in with OAuth: - `codex mcp login figma` After successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1. ### Step 1: Get Node ID #### Option A: Parse from Figma URL When the user provides a Figma URL, extract the file key and node ID to pass as arguments to MCP tools. **URL format:** `https://figma.com/design/:fileKey/:fileName?node-id=1-2` **Extract:** - **File key:** `:fileKey` (the segment after `/design/`) - **Node ID:** `1-2` (the value of the `node-id` query parameter) **Note:** When using the local desktop MCP (`figma-desktop`), `fileKey` is not passed as a parameter to tool calls. The server automatically uses the currently open file, so only `nodeId` is needed. **Example:** - URL: `https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15` - File key: `kL9xQn2VwM8pYrTb4ZcHjF` - Node ID: `42-15` #### Option B: Use Current Selection from Figma Desktop App (figma-desktop MCP only) When using the `figma-desktop` MCP and the user has NOT provided a URL, the tools automatically use the currently selected node from the open Figma file in the desktop app. **Note:** Selection-based prompting only works with the `figma-desktop` MCP server. The remote server requires a link to a frame or layer to extract context. The user must have the Figma desktop app open with a node selected. ### Step 2: Fetch Design Context Run `get_design_context` with the extracted file key and node ID. ``` get_design_context(fileKey=":fileKey", nodeId="1-2") ``` This provides the structured data including: - Layout properties (Auto Layout, constraints, sizing) - Typography specifications - Color values and design tokens - Component structure and variants - Spacing and padding values **If the response is too large or truncated:** 1. Run `get_metadata(fileKey=":fileKey", nodeId="1-2")` to get the high-level node map 2. Identify the specific child nodes needed from the metadata 3. Fetch individual child nodes with `get_design_context(fileKey=":fileKey", nodeId=":childNodeId")` ### Step 3: Capture Visual Reference Run `get_screenshot` with the same file key and node ID for a visual reference. ``` get_screenshot(fileKey=":fileKey", nodeId="1-2") ``` This screenshot serves as the source of truth for visual validation. Keep it accessible throughout implementation. ### Step 4: Download Required Assets Download any assets (images, icons, SVGs) returned by the Figma MCP server. **IMPORTANT:** Follow these asset rules: - If the Figma MCP server returns a `localhost` source for an image or SVG, use that source directly - DO NOT import or add new icon packages - all assets should come from the Figma payload - DO NOT use or create placeholders if a `localhost` source is provided - Assets are served through the Figma MCP server's built-in assets endpoint ### Step 5: Translate to Project Conventions Translate the Figma output into this project's framework, styles, and conventions. **Key principles:** - Treat the Figma MCP output (typically React + Tailwind) as a representation of design and behavior, not as final code style - Replace Tailwind utility classes with the project's preferred utilities or design system tokens - Reuse existing components (buttons, inputs, typography, icon wrappers) instead of duplicating functionality - Use the project's color system, typography scale, and spacing tokens consistently - Respect existing routing, state management, and data-fetch patterns ### Step 6: Achieve 1:1 Visual Parity Strive for pixel-perfect visual parity with the Figma design. **Guidelines:** - Prioritize Figma fidelity to match designs exactly - Avoid hardcoded values - use design tokens from Figma where available - When conflicts arise between design system tokens and Figma specs, prefer design system tokens but adjust spacing or sizes minimally to match visuals - Follow WCAG requirements for accessibility - Add component documentation as needed ### Step 7: Validate Against Figma Before marking complete, validate the final UI against the Figma screenshot. **Validation checklist:** - [ ] Layout matches (spacing, alignment, sizing) - [ ] Typography matches (font, size, weight, line height) - [ ] Colors match exactly - [ ] Interactive states work as designed (hover, active, disabled) - [ ] Responsive behavior follows Figma constraints - [ ] Assets render correctly - [ ] Accessibility standards met ## Implementation Rules ### Component Organization - Place UI components in the project's designated design system directory - Follow the project's component naming conventions - Avoid inline styles unless truly necessary for dynamic values ### Design System Integration - ALWAYS use components from the project's design system when possible - Map Figma design tokens to project design tokens - When a matching component exists, extend it rather than creating a new one - Document any new components added to the design system ### Code Quality - Avoid hardcoded values - extract to constants or design tokens - Keep components composable and reusable - Add TypeScript types for component props - Include JSDoc comments for exported components ## Examples ### Example 1: Implementing a Button Component User says: "Implement this Figma button component: https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15" **Actions:** 1. Parse URL to extract fileKey=`kL9xQn2VwM8pYrTb4ZcHjF` and nodeId=`42-15` 2. Run `get_design_context(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")` 3. Run `get_screenshot(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")` for visual reference 4. Download any button icons from the assets endpoint 5. Check if project has existing button component 6. If yes, extend it with new variant; if no, create new component using project conventions 7. Map Figma colors to project design tokens (e.g., `primary-500`, `primary-hover`) 8. Validate against screenshot for padding, border radius, typography **Result:** Button component matching Figma design, integrated with project design system. ### Example 2: Building a Dashboard Layout User says: "Build this dashboard: https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Dashboard?node-id=10-5" **Actions:** 1. Parse URL to extract fileKey=`pR8mNv5KqXzGwY2JtCfL4D` and nodeId=`10-5` 2. Run `get_metadata(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` to understand the page structure 3. Identify main sections from metadata (header, sidebar, content area, cards) and their child node IDs 4. Run `get_design_context(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId=":childNodeId")` for each major section 5. Run `get_screenshot(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` for the full page 6. Download all assets (logos, icons, charts) 7. Build layout using project's layout primitives 8. Implement each section using existing components where possible 9. Validate responsive behavior against Figma constraints **Result:** Complete dashboard matching Figma design with responsive layout. ## Best Practices ### Always Start with Context Never implement based on assumptions. Always fetch `get_design_context` and `get_screenshot` first. ### Incremental Validation Validate frequently during implementation, not just at the end. This catches issues early. ### Document Deviations If you must deviate from the Figma design (e.g., for accessibility or technical constraints), document why in code comments. ### Reuse Over Recreation Always check for existing components before creating new ones. Consistency across the codebase is more important than exact Figma replication. ### Design System First When in doubt, prefer the project's design system patterns over literal Figma translation. ## Common Issues and Solutions ### Issue: Figma output is truncated **Cause:** The design is too complex or has too many nested layers to return in a single response. **Solution:** Use `get_metadata` to get the node structure, then fetch specific nodes individually with `get_design_context`. ### Issue: Design doesn't match after implementation **Cause:** Visual discrepancies between the implemented code and the original Figma design. **Solution:** Compare side-by-side with the screenshot from Step 3. Check spacing, colors, and typography values in the design context data. ### Issue: Assets not loading **Cause:** The Figma MCP server's assets endpoint is not accessible or the URLs are being modified. **Solution:** Verify the Figma MCP server's assets endpoint is accessible. The server serves assets at `localhost` URLs. Use these directly without modification. ### Issue: Design token values differ from Figma **Cause:** The project's design system tokens have different values than those specified in the Figma design. **Solution:** When project tokens differ from Figma values, prefer project tokens for consistency but adjust spacing/sizing to maintain visual fidelity. ## Understanding Design Implementation The Figma implementation workflow establishes a reliable process for translating designs to code: **For designers:** Confidence that implementations will match their designs with pixel-perfect accuracy. **For developers:** A structured approach that eliminates guesswork and reduces back-and-forth revisions. **For teams:** Consistent, high-quality implementations that maintain design system integrity. By following this workflow, you ensure that every Figma design is implemented with the same level of care and attention to detail. ## Additional Resources - [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server/) - [Figma MCP Server Tools and Prompts](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/) - [Figma Variables and Design Tokens](https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma) ================================================ FILE: skills/.curated/figma-implement-design/agents/openai.yaml ================================================ interface: display_name: "Figma Implement Design" short_description: "Turn Figma designs into production-ready code" icon_small: "./assets/figma-small.svg" icon_large: "./assets/figma.png" default_prompt: "Implement this Figma design in this codebase, matching layout, states, and responsive behavior." dependencies: tools: - type: "mcp" value: "figma" description: "Figma MCP server" transport: "streamable_http" url: "https://mcp.figma.com/mcp" ================================================ FILE: skills/.curated/gh-address-comments/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/gh-address-comments/SKILL.md ================================================ --- name: gh-address-comments description: Help address review/issue comments on the open GitHub PR for the current branch using gh CLI; verify gh auth first and prompt the user to authenticate if not logged in. metadata: short-description: Address comments in a GitHub PR review --- # PR Comment Handler Guide to find the open PR for the current branch and address its comments with gh CLI. Run all `gh` commands with elevated network access. Prereq: ensure `gh` is authenticated (for example, run `gh auth login` once), then run `gh auth status` with escalated permissions (include workflow/repo scopes) so `gh` commands succeed. If sandboxing blocks `gh auth status`, rerun it with `sandbox_permissions=require_escalated`. ## 1) Inspect comments needing attention - Run scripts/fetch_comments.py which will print out all the comments and review threads on the PR ## 2) Ask the user for clarification - Number all the review threads and comments and provide a short summary of what would be required to apply a fix for it - Ask the user which numbered comments should be addressed ## 3) If user chooses comments - Apply fixes for the selected comments Notes: - If gh hits auth/rate issues mid-run, prompt the user to re-authenticate with `gh auth login`, then retry. ================================================ FILE: skills/.curated/gh-address-comments/agents/openai.yaml ================================================ interface: display_name: "GitHub Address Comments" short_description: Address comments in a GitHub PR review" icon_small: "./assets/github-small.svg" icon_large: "./assets/github.png" default_prompt: "Address all actionable GitHub PR review comments in this branch and summarize the updates." ================================================ FILE: skills/.curated/gh-address-comments/scripts/fetch_comments.py ================================================ #!/usr/bin/env python3 """ Fetch all PR conversation comments + reviews + review threads (inline threads) for the PR associated with the current git branch, by shelling out to: gh api graphql Requires: - `gh auth login` already set up - current branch has an associated (open) PR Usage: python fetch_comments.py > pr_comments.json """ from __future__ import annotations import json import subprocess import sys from typing import Any QUERY = """\ query( $owner: String!, $repo: String!, $number: Int!, $commentsCursor: String, $reviewsCursor: String, $threadsCursor: String ) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { number url title state # Top-level "Conversation" comments (issue comments on the PR) comments(first: 100, after: $commentsCursor) { pageInfo { hasNextPage endCursor } nodes { id body createdAt updatedAt author { login } } } # Review submissions (Approve / Request changes / Comment), with body if present reviews(first: 100, after: $reviewsCursor) { pageInfo { hasNextPage endCursor } nodes { id state body submittedAt author { login } } } # Inline review threads (grouped), includes resolved state reviewThreads(first: 100, after: $threadsCursor) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line diffSide startLine startDiffSide originalLine originalStartLine resolvedBy { login } comments(first: 100) { nodes { id body createdAt updatedAt author { login } } } } } } } } """ def _run(cmd: list[str], stdin: str | None = None) -> str: p = subprocess.run(cmd, input=stdin, capture_output=True, text=True) if p.returncode != 0: raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{p.stderr}") return p.stdout def _run_json(cmd: list[str], stdin: str | None = None) -> dict[str, Any]: out = _run(cmd, stdin=stdin) try: return json.loads(out) except json.JSONDecodeError as e: raise RuntimeError(f"Failed to parse JSON from command output: {e}\nRaw:\n{out}") from e def _ensure_gh_authenticated() -> None: try: _run(["gh", "auth", "status"]) except RuntimeError: print("run `gh auth login` to authenticate the GitHub CLI", file=sys.stderr) raise RuntimeError("gh auth status failed; run `gh auth login` to authenticate the GitHub CLI") from None def gh_pr_view_json(fields: str) -> dict[str, Any]: # fields is a comma-separated list like: "number,headRepositoryOwner,headRepository" return _run_json(["gh", "pr", "view", "--json", fields]) def get_current_pr_ref() -> tuple[str, str, int]: """ Resolve the PR for the current branch (whatever gh considers associated). Works for cross-repo PRs too, by reading head repository owner/name. """ pr = gh_pr_view_json("number,headRepositoryOwner,headRepository") owner = pr["headRepositoryOwner"]["login"] repo = pr["headRepository"]["name"] number = int(pr["number"]) return owner, repo, number def gh_api_graphql( owner: str, repo: str, number: int, comments_cursor: str | None = None, reviews_cursor: str | None = None, threads_cursor: str | None = None, ) -> dict[str, Any]: """ Call `gh api graphql` using -F variables, avoiding JSON blobs with nulls. Query is passed via stdin using query=@- to avoid shell newline/quoting issues. """ cmd = [ "gh", "api", "graphql", "-F", "query=@-", "-F", f"owner={owner}", "-F", f"repo={repo}", "-F", f"number={number}", ] if comments_cursor: cmd += ["-F", f"commentsCursor={comments_cursor}"] if reviews_cursor: cmd += ["-F", f"reviewsCursor={reviews_cursor}"] if threads_cursor: cmd += ["-F", f"threadsCursor={threads_cursor}"] return _run_json(cmd, stdin=QUERY) def fetch_all(owner: str, repo: str, number: int) -> dict[str, Any]: conversation_comments: list[dict[str, Any]] = [] reviews: list[dict[str, Any]] = [] review_threads: list[dict[str, Any]] = [] comments_cursor: str | None = None reviews_cursor: str | None = None threads_cursor: str | None = None pr_meta: dict[str, Any] | None = None while True: payload = gh_api_graphql( owner=owner, repo=repo, number=number, comments_cursor=comments_cursor, reviews_cursor=reviews_cursor, threads_cursor=threads_cursor, ) if "errors" in payload and payload["errors"]: raise RuntimeError(f"GitHub GraphQL errors:\n{json.dumps(payload['errors'], indent=2)}") pr = payload["data"]["repository"]["pullRequest"] if pr_meta is None: pr_meta = { "number": pr["number"], "url": pr["url"], "title": pr["title"], "state": pr["state"], "owner": owner, "repo": repo, } c = pr["comments"] r = pr["reviews"] t = pr["reviewThreads"] conversation_comments.extend(c.get("nodes") or []) reviews.extend(r.get("nodes") or []) review_threads.extend(t.get("nodes") or []) comments_cursor = c["pageInfo"]["endCursor"] if c["pageInfo"]["hasNextPage"] else None reviews_cursor = r["pageInfo"]["endCursor"] if r["pageInfo"]["hasNextPage"] else None threads_cursor = t["pageInfo"]["endCursor"] if t["pageInfo"]["hasNextPage"] else None if not (comments_cursor or reviews_cursor or threads_cursor): break assert pr_meta is not None return { "pull_request": pr_meta, "conversation_comments": conversation_comments, "reviews": reviews, "review_threads": review_threads, } def main() -> None: _ensure_gh_authenticated() owner, repo, number = get_current_pr_ref() result = fetch_all(owner, repo, number) print(json.dumps(result, indent=2)) if __name__ == "__main__": main() ================================================ FILE: skills/.curated/gh-fix-ci/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don\'t include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/gh-fix-ci/SKILL.md ================================================ --- name: "gh-fix-ci" description: "Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions; use `gh` to inspect checks and logs, summarize failure context, draft a fix plan, and implement only after explicit approval. Treat external providers (for example Buildkite) as out of scope and report only the details URL." --- # Gh Pr Checks Plan Fix ## Overview Use gh to locate failing PR checks, fetch GitHub Actions logs for actionable failures, summarize the failure snippet, then propose a fix plan and implement after explicit approval. - If a plan-oriented skill (for example `create-plan`) is available, use it; otherwise draft a concise plan inline and request approval before implementing. Prereq: authenticate with the standard GitHub CLI once (for example, run `gh auth login`), then confirm with `gh auth status` (repo + workflow scopes are typically required). ## Inputs - `repo`: path inside the repo (default `.`) - `pr`: PR number or URL (optional; defaults to current branch PR) - `gh` authentication for the repo host ## Quick start - `python "/scripts/inspect_pr_checks.py" --repo "." --pr ""` - Add `--json` if you want machine-friendly output for summarization. ## Workflow 1. Verify gh authentication. - Run `gh auth status` in the repo. - If unauthenticated, ask the user to run `gh auth login` (ensuring repo + workflow scopes) before proceeding. 2. Resolve the PR. - Prefer the current branch PR: `gh pr view --json number,url`. - If the user provides a PR number or URL, use that directly. 3. Inspect failing checks (GitHub Actions only). - Preferred: run the bundled script (handles gh field drift and job-log fallbacks): - `python "/scripts/inspect_pr_checks.py" --repo "." --pr ""` - Add `--json` for machine-friendly output. - Manual fallback: - `gh pr checks --json name,state,bucket,link,startedAt,completedAt,workflow` - If a field is rejected, rerun with the available fields reported by `gh`. - For each failing check, extract the run id from `detailsUrl` and run: - `gh run view --json name,workflowName,conclusion,status,url,event,headBranch,headSha` - `gh run view --log` - If the run log says it is still in progress, fetch job logs directly: - `gh api "/repos///actions/jobs//logs" > ""` 4. Scope non-GitHub Actions checks. - If `detailsUrl` is not a GitHub Actions run, label it as external and only report the URL. - Do not attempt Buildkite or other providers; keep the workflow lean. 5. Summarize failures for the user. - Provide the failing check name, run URL (if any), and a concise log snippet. - Call out missing logs explicitly. 6. Create a plan. - Use the `create-plan` skill to draft a concise plan and request approval. 7. Implement after approval. - Apply the approved plan, summarize diffs/tests, and ask about opening a PR. 8. Recheck status. - After changes, suggest re-running the relevant tests and `gh pr checks` to confirm. ## Bundled Resources ### scripts/inspect_pr_checks.py Fetch failing PR checks, pull GitHub Actions logs, and extract a failure snippet. Exits non-zero when failures remain so it can be used in automation. Usage examples: - `python "/scripts/inspect_pr_checks.py" --repo "." --pr "123"` - `python "/scripts/inspect_pr_checks.py" --repo "." --pr "https://github.com/org/repo/pull/123" --json` - `python "/scripts/inspect_pr_checks.py" --repo "." --max-lines 200 --context 40` ================================================ FILE: skills/.curated/gh-fix-ci/agents/openai.yaml ================================================ interface: display_name: "GitHub Fix CI" short_description: "Debug failing GitHub Actions CI" icon_small: "./assets/github-small.svg" icon_large: "./assets/github.png" default_prompt: "Inspect failing GitHub Actions checks in this repo, summarize root cause, and propose a focused fix plan." ================================================ FILE: skills/.curated/gh-fix-ci/scripts/inspect_pr_checks.py ================================================ #!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import subprocess import sys from pathlib import Path from shutil import which from typing import Any, Iterable, Sequence FAILURE_CONCLUSIONS = { "failure", "cancelled", "timed_out", "action_required", } FAILURE_STATES = { "failure", "error", "cancelled", "timed_out", "action_required", } FAILURE_BUCKETS = {"fail"} FAILURE_MARKERS = ( "error", "fail", "failed", "traceback", "exception", "assert", "panic", "fatal", "timeout", "segmentation fault", ) DEFAULT_MAX_LINES = 160 DEFAULT_CONTEXT_LINES = 30 PENDING_LOG_MARKERS = ( "still in progress", "log will be available when it is complete", ) class GhResult: def __init__(self, returncode: int, stdout: str, stderr: str): self.returncode = returncode self.stdout = stdout self.stderr = stderr def run_gh_command(args: Sequence[str], cwd: Path) -> GhResult: process = subprocess.run( ["gh", *args], cwd=cwd, text=True, capture_output=True, ) return GhResult(process.returncode, process.stdout, process.stderr) def run_gh_command_raw(args: Sequence[str], cwd: Path) -> tuple[int, bytes, str]: process = subprocess.run( ["gh", *args], cwd=cwd, capture_output=True, ) stderr = process.stderr.decode(errors="replace") return process.returncode, process.stdout, stderr def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description=( "Inspect failing GitHub PR checks, fetch GitHub Actions logs, and extract a " "failure snippet." ), formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("--repo", default=".", help="Path inside the target Git repository.") parser.add_argument( "--pr", default=None, help="PR number or URL (defaults to current branch PR)." ) parser.add_argument("--max-lines", type=int, default=DEFAULT_MAX_LINES) parser.add_argument("--context", type=int, default=DEFAULT_CONTEXT_LINES) parser.add_argument("--json", action="store_true", help="Emit JSON instead of text output.") return parser.parse_args() def main() -> int: args = parse_args() repo_root = find_git_root(Path(args.repo)) if repo_root is None: print("Error: not inside a Git repository.", file=sys.stderr) return 1 if not ensure_gh_available(repo_root): return 1 pr_value = resolve_pr(args.pr, repo_root) if pr_value is None: return 1 checks = fetch_checks(pr_value, repo_root) if checks is None: return 1 failing = [c for c in checks if is_failing(c)] if not failing: print(f"PR #{pr_value}: no failing checks detected.") return 0 results = [] for check in failing: results.append( analyze_check( check, repo_root=repo_root, max_lines=max(1, args.max_lines), context=max(1, args.context), ) ) if args.json: print(json.dumps({"pr": pr_value, "results": results}, indent=2)) else: render_results(pr_value, results) return 1 def find_git_root(start: Path) -> Path | None: result = subprocess.run( ["git", "rev-parse", "--show-toplevel"], cwd=start, text=True, capture_output=True, ) if result.returncode != 0: return None return Path(result.stdout.strip()) def ensure_gh_available(repo_root: Path) -> bool: if which("gh") is None: print("Error: gh is not installed or not on PATH.", file=sys.stderr) return False result = run_gh_command(["auth", "status"], cwd=repo_root) if result.returncode == 0: return True message = (result.stderr or result.stdout or "").strip() print(message or "Error: gh not authenticated.", file=sys.stderr) return False def resolve_pr(pr_value: str | None, repo_root: Path) -> str | None: if pr_value: return pr_value result = run_gh_command(["pr", "view", "--json", "number"], cwd=repo_root) if result.returncode != 0: message = (result.stderr or result.stdout or "").strip() print(message or "Error: unable to resolve PR.", file=sys.stderr) return None try: data = json.loads(result.stdout or "{}") except json.JSONDecodeError: print("Error: unable to parse PR JSON.", file=sys.stderr) return None number = data.get("number") if not number: print("Error: no PR number found.", file=sys.stderr) return None return str(number) def fetch_checks(pr_value: str, repo_root: Path) -> list[dict[str, Any]] | None: primary_fields = ["name", "state", "conclusion", "detailsUrl", "startedAt", "completedAt"] result = run_gh_command( ["pr", "checks", pr_value, "--json", ",".join(primary_fields)], cwd=repo_root, ) if result.returncode != 0: message = "\n".join(filter(None, [result.stderr, result.stdout])).strip() available_fields = parse_available_fields(message) if available_fields: fallback_fields = [ "name", "state", "bucket", "link", "startedAt", "completedAt", "workflow", ] selected_fields = [field for field in fallback_fields if field in available_fields] if not selected_fields: print("Error: no usable fields available for gh pr checks.", file=sys.stderr) return None result = run_gh_command( ["pr", "checks", pr_value, "--json", ",".join(selected_fields)], cwd=repo_root, ) if result.returncode != 0: message = (result.stderr or result.stdout or "").strip() print(message or "Error: gh pr checks failed.", file=sys.stderr) return None else: print(message or "Error: gh pr checks failed.", file=sys.stderr) return None try: data = json.loads(result.stdout or "[]") except json.JSONDecodeError: print("Error: unable to parse checks JSON.", file=sys.stderr) return None if not isinstance(data, list): print("Error: unexpected checks JSON shape.", file=sys.stderr) return None return data def is_failing(check: dict[str, Any]) -> bool: conclusion = normalize_field(check.get("conclusion")) if conclusion in FAILURE_CONCLUSIONS: return True state = normalize_field(check.get("state") or check.get("status")) if state in FAILURE_STATES: return True bucket = normalize_field(check.get("bucket")) return bucket in FAILURE_BUCKETS def analyze_check( check: dict[str, Any], repo_root: Path, max_lines: int, context: int, ) -> dict[str, Any]: url = check.get("detailsUrl") or check.get("link") or "" run_id = extract_run_id(url) job_id = extract_job_id(url) base: dict[str, Any] = { "name": check.get("name", ""), "detailsUrl": url, "runId": run_id, "jobId": job_id, } if run_id is None: base["status"] = "external" base["note"] = "No GitHub Actions run id detected in detailsUrl." return base metadata = fetch_run_metadata(run_id, repo_root) log_text, log_error, log_status = fetch_check_log( run_id=run_id, job_id=job_id, repo_root=repo_root, ) if log_status == "pending": base["status"] = "log_pending" base["note"] = log_error or "Logs are not available yet." if metadata: base["run"] = metadata return base if log_error: base["status"] = "log_unavailable" base["error"] = log_error if metadata: base["run"] = metadata return base snippet = extract_failure_snippet(log_text, max_lines=max_lines, context=context) base["status"] = "ok" base["run"] = metadata or {} base["logSnippet"] = snippet base["logTail"] = tail_lines(log_text, max_lines) return base def extract_run_id(url: str) -> str | None: if not url: return None for pattern in (r"/actions/runs/(\d+)", r"/runs/(\d+)"): match = re.search(pattern, url) if match: return match.group(1) return None def extract_job_id(url: str) -> str | None: if not url: return None match = re.search(r"/actions/runs/\d+/job/(\d+)", url) if match: return match.group(1) match = re.search(r"/job/(\d+)", url) if match: return match.group(1) return None def fetch_run_metadata(run_id: str, repo_root: Path) -> dict[str, Any] | None: fields = [ "conclusion", "status", "workflowName", "name", "event", "headBranch", "headSha", "url", ] result = run_gh_command(["run", "view", run_id, "--json", ",".join(fields)], cwd=repo_root) if result.returncode != 0: return None try: data = json.loads(result.stdout or "{}") except json.JSONDecodeError: return None if not isinstance(data, dict): return None return data def fetch_check_log( run_id: str, job_id: str | None, repo_root: Path, ) -> tuple[str, str, str]: log_text, log_error = fetch_run_log(run_id, repo_root) if not log_error: return log_text, "", "ok" if is_log_pending_message(log_error) and job_id: job_log, job_error = fetch_job_log(job_id, repo_root) if job_log: return job_log, "", "ok" if job_error and is_log_pending_message(job_error): return "", job_error, "pending" if job_error: return "", job_error, "error" return "", log_error, "pending" if is_log_pending_message(log_error): return "", log_error, "pending" return "", log_error, "error" def fetch_run_log(run_id: str, repo_root: Path) -> tuple[str, str]: result = run_gh_command(["run", "view", run_id, "--log"], cwd=repo_root) if result.returncode != 0: error = (result.stderr or result.stdout or "").strip() return "", error or "gh run view failed" return result.stdout, "" def fetch_job_log(job_id: str, repo_root: Path) -> tuple[str, str]: repo_slug = fetch_repo_slug(repo_root) if not repo_slug: return "", "Error: unable to resolve repository name for job logs." endpoint = f"/repos/{repo_slug}/actions/jobs/{job_id}/logs" returncode, stdout_bytes, stderr = run_gh_command_raw(["api", endpoint], cwd=repo_root) if returncode != 0: message = (stderr or stdout_bytes.decode(errors="replace")).strip() return "", message or "gh api job logs failed" if is_zip_payload(stdout_bytes): return "", "Job logs returned a zip archive; unable to parse." return stdout_bytes.decode(errors="replace"), "" def fetch_repo_slug(repo_root: Path) -> str | None: result = run_gh_command(["repo", "view", "--json", "nameWithOwner"], cwd=repo_root) if result.returncode != 0: return None try: data = json.loads(result.stdout or "{}") except json.JSONDecodeError: return None name_with_owner = data.get("nameWithOwner") if not name_with_owner: return None return str(name_with_owner) def normalize_field(value: Any) -> str: if value is None: return "" return str(value).strip().lower() def parse_available_fields(message: str) -> list[str]: if "Available fields:" not in message: return [] fields: list[str] = [] collecting = False for line in message.splitlines(): if "Available fields:" in line: collecting = True continue if not collecting: continue field = line.strip() if not field: continue fields.append(field) return fields def is_log_pending_message(message: str) -> bool: lowered = message.lower() return any(marker in lowered for marker in PENDING_LOG_MARKERS) def is_zip_payload(payload: bytes) -> bool: return payload.startswith(b"PK") def extract_failure_snippet(log_text: str, max_lines: int, context: int) -> str: lines = log_text.splitlines() if not lines: return "" marker_index = find_failure_index(lines) if marker_index is None: return "\n".join(lines[-max_lines:]) start = max(0, marker_index - context) end = min(len(lines), marker_index + context) window = lines[start:end] if len(window) > max_lines: window = window[-max_lines:] return "\n".join(window) def find_failure_index(lines: Sequence[str]) -> int | None: for idx in range(len(lines) - 1, -1, -1): lowered = lines[idx].lower() if any(marker in lowered for marker in FAILURE_MARKERS): return idx return None def tail_lines(text: str, max_lines: int) -> str: if max_lines <= 0: return "" lines = text.splitlines() return "\n".join(lines[-max_lines:]) def render_results(pr_number: str, results: Iterable[dict[str, Any]]) -> None: results_list = list(results) print(f"PR #{pr_number}: {len(results_list)} failing checks analyzed.") for result in results_list: print("-" * 60) print(f"Check: {result.get('name', '')}") if result.get("detailsUrl"): print(f"Details: {result['detailsUrl']}") run_id = result.get("runId") if run_id: print(f"Run ID: {run_id}") job_id = result.get("jobId") if job_id: print(f"Job ID: {job_id}") status = result.get("status", "unknown") print(f"Status: {status}") run_meta = result.get("run", {}) if run_meta: branch = run_meta.get("headBranch", "") sha = (run_meta.get("headSha") or "")[:12] workflow = run_meta.get("workflowName") or run_meta.get("name") or "" conclusion = run_meta.get("conclusion") or run_meta.get("status") or "" print(f"Workflow: {workflow} ({conclusion})") if branch or sha: print(f"Branch/SHA: {branch} {sha}") if run_meta.get("url"): print(f"Run URL: {run_meta['url']}") if result.get("note"): print(f"Note: {result['note']}") if result.get("error"): print(f"Error fetching logs: {result['error']}") continue snippet = result.get("logSnippet") or "" if snippet: print("Failure snippet:") print(indent_block(snippet, prefix=" ")) else: print("No snippet available.") print("-" * 60) def indent_block(text: str, prefix: str = " ") -> str: return "\n".join(f"{prefix}{line}" for line in text.splitlines()) if __name__ == "__main__": raise SystemExit(main()) ================================================ FILE: skills/.curated/imagegen/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don\'t include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: skills/.curated/imagegen/SKILL.md ================================================ --- name: "imagegen" description: "Use when the user asks to generate or edit images via the OpenAI Image API (for example: generate image, edit/inpaint/mask, background removal or replacement, transparent background, product shots, concept art, covers, or batch variants); run the bundled CLI (`scripts/image_gen.py`) and require `OPENAI_API_KEY` for live calls." --- # Image Generation Skill Generates or edits images for the current project (e.g., website assets, game assets, UI mockups, product mockups, wireframes, logo design, photorealistic images, infographics). Defaults to `gpt-image-1.5` and the OpenAI Image API, and prefers the bundled CLI for deterministic, reproducible runs. ## When to use - Generate a new image (concept art, product shot, cover, website hero) - Edit an existing image (inpainting, masked edits, lighting or weather transformations, background replacement, object removal, compositing, transparent background) - Batch runs (many prompts, or many variants across prompts) ## Decision tree (generate vs edit vs batch) - If the user provides an input image (or says “edit/retouch/inpaint/mask/translate/localize/change only X”) → **edit** - Else if the user needs many different prompts/assets → **generate-batch** - Else → **generate** ## Workflow 1. Decide intent: generate vs edit vs batch (see decision tree above). 2. Collect inputs up front: prompt(s), exact text (verbatim), constraints/avoid list, and any input image(s)/mask(s). For multi-image edits, label each input by index and role; for edits, list invariants explicitly. 3. If batch: write a temporary JSONL under tmp/ (one job per line), run once, then delete the JSONL. 4. Augment prompt into a short labeled spec (structure + constraints) without inventing new creative requirements. 5. Run the bundled CLI (`scripts/image_gen.py`) with sensible defaults (see references/cli.md). 6. For complex edits/generations, inspect outputs (open/view images) and validate: subject, style, composition, text accuracy, and invariants/avoid items. 7. Iterate: make a single targeted change (prompt or mask), re-run, re-check. 8. Save/return final outputs and note the final prompt + flags used. ## Temp and output conventions - Use `tmp/imagegen/` for intermediate files (for example JSONL batches); delete when done. - Write final artifacts under `output/imagegen/` when working in this repo. - Use `--out` or `--out-dir` to control output paths; keep filenames stable and descriptive. ## Dependencies (install if missing) Prefer `uv` for dependency management. Python packages: ``` uv pip install openai pillow ``` If `uv` is unavailable: ``` python3 -m pip install openai pillow ``` ## Environment - `OPENAI_API_KEY` must be set for live API calls. If the key is missing, give the user these steps: 1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys 2. Set `OPENAI_API_KEY` as an environment variable in their system. 3. Offer to guide them through setting the environment variable for their OS/shell if needed. - Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready. If installation isn't possible in this environment, tell the user which dependency is missing and how to install it locally. ## Defaults & rules - Use `gpt-image-1.5` unless the user explicitly asks for `gpt-image-1-mini` or explicitly prefers a cheaper/faster model. - Assume the user wants a new image unless they explicitly ask for an edit. - Require `OPENAI_API_KEY` before any live API call. - Use the OpenAI Python SDK (`openai` package) for all API calls; do not use raw HTTP. - If the user requests edits, use `client.images.edit(...)` and include input images (and mask if provided). - Prefer the bundled CLI (`scripts/image_gen.py`) over writing new one-off scripts. - Never modify `scripts/image_gen.py`. If something is missing, ask the user before doing anything else. - If the result isn’t clearly relevant or doesn’t satisfy constraints, iterate with small targeted prompt changes; only ask a question if a missing detail blocks success. ## Prompt augmentation Reformat user prompts into a structured, production-oriented spec. Only make implicit details explicit; do not invent new requirements. ## Use-case taxonomy (exact slugs) Classify each request into one of these buckets and keep the slug consistent across prompts and references. Generate: - photorealistic-natural — candid/editorial lifestyle scenes with real texture and natural lighting. - product-mockup — product/packaging shots, catalog imagery, merch concepts. - ui-mockup — app/web interface mockups that look shippable. - infographic-diagram — diagrams/infographics with structured layout and text. - logo-brand — logo/mark exploration, vector-friendly. - illustration-story — comics, children’s book art, narrative scenes. - stylized-concept — style-driven concept art, 3D/stylized renders. - historical-scene — period-accurate/world-knowledge scenes. Edit: - text-localization — translate/replace in-image text, preserve layout. - identity-preserve — try-on, person-in-scene; lock face/body/pose. - precise-object-edit — remove/replace a specific element (incl. interior swaps). - lighting-weather — time-of-day/season/atmosphere changes only. - background-extraction — transparent background / clean cutout. - style-transfer — apply reference style while changing subject/scene. - compositing — multi-image insert/merge with matched lighting/perspective. - sketch-to-render — drawing/line art to photoreal render. Quick clarification (augmentation vs invention): - If the user says “a hero image for a landing page”, you may add *layout/composition constraints* that are implied by that use (e.g., “generous negative space on the right for headline text”). - Do not introduce new creative elements the user didn’t ask for (e.g., adding a mascot, changing the subject, inventing brand names/logos). Template (include only relevant lines): ``` Use case: Asset type: Primary request: Scene/background: Subject:
Style/medium: Composition/framing: Lighting/mood: Color palette: Materials/textures: Quality: Input fidelity (edits): Text (verbatim): "" Constraints: Avoid: ``` Augmentation rules: - Keep it short; add only details the user already implied or provided elsewhere. - Always classify the request into a taxonomy slug above and tailor constraints/composition/quality to that bucket. Use the slug to find the matching example in `references/sample-prompts.md`. - If the user gives a broad request (e.g., "Generate images for this website"), use judgment to propose tasteful, context-appropriate assets and map each to a taxonomy slug. - For edits, explicitly list invariants ("change only X; keep Y unchanged"). - If any critical detail is missing and blocks success, ask a question; otherwise proceed. ## Examples ### Generation example (hero image) ``` Use case: stylized-concept Asset type: landing page hero Primary request: a minimal hero image of a ceramic coffee mug Style/medium: clean product photography Composition/framing: centered product, generous negative space on the right Lighting/mood: soft studio lighting Constraints: no logos, no text, no watermark ``` ### Edit example (invariants) ``` Use case: precise-object-edit Asset type: product photo background replacement Primary request: replace the background with a warm sunset gradient Constraints: change only the background; keep the product and its edges unchanged; no text; no watermark ``` ## Prompting best practices (short list) - Structure prompt as scene -> subject -> details -> constraints. - Include intended use (ad, UI mock, infographic) to set the mode and polish level. - Use camera/composition language for photorealism. - Quote exact text and specify typography + placement. - For tricky words, spell them letter-by-letter and require verbatim rendering. - For multi-image inputs, reference images by index and describe how to combine them. - For edits, repeat invariants every iteration to reduce drift. - Iterate with single-change follow-ups. - For latency-sensitive runs, start with quality=low; use quality=high for text-heavy or detail-critical outputs. - For strict edits (identity/layout lock), consider input_fidelity=high. - If results feel “tacky”, add a brief “Avoid:” line (stock-photo vibe; cheesy lens flare; oversaturated neon; harsh bloom; oversharpening; clutter) and specify restraint (“editorial”, “premium”, “subtle”). More principles: `references/prompting.md`. Copy/paste specs: `references/sample-prompts.md`. ## Guidance by asset type Asset-type templates (website assets, game assets, wireframes, logo) are consolidated in `references/sample-prompts.md`. ## CLI + environment notes - CLI commands + examples: `references/cli.md` - API parameter quick reference: `references/image-api.md` - If network approvals / sandbox settings are getting in the way: `references/codex-network.md` ## Reference map - **`references/cli.md`**: how to *run* image generation/edits/batches via `scripts/image_gen.py` (commands, flags, recipes). - **`references/image-api.md`**: what knobs exist at the API level (parameters, sizes, quality, background, edit-only fields). - **`references/prompting.md`**: prompting principles (structure, constraints/invariants, iteration patterns). - **`references/sample-prompts.md`**: copy/paste prompt recipes (generate + edit workflows; examples only). - **`references/codex-network.md`**: environment/sandbox/network-approval troubleshooting. ================================================ FILE: skills/.curated/imagegen/agents/openai.yaml ================================================ interface: display_name: "Image Gen" short_description: "Generate and edit images using OpenAI" icon_small: "./assets/imagegen-small.svg" icon_large: "./assets/imagegen.png" default_prompt: "Generate or edit images for this task and return the final prompt plus selected outputs." ================================================ FILE: skills/.curated/imagegen/references/cli.md ================================================ # CLI reference (`scripts/image_gen.py`) This file contains the “command catalog” for the bundled image generation CLI. Keep `SKILL.md` as overview-first; put verbose CLI details here. ## What this CLI does - `generate`: generate new images from a prompt - `edit`: edit an existing image (optionally with a mask) — inpainting / background replacement / “change only X” - `generate-batch`: run many jobs from a JSONL file (one job per line) Real API calls require **network access** + `OPENAI_API_KEY`. `--dry-run` does not. ## Quick start (works from any repo) Set a stable path to the skill CLI (default `CODEX_HOME` is `~/.codex`): ``` export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" export IMAGE_GEN="$CODEX_HOME/skills/imagegen/scripts/image_gen.py" ``` Dry-run (no API call; no network required; does not require the `openai` package): ``` python "$IMAGE_GEN" generate --prompt "Test" --dry-run ``` Generate (requires `OPENAI_API_KEY` + network): ``` uv run --with openai python "$IMAGE_GEN" generate --prompt "A cozy alpine cabin at dawn" --size 1024x1024 ``` No `uv` installed? Use your active Python env: ``` python "$IMAGE_GEN" generate --prompt "A cozy alpine cabin at dawn" --size 1024x1024 ``` ## Guardrails (important) - Use `python "$IMAGE_GEN" ...` (or equivalent full path) for generations/edits/batch work. - Do **not** create one-off runners (e.g. `gen_images.py`) unless the user explicitly asks for a custom wrapper. - **Never modify** `scripts/image_gen.py`. If something is missing, ask the user before doing anything else. ## Defaults (unless overridden by flags) - Model: `gpt-image-1.5` - Size: `1024x1024` - Quality: `auto` - Output format: `png` - Background: unspecified (API default). If you set `--background transparent`, also set `--output-format png` or `webp`. ## Quality + input fidelity - `--quality` works for `generate`, `edit`, and `generate-batch`: `low|medium|high|auto`. - `--input-fidelity` is **edit-only**: `low|high` (use `high` for strict edits like identity or layout lock). Example: ``` python "$IMAGE_GEN" edit --image input.png --prompt "Change only the background" --quality high --input-fidelity high ``` ## Masks (edits) - Use a **PNG** mask; an alpha channel is strongly recommended. - The mask should match the input image dimensions. - In the edit prompt, repeat invariants (e.g., “change only the background; keep the subject unchanged”) to reduce drift. ## Optional deps Prefer `uv run --with ...` for an out-of-the-box run without changing the current project env; otherwise install into your active env: ``` uv pip install openai ``` ## Common recipes Generate + also write a downscaled copy for fast web loading: ``` uv run --with openai --with pillow python "$IMAGE_GEN" generate \ --prompt "A cozy alpine cabin at dawn" \ --size 1024x1024 \ --downscale-max-dim 1024 ``` Notes: - Downscaling writes an extra file next to the original (default suffix `-web`, e.g. `output-web.png`). - Downscaling requires Pillow (use `uv run --with pillow ...` or install it into your env). Generate with augmentation fields: ``` python "$IMAGE_GEN" generate \ --prompt "A minimal hero image of a ceramic coffee mug" \ --use-case "landing page hero" \ --style "clean product photography" \ --composition "centered product, generous negative space" \ --constraints "no logos, no text" ``` Generate multiple prompts concurrently (async batch): ``` mkdir -p tmp/imagegen cat > tmp/imagegen/prompts.jsonl << 'EOF' {"prompt":"Cavernous hangar interior with a compact shuttle parked center-left, open bay door","use_case":"game concept art environment","composition":"wide-angle, low-angle, cinematic framing","lighting":"volumetric light rays through drifting fog","constraints":"no logos or trademarks; no watermark","size":"1536x1024"} {"prompt":"Gray wolf in profile in a snowy forest, crisp fur texture","use_case":"wildlife photography print","composition":"100mm, eye-level, shallow depth of field","constraints":"no logos or trademarks; no watermark","size":"1024x1024"} EOF python "$IMAGE_GEN" generate-batch --input tmp/imagegen/prompts.jsonl --out-dir out --concurrency 5 # Cleanup (recommended) rm -f tmp/imagegen/prompts.jsonl ``` Notes: - Use `--concurrency` to control parallelism (default `5`). Higher concurrency can hit rate limits; the CLI retries on transient errors. - Per-job overrides are supported in JSONL (e.g., `size`, `quality`, `background`, `output_format`, `n`, and prompt-augmentation fields). - `--n` generates multiple variants for a single prompt; `generate-batch` is for many different prompts. - Treat the JSONL file as temporary: write it under `tmp/` and delete it after the run (don’t commit it). Edit: ``` python "$IMAGE_GEN" edit --image input.png --mask mask.png --prompt "Replace the background with a warm sunset" ``` ## CLI notes - Supported sizes: `1024x1024`, `1536x1024`, `1024x1536`, or `auto`. - Transparent backgrounds require `output_format` to be `png` or `webp`. - Default output is `output.png`; multiple images become `output-1.png`, `output-2.png`, etc. - Use `--no-augment` to skip prompt augmentation. ## See also - API parameter quick reference: `references/image-api.md` - Prompt examples: `references/sample-prompts.md` ================================================ FILE: skills/.curated/imagegen/references/codex-network.md ================================================ # Codex network approvals / sandbox notes This guidance is intentionally isolated from `SKILL.md` because it can vary by environment and may become stale. Prefer the defaults in your environment when in doubt. ## Why am I asked to approve every image generation call? Image generation uses the OpenAI Image API, so the CLI needs outbound network access. In many Codex setups, network access is disabled by default (especially under stricter sandbox modes), and/or the approval policy may require confirmation before networked commands run. ## How do I reduce repeated approval prompts (network)? If you trust the repo and want fewer prompts, enable network access for the relevant sandbox mode and relax the approval policy. Example `~/.codex/config.toml` pattern: ``` approval_policy = "never" sandbox_mode = "workspace-write" [sandbox_workspace_write] network_access = true ``` Or for a single session: ``` codex --sandbox workspace-write --ask-for-approval never ``` ## Safety note Use caution: enabling network and disabling approvals reduces friction but increases risk if you run untrusted code or work in an untrusted repository. ================================================ FILE: skills/.curated/imagegen/references/image-api.md ================================================ # Image API quick reference ## Endpoints - Generate: `POST /v1/images/generations` (`client.images.generate(...)`) - Edit: `POST /v1/images/edits` (`client.images.edit(...)`) ## Models - Default: `gpt-image-1.5` - Alternatives: `gpt-image-1-mini` (for faster, lower-cost generation) ## Core parameters (generate + edit) - `prompt`: text prompt - `model`: image model - `n`: number of images (1-10) - `size`: `1024x1024`, `1536x1024`, `1024x1536`, or `auto` - `quality`: `low`, `medium`, `high`, or `auto` - `background`: `transparent`, `opaque`, or `auto` (transparent requires `png`/`webp`) - `output_format`: `png` (default), `jpeg`, `webp` - `output_compression`: 0-100 (jpeg/webp only) - `moderation`: `auto` (default) or `low` ## Edit-specific parameters - `image`: one or more input images (first image is primary) - `mask`: optional mask image (same size, alpha channel required) - `input_fidelity`: `low` (default) or `high` (support varies by model) - set it to `high` if the user needs a very specific edit and you can't achieve it with the default `low` fidelity. ## Output - `data[]` list with `b64_json` per image ## Limits & notes - Input images and masks must be under 50MB. - Use edits endpoint when the user requests changes to an existing image. - Masking is prompt-guided; exact shapes are not guaranteed. - Large sizes and high quality increase latency and cost. - For fast iteration or latency-sensitive runs, start with `quality=low`; raise to `high` for text-heavy or detail-critical outputs. - Use `input_fidelity=high` for strict edits (identity preservation, layout lock, or precise compositing). ================================================ FILE: skills/.curated/imagegen/references/prompting.md ================================================ # Prompting best practices (gpt-image-1.5) ## Contents - [Structure](#structure) - [Specificity](#specificity) - [Avoiding “tacky” outputs](#avoiding-tacky-outputs) - [Composition & layout](#composition--layout) - [Constraints & invariants](#constraints--invariants) - [Text in images](#text-in-images) - [Multi-image inputs](#multi-image-inputs) - [Iterate deliberately](#iterate-deliberately) - [Quality vs latency](#quality-vs-latency) - [Use-case tips](#use-case-tips) - [Where to find copy/paste recipes](#where-to-find-copypaste-recipes) ## Structure - Use a consistent order: scene/background -> subject -> key details -> constraints -> output intent. - Include intended use (ad, UI mock, infographic) to set the mode and polish level. - For complex requests, use short labeled lines instead of a long paragraph. ## Specificity - Name materials, textures, and visual medium (photo, watercolor, 3D render). - For photorealism, include camera/composition language (lens, framing, lighting). - Add targeted quality cues only when needed (film grain, textured brushstrokes, macro detail); avoid generic "8K" style prompts. ## Avoiding “tacky” outputs - Don’t use vibe-only buzzwords (“epic”, “cinematic”, “trending”, “8k”, “award-winning”, “unreal engine”, “artstation”) unless the user explicitly wants that look. - Specify restraint: “minimal”, “editorial”, “premium”, “subtle”, “natural color grading”, “soft contrast”, “no harsh bloom”, “no oversharpening”. - For 3D/illustration, name the finish you want: “matte”, “paper grain”, “ink texture”, “flat color with soft shadow”; avoid “glossy plastic” unless requested. - Add a short negative line when needed (especially for marketing art): “Avoid: stock-photo vibe; cheesy lens flare; oversaturated neon; excessive bokeh; fake-looking smiles; clutter”. ## Composition & layout - Specify framing and viewpoint (close-up, wide, top-down) and placement ("logo top-right"). - Call out negative space if you need room for UI or overlays. ## Constraints & invariants - State what must not change ("keep background unchanged"). - For edits, say "change only X; keep Y unchanged" and repeat invariants on every iteration to reduce drift. ## Text in images - Put literal text in quotes or ALL CAPS and specify typography (font style, size, color, placement). - Spell uncommon words letter-by-letter if accuracy matters. - For in-image copy, require verbatim rendering and no extra characters. ## Multi-image inputs - Reference inputs by index and role ("Image 1: product, Image 2: style"). - Describe how to combine them ("apply Image 2's style to Image 1"). - For compositing, specify what moves where and what must remain unchanged. ## Iterate deliberately - Start with a clean base prompt, then make small single-change edits. - Re-specify critical constraints when you iterate. ## Quality vs latency - For latency-sensitive runs, start at `quality=low` and only raise it if needed. - Use `quality=high` for text-heavy or detail-critical images. - For strict edits (identity preservation, layout lock), consider `input_fidelity=high`. ## Use-case tips Generate: - photorealistic-natural: Prompt as if a real photo is captured in the moment; use photography language (lens, lighting, framing); call for real texture (pores, wrinkles, fabric wear, imperfections); avoid studio polish or staging; use `quality=high` when detail matters. - product-mockup: Describe the product/packaging and materials; ensure clean silhouette and label clarity; if in-image text is needed, require verbatim rendering and specify typography. - ui-mockup: Describe a real product; focus on layout, hierarchy, and common UI elements; avoid concept-art language so it looks shippable. - infographic-diagram: Define the audience and layout flow; label parts explicitly; require verbatim text; use `quality=high`. - logo-brand: Keep it simple and scalable; ask for a strong silhouette and balanced negative space; avoid gradients and fine detail. - illustration-story: Define panels or scene beats; keep each action concrete; for continuity, restate character traits and outfit each time. - stylized-concept: Specify style cues, material finish, and rendering approach (3D, painterly, clay); add a short "Avoid" line to prevent tacky effects. - historical-scene: State the location/date and required period accuracy; constrain clothing, props, and environment to match the era. Edit: - text-localization: Change only the text; preserve layout, typography, spacing, and hierarchy; no extra words or reflow unless needed. - identity-preserve: Lock identity (face, body, pose, hair, expression); change only the specified elements; match lighting and shadows; use `input_fidelity=high` if likeness drifts. - precise-object-edit: Specify exactly what to remove/replace; preserve surrounding texture and lighting; keep everything else unchanged. - lighting-weather: Change only environmental conditions (light, shadows, atmosphere, precipitation); keep geometry, framing, and subject identity. - background-extraction: Request transparent background; crisp silhouette; no halos; preserve label text exactly; optionally add a subtle contact shadow. - style-transfer: Specify style cues to preserve (palette, texture, brushwork) and what must change; add "no extra elements" to prevent drift. - compositing: Reference inputs by index; specify what moves where; match lighting, perspective, and scale; keep background and framing unchanged. - sketch-to-render: Preserve layout, proportions, and perspective; add plausible materials, lighting, and environment; "do not add new elements or text." ## Where to find copy/paste recipes For copy/paste prompt specs (examples only), see `references/sample-prompts.md`. This file focuses on principles, structure, and iteration patterns. ================================================ FILE: skills/.curated/imagegen/references/sample-prompts.md ================================================ # Sample prompts (copy/paste) Use these as starting points (recipes only). Keep user-provided requirements; do not invent new creative elements. For prompting principles (structure, invariants, iteration), see `references/prompting.md`. ## Generate ### photorealistic-natural ``` Use case: photorealistic-natural Primary request: candid photo of an elderly sailor on a small fishing boat adjusting a net Scene/background: coastal water with soft haze Subject: weathered skin with wrinkles and sun texture; a calm dog on deck nearby Style/medium: photorealistic candid photo Composition/framing: medium close-up, eye-level, 50mm lens Lighting/mood: soft coastal daylight, shallow depth of field, subtle film grain Materials/textures: real skin texture, worn fabric, salt-worn wood Constraints: natural color balance; no heavy retouching; no glamorization; no watermark Avoid: studio polish; staged look Quality: high ``` ### product-mockup ``` Use case: product-mockup Primary request: premium product photo of a matte black shampoo bottle with a minimal label Scene/background: clean studio gradient from light gray to white Subject: single bottle centered with subtle reflection Style/medium: premium product photography Composition/framing: centered, slight three-quarter angle, generous padding Lighting/mood: softbox lighting, clean highlights, controlled shadows Materials/textures: matte plastic, crisp label printing Constraints: no logos or trademarks; no watermark Quality: high ``` ### ui-mockup ``` Use case: ui-mockup Primary request: mobile app UI for a local farmers market with vendors and specials Scene/background: clean white background with subtle natural accents Subject: header, vendor list with small photos, "Today's specials" section, location and hours Style/medium: realistic product UI, not concept art Composition/framing: iPhone frame, balanced spacing and hierarchy Constraints: practical layout, clear typography, no logos or trademarks, no watermark ``` ### infographic-diagram ``` Use case: infographic-diagram Primary request: detailed infographic of an automatic coffee machine flow Scene/background: clean, light neutral background Subject: bean hopper -> grinder -> brew group -> boiler -> water tank -> drip tray Style/medium: clean vector-like infographic with clear callouts and arrows Composition/framing: vertical poster layout, top-to-bottom flow Text (verbatim): "Bean Hopper", "Grinder", "Brew Group", "Boiler", "Water Tank", "Drip Tray" Constraints: clear labels, strong contrast, no logos or trademarks, no watermark Quality: high ``` ### logo-brand ``` Use case: logo-brand Primary request: original logo for "Field & Flour", a local bakery Style/medium: vector logo mark; flat colors; minimal Composition/framing: single centered logo on plain background with padding Constraints: strong silhouette, balanced negative space; original design only; no gradients unless essential; no trademarks; no watermark ``` ### illustration-story ``` Use case: illustration-story Primary request: 4-panel comic about a pet left alone at home Scene/background: cozy living room across panels Subject: pet reacting to the owner leaving, then relaxing, then returning to a composed pose Style/medium: comic illustration with clear panels Composition/framing: 4 equal-sized vertical panels, readable actions per panel Constraints: no text; no logos or trademarks; no watermark ``` ### stylized-concept ``` Use case: stylized-concept Primary request: cavernous hangar interior with tall support beams and drifting fog Scene/background: industrial hangar interior, deep scale, light haze Subject: compact shuttle, parked center-left, bay door open Style/medium: cinematic concept art, industrial realism Composition/framing: wide-angle, low-angle, cinematic framing Lighting/mood: volumetric light rays cutting through fog Constraints: no logos or trademarks; no watermark ``` ### historical-scene ``` Use case: historical-scene Primary request: outdoor crowd scene in Bethel, New York on August 16, 1969 Scene/background: open field, temporary stages, period-accurate tents and signage Subject: crowd in period-accurate clothing, authentic staging and environment Style/medium: photorealistic photo Composition/framing: wide shot, eye-level Constraints: period-accurate details; no modern objects; no logos or trademarks; no watermark ``` ## Asset type templates (taxonomy-aligned) ### Website assets template ``` Use case: Asset type: Primary request: Scene/background: Subject:
Style/medium: Composition/framing: Lighting/mood: Color palette: Constraints: ``` ### Website assets example: minimal hero background ``` Use case: stylized-concept Asset type: landing page hero background Primary request: minimal abstract background with a soft gradient and subtle texture (calm, modern) Style/medium: matte illustration / soft-rendered abstract background (not glossy 3D) Composition/framing: wide composition; large negative space on the right for headline Lighting/mood: gentle studio glow Color palette: cool neutrals with a restrained blue accent Constraints: no text; no logos; no watermark ``` ### Website assets example: feature section illustration ``` Use case: stylized-concept Asset type: feature section illustration Primary request: simple abstract shapes suggesting connection and flow (tasteful, minimal) Scene/background: subtle light-gray backdrop with faint texture Style/medium: flat illustration; soft shadows; restrained contrast Composition/framing: centered cluster; open margins for UI Color palette: muted teal and slate, low contrast accents Constraints: no text; no logos; no watermark ``` ### Website assets example: blog header image ``` Use case: photorealistic-natural Asset type: blog header image Primary request: overhead desk scene with notebook, pen, and coffee cup Scene/background: warm wooden tabletop Style/medium: photorealistic photo Composition/framing: wide crop; subject placed left; right side left empty Lighting/mood: soft morning light Constraints: no text; no logos; no watermark ``` ### Game assets template ``` Use case: stylized-concept Asset type: Primary request: Scene/background: (if applicable) Subject:
Style/medium: ; Composition/framing: ; ; Lighting/mood: