diff --git a/plugins/cloudflare/.cursor/plugin.json b/plugins/cloudflare/.cursor/plugin.json new file mode 100644 index 0000000..e3a8c7b --- /dev/null +++ b/plugins/cloudflare/.cursor/plugin.json @@ -0,0 +1,18 @@ +{ + "name": "cloudflare", + "version": "1.0.0", + "description": "Cursor plugin for Cloudflare — Workers, Pages, R2, KV, D1, and edge computing", + "author": { "name": "Cursor", "email": "plugins@cursor.com" }, + "homepage": "https://github.com/cursor/service-plugin-generation", + "repository": "https://github.com/cursor/service-plugin-generation", + "license": "MIT", + "keywords": ["cloudflare", "workers", "edge", "cdn", "r2", "kv", "d1", "pages"], + "category": "infrastructure", + "tags": ["edge-computing", "cdn", "serverless"], + "agents": "./agents/", + "skills": "./skills/", + "rules": "./rules/", + "hooks": "./hooks/hooks.json", + "mcpServers": "./mcp.json", + "extensions": "./extensions/" +} diff --git a/plugins/cloudflare/CHANGELOG.md b/plugins/cloudflare/CHANGELOG.md new file mode 100644 index 0000000..58309be --- /dev/null +++ b/plugins/cloudflare/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to the Cloudflare plugin for Cursor will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-02-07 + +### Added + +- **Plugin manifest** (`.cursor/plugin.json`) — plugin metadata, entry points, and configuration +- **Rules** + - `cloudflare-workers.mdc` — best practices for Cloudflare Workers including Web Standard APIs, error handling, storage selection (KV, D1, R2, Durable Objects, Queues), CPU time limits, `waitUntil()`, CORS, environment bindings, and module Worker syntax + - `cloudflare-config.mdc` — configuration best practices for `wrangler.toml` and `wrangler.jsonc`, covering compatibility date, bindings (KV, R2, D1, Durable Objects, Queues), routes, environments, cron triggers, secrets management, Smart Placement, and observability +- **Agent** + - `cloudflare-worker-agent.md` — Worker development agent for creating, debugging, and optimizing Cloudflare Workers, with guidance on architecture patterns and storage selection +- **Skills** + - `create-worker/SKILL.md` — step-by-step guide for creating a new Cloudflare Worker from project setup through deployment with bindings, local development, testing, custom domains, and CI/CD + - `setup-d1-database/SKILL.md` — comprehensive guide for setting up a D1 database with migrations, schema design, parameterized queries, batch transactions, seeding, and production deployment +- **Hooks** (`hooks/hooks.json`) — automated build validation on pre-commit/pre-push, wrangler.toml change detection, and TypeScript type checking on source file changes +- **MCP Server** (`mcp.json`) — Cloudflare MCP server configuration for managing Workers, KV namespaces, R2 buckets, D1 databases, and Pages projects +- **Deploy Script** (`scripts/deploy-worker.sh`) — production-ready deployment script with TypeScript validation, D1 migration support, environment selection, health checks, live log tailing, and CI/CD compatibility +- **Documentation** — `README.md` with full plugin overview, directory structure, and usage guide +- **License** — MIT license (Cursor, 2026) diff --git a/plugins/cloudflare/LICENSE b/plugins/cloudflare/LICENSE new file mode 100644 index 0000000..ca2bba7 --- /dev/null +++ b/plugins/cloudflare/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Cursor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/cloudflare/README.md b/plugins/cloudflare/README.md new file mode 100644 index 0000000..28797fb --- /dev/null +++ b/plugins/cloudflare/README.md @@ -0,0 +1,158 @@ +# Cloudflare Plugin for Cursor + +A comprehensive Cursor plugin for building and deploying applications on the **Cloudflare Developer Platform**. Provides rules, agents, skills, and tooling for Workers, Pages, R2, KV, D1, Durable Objects, Queues, and edge computing. + +## Features + +- **Rules** — Contextual best-practice guidance for Cloudflare Workers and wrangler.toml configuration +- **Agent** — Worker development agent for creating, debugging, and optimizing Cloudflare Workers +- **Skills** — Step-by-step guides for creating Workers and setting up D1 databases +- **Hooks** — Automated build validation and type checking on file changes and pre-push +- **MCP Server** — Integration with the Cloudflare API for managing Workers, KV, R2, D1, and Pages +- **Deploy Script** — Production-ready deployment script with preflight checks, D1 migration support, and health checks + +## Directory Structure + +``` +plugins/cloudflare/ +├── .cursor/ +│ └── plugin.json # Plugin manifest +├── agents/ +│ └── cloudflare-worker-agent.md # Worker development agent +├── rules/ +│ ├── cloudflare-workers.mdc # Workers best practices +│ └── cloudflare-config.mdc # wrangler.toml configuration rules +├── skills/ +│ ├── create-worker/ +│ │ └── SKILL.md # Creating a new Worker +│ └── setup-d1-database/ +│ └── SKILL.md # Setting up D1 database +├── hooks/ +│ └── hooks.json # Git & file-change hooks +├── scripts/ +│ └── deploy-worker.sh # Deployment script +├── extensions/ # Reserved for future extensions +├── mcp.json # MCP server configuration +├── README.md # This file +├── CHANGELOG.md # Release history +└── LICENSE # MIT License +``` + +## Getting Started + +### Prerequisites + +- [Cursor](https://cursor.com/) editor +- [Node.js](https://nodejs.org/) 18+ +- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) (`npm i -g wrangler`) +- A [Cloudflare](https://dash.cloudflare.com/sign-up) account + +### Installation + +This plugin is included in the `service-plugin-generation` repository. To use it: + +1. Clone the repository or copy the `plugins/cloudflare/` directory into your project +2. The plugin's rules will automatically activate when you edit matching files: + - `src/**/*.ts`, `src/**/*.js`, `worker.*`, `functions/**/*` → Workers best practices + - `wrangler.toml`, `wrangler.jsonc` → configuration best practices +3. Access the Worker agent for interactive help with development and deployment + +### Environment Variables + +Set `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` for MCP server and CI/CD integration: + +```bash +# Local development +export CLOUDFLARE_API_TOKEN="your-api-token" +export CLOUDFLARE_ACCOUNT_ID="your-account-id" + +# CI/CD — set as secrets in your provider +# GitHub Actions: Settings → Secrets → CLOUDFLARE_API_TOKEN +``` + +## Rules + +### cloudflare-workers.mdc + +Activated when editing Worker source files. Covers: + +- Web Standard APIs (fetch, Request, Response, Headers, URL, crypto) +- Error handling with proper Response objects +- Choosing the right storage: KV, D1, R2, Durable Objects, Queues +- CPU time limits and performance optimization +- Background tasks with `waitUntil()` +- CORS configuration +- Environment bindings and typed `Env` interface +- Module Worker syntax (ES modules) +- Cache API and HTMLRewriter + +### cloudflare-config.mdc + +Activated when editing `wrangler.toml` or `wrangler.jsonc`. Covers: + +- Compatibility date and flags +- KV, R2, D1, Durable Object, and Queue bindings +- Routes and custom domains +- Environment-specific configuration (staging/production) +- Cron triggers +- Secrets management +- Smart Placement and observability +- Build configuration and static assets + +## Agent + +The **Cloudflare Worker Agent** helps with: + +- Creating new Workers with proper project structure and TypeScript types +- Debugging Worker errors using `wrangler tail` and log analysis +- Optimizing performance (CPU time, caching, Smart Placement) +- Choosing between Workers, Pages Functions, and Durable Objects +- Configuring bindings for KV, R2, D1, Durable Objects, and Queues +- Setting up D1 databases with migrations and query patterns +- Implementing real-time features with Durable Objects and WebSockets + +## Skills + +### Create a Worker + +Complete walkthrough for building a new Worker: + +1. Install Wrangler CLI and authenticate +2. Scaffold a new project +3. Configure `wrangler.toml` with bindings +4. Write Worker code with typed environment +5. Local development with `wrangler dev` +6. Write tests with Vitest +7. Deploy and set up custom domains +8. CI/CD with GitHub Actions + +### Set Up D1 Database + +Guide for Cloudflare D1 (serverless SQLite): + +1. Create a D1 database +2. Write and apply migrations +3. Design schemas with proper types and indexes +4. Query from Workers with parameterized statements +5. Use `.batch()` for transactions +6. Seed development data +7. Deploy migrations to production + +## Deploy Script + +The included `scripts/deploy-worker.sh` provides: + +```bash +./scripts/deploy-worker.sh # Deploy to default environment +./scripts/deploy-worker.sh --env production # Deploy to production +./scripts/deploy-worker.sh --env staging --migrate # Deploy with D1 migrations +./scripts/deploy-worker.sh --dry-run # Validate build only +./scripts/deploy-worker.sh --tail # Stream logs after deploy +./scripts/deploy-worker.sh --help # Show all options +``` + +Features: TypeScript validation, D1 migration support, post-deploy health checks, live log tailing, CI/CD compatibility. + +## License + +MIT — see [LICENSE](./LICENSE) for details. diff --git a/plugins/cloudflare/agents/cloudflare-worker-agent.md b/plugins/cloudflare/agents/cloudflare-worker-agent.md new file mode 100644 index 0000000..e00a049 --- /dev/null +++ b/plugins/cloudflare/agents/cloudflare-worker-agent.md @@ -0,0 +1,184 @@ +# Cloudflare Worker Agent + +## Identity + +You are the **Cloudflare Worker Agent**, an expert in building, debugging, and optimizing applications on the Cloudflare Developer Platform. You help developers ship performant edge-first applications using Workers, Pages, R2, KV, D1, Durable Objects, Queues, and other Cloudflare services. + +## Capabilities + +- Create new Workers with proper project structure, bindings, and TypeScript types +- Debug Worker errors using `wrangler tail`, `wrangler dev`, and log analysis +- Optimize Worker performance (CPU time, cold starts, caching strategies) +- Guide architecture decisions: Workers vs. Pages Functions vs. Durable Objects +- Configure `wrangler.toml` bindings for KV, R2, D1, Durable Objects, Queues, and Hyperdrive +- Set up D1 databases with migrations, schemas, and query patterns +- Implement real-time features with Durable Objects and WebSockets +- Configure routing, custom domains, and cron triggers +- Advise on Cloudflare Pages for full-stack applications + +## Workflow + +### 1. Understand the Project + +Before making recommendations, gather context: + +- **Application type**: API, full-stack app, static site with functions, real-time service, or scheduled job? +- **Storage needs**: Key-value (KV), relational (D1), object storage (R2), or stateful coordination (Durable Objects)? +- **Traffic pattern**: Read-heavy, write-heavy, bursty, or steady? +- **Existing configuration**: Check `wrangler.toml`, `package.json`, `tsconfig.json` +- **Deployment target**: `workers.dev` subdomain, custom domain, or Cloudflare Pages? + +### 2. Choose the Right Compute Primitive + +| Primitive | Best For | Key Characteristics | +|---|---|---| +| **Workers** | Stateless HTTP handlers, API endpoints, middleware, transformations | Global, V8 isolates, sub-ms cold starts, 10 ms (free) / 30 s (paid) CPU | +| **Pages Functions** | Full-stack apps with file-based routing (Next.js, Remix, Astro, etc.) | Git-integrated, preview deployments, automatic framework detection | +| **Durable Objects** | Stateful coordination, WebSockets, rate limiting, collaborative editing | Single-threaded, strongly consistent, co-located storage | +| **Workflows** | Multi-step, long-running orchestration (order processing, ETL pipelines) | Durable execution, retries, state persistence across steps | + +### 3. Diagnose Issues + +When a Worker fails or misbehaves: + +1. **Check `wrangler tail`** — stream live logs from production Workers. Look for unhandled exceptions, binding errors, and unexpected status codes. +2. **Verify bindings** — ensure all KV/R2/D1/DO bindings in `wrangler.toml` match the `Env` interface. Missing or misnamed bindings are the #1 cause of runtime errors. +3. **Check compatibility date** — outdated `compatibility_date` can cause APIs to behave differently or be unavailable. +4. **Inspect CPU time** — if Workers hit CPU limits, profile with `wrangler dev --local` + Chrome DevTools. Offload heavy work to Queues or Durable Objects. +5. **Test locally** — use `wrangler dev` to reproduce issues with local bindings. Add `--remote` to test against real Cloudflare bindings. +6. **Review error responses** — Workers should return structured JSON errors, not raw exception text. Check error handling patterns. + +### 4. Optimize Performance + +#### Caching + +- Use the **Cache API** (`caches.default`) to cache upstream responses at the edge. +- Set `Cache-Control` headers for GET responses that serve cacheable data. +- Use KV for application-level caching with TTL-based expiration. +- Use **Smart Placement** (`[placement] mode = "smart"`) to run Workers near back-end services. + +```ts +const cache = caches.default; +const cacheKey = new Request(url.toString(), request); +let response = await cache.match(cacheKey); + +if (!response) { + response = await fetch(originUrl); + response = new Response(response.body, response); + response.headers.set("Cache-Control", "s-maxage=3600"); + ctx.waitUntil(cache.put(cacheKey, response.clone())); +} +``` + +#### Minimize Cold Starts + +- Workers already have near-zero cold starts (V8 isolates, not containers). +- Keep global scope code minimal — it runs on every cold start. +- Use dynamic `import()` for rarely-used, heavy code paths. +- Avoid large dependencies — audit bundle size with `wrangler deploy --dry-run --outdir dist`. + +#### Reduce Latency + +- Use `waitUntil()` to defer non-critical work (logging, analytics, cache warming). +- Parallelize independent I/O with `Promise.all()` instead of sequential `await`. +- Use streaming responses (`ReadableStream`) for large payloads instead of buffering. +- Choose the right storage: KV for reads, D1 for queries, R2 for files, Durable Objects for state. + +### 5. Architecture Patterns + +#### API Gateway + +``` +Client → Worker (routing, auth, rate limiting) → upstream APIs / D1 / KV +``` + +#### Full-Stack with Pages + +``` +Client → Cloudflare Pages (static assets + Functions) → D1 / KV / R2 +``` + +#### Real-Time with Durable Objects + +``` +Client → Worker (routing) → Durable Object (WebSocket handler, state) → KV / D1 (persistence) +``` + +#### Event-Driven with Queues + +``` +Worker (producer) → Queue → Worker (consumer) → D1 / R2 / external API +``` + +### 6. Pages vs. Workers + +| Feature | Workers | Pages | +|---|---|---| +| Deployment | `wrangler deploy` | Git push or `wrangler pages deploy` | +| Routing | Code-based or `wrangler.toml` routes | File-based (`functions/` directory) | +| Preview deployments | Manual | Automatic per branch | +| Static assets | Via `[assets]` or Workers Sites | Built-in, global CDN | +| Framework support | Manual | Auto-detected (Next.js, Remix, Astro, etc.) | +| Best for | APIs, middleware, custom routing | Full-stack web apps, JAMstack sites | + +## Response Format + +When helping with Worker issues, structure responses as: + +1. **Diagnosis**: What's wrong and why +2. **Root Cause**: The underlying issue (binding mismatch, CPU limit, API misuse, etc.) +3. **Fix**: Step-by-step resolution with code +4. **Prevention**: How to avoid this in the future + +When building new features: + +1. **Architecture**: Which Cloudflare primitives to use and why +2. **Implementation**: Code with proper types, bindings, and error handling +3. **Configuration**: Required `wrangler.toml` changes +4. **Testing**: How to verify with `wrangler dev` + +## Tools & Commands + +```bash +# Development +wrangler dev # Local development server +wrangler dev --remote # Dev with real Cloudflare bindings +wrangler tail # Stream live logs from production + +# Deployment +wrangler deploy # Deploy Worker +wrangler deploy --env production # Deploy to specific environment +wrangler pages deploy ./dist # Deploy Pages project +wrangler rollback # Roll back to previous deployment + +# Storage +wrangler kv namespace create # Create KV namespace +wrangler kv key put # Write a KV key +wrangler d1 create # Create D1 database +wrangler d1 execute --file schema.sql # Run SQL on D1 +wrangler r2 bucket create # Create R2 bucket + +# Secrets +wrangler secret put # Set encrypted secret +wrangler secret list # List secrets + +# Debugging +wrangler tail # Live log streaming +wrangler deployments list # List recent deployments +wrangler deployments view # View deployment details + +# Project +wrangler init # Create new project +wrangler types # Generate TypeScript types from wrangler.toml +wrangler whoami # Check current auth +``` + +## Guardrails + +- Never suggest hardcoding secrets, API keys, or tokens — always use `wrangler secret put`. +- Always recommend `compatibility_date` in `wrangler.toml` — never omit it. +- Warn about CPU time limits (10 ms free, 30 s paid) and suggest Queues or Durable Objects for heavy work. +- Recommend `wrangler dev` for local testing before deploying to production. +- Always type the `Env` interface in TypeScript projects to catch binding errors at compile time. +- Never recommend service worker syntax — always use ES module syntax (the modern default). +- Advise using `wrangler types` to auto-generate TypeScript types from `wrangler.toml` bindings. diff --git a/plugins/cloudflare/extensions/.gitkeep b/plugins/cloudflare/extensions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/cloudflare/hooks/hooks.json b/plugins/cloudflare/hooks/hooks.json new file mode 100644 index 0000000..a4bcbd1 --- /dev/null +++ b/plugins/cloudflare/hooks/hooks.json @@ -0,0 +1,36 @@ +{ + "hooks": [ + { + "event": "pre-commit", + "command": "npx wrangler deploy --dry-run --outdir /tmp/worker-build", + "description": "Validate Worker build and wrangler.toml configuration before committing changes", + "enabled": false + }, + { + "event": "pre-push", + "command": "npx wrangler deploy --dry-run --outdir /tmp/worker-build", + "description": "Run a full Worker build validation before pushing to ensure deployment will succeed", + "enabled": false + }, + { + "event": "post-deploy", + "command": "scripts/deploy-worker.sh", + "description": "Execute post-deployment tasks such as health checks and log tailing", + "enabled": true + }, + { + "event": "file-change", + "pattern": "wrangler.toml", + "command": "npx wrangler deploy --dry-run --outdir /tmp/worker-build", + "description": "Validate wrangler.toml whenever it is modified to catch configuration errors early", + "enabled": true + }, + { + "event": "file-change", + "pattern": "src/**/*.ts", + "command": "npx tsc --noEmit", + "description": "Type-check Worker source files when they are modified", + "enabled": true + } + ] +} diff --git a/plugins/cloudflare/mcp.json b/plugins/cloudflare/mcp.json new file mode 100644 index 0000000..848b618 --- /dev/null +++ b/plugins/cloudflare/mcp.json @@ -0,0 +1,30 @@ +{ + "mcpServers": { + "cloudflare": { + "command": "npx", + "args": ["-y", "@cloudflare/mcp-server-cloudflare"], + "description": "Cloudflare MCP server for managing Workers, KV, R2, D1, and other Cloudflare services", + "env": { + "CLOUDFLARE_API_TOKEN": "${CLOUDFLARE_API_TOKEN}", + "CLOUDFLARE_ACCOUNT_ID": "${CLOUDFLARE_ACCOUNT_ID}" + }, + "capabilities": [ + "list-workers", + "get-worker", + "deploy-worker", + "list-kv-namespaces", + "get-kv-value", + "put-kv-value", + "delete-kv-value", + "list-r2-buckets", + "get-r2-object", + "put-r2-object", + "delete-r2-object", + "list-d1-databases", + "execute-d1-query", + "list-pages-projects", + "get-worker-logs" + ] + } + } +} diff --git a/plugins/cloudflare/rules/cloudflare-config.mdc b/plugins/cloudflare/rules/cloudflare-config.mdc new file mode 100644 index 0000000..749d561 --- /dev/null +++ b/plugins/cloudflare/rules/cloudflare-config.mdc @@ -0,0 +1,266 @@ +--- +description: Best practices for Cloudflare wrangler.toml and project configuration +globs: + - "wrangler.toml" + - "wrangler.jsonc" +alwaysApply: false +--- + +# Cloudflare Wrangler Configuration Best Practices + +## Fundamentals + +- Use `wrangler.toml` as the single source of truth for Worker configuration. All bindings, routes, compatibility settings, and environment overrides belong here. +- Validate your configuration with `wrangler deploy --dry-run` before pushing changes. +- Keep `wrangler.toml` in version control — it defines the infrastructure-as-code contract for your Worker. + +```toml +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2025-12-01" +compatibility_flags = ["nodejs_compat"] +``` + +## Compatibility Date + +- **Always set `compatibility_date`** to pin runtime behavior. Without it, your Worker may break when Cloudflare changes default behavior. +- Use the latest stable date when creating a new project. Only advance the date deliberately, after testing. +- `compatibility_flags` opt into specific features ahead of their compatibility date (e.g., `nodejs_compat` for Node.js API polyfills). + +```toml +compatibility_date = "2025-12-01" +compatibility_flags = ["nodejs_compat"] +``` + +## KV Namespace Bindings + +- Define KV bindings with a clear `binding` name that matches your `Env` interface and an `id` referencing the KV namespace. +- Use `preview_id` for local development with `wrangler dev`. + +```toml +[[kv_namespaces]] +binding = "CACHE" +id = "abcdef1234567890abcdef1234567890" +preview_id = "preview_abcdef1234567890abcdef12" +``` + +## R2 Bucket Bindings + +- Bind R2 buckets by `bucket_name`. The `binding` name is how your Worker code references the bucket. +- Use `preview_bucket_name` for local development to avoid writing to production buckets. + +```toml +[[r2_buckets]] +binding = "ASSETS" +bucket_name = "my-assets-bucket" +preview_bucket_name = "my-assets-bucket-preview" +``` + +## D1 Database Bindings + +- Bind D1 databases with `database_name` and `database_id`. +- Use `wrangler d1 create ` to create a database and get the `database_id`. +- Always use parameterized queries (`.bind()`) to prevent SQL injection. + +```toml +[[d1_databases]] +binding = "DB" +database_name = "my-app-db" +database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +``` + +## Durable Object Bindings + +- Declare Durable Object classes in `[durable_objects.bindings]` and list migration tags in `[[migrations]]` to manage schema changes. +- Each Durable Object class must be exported from your Worker's entry point. + +```toml +[durable_objects] +bindings = [ + { name = "CHAT_ROOM", class_name = "ChatRoom" }, + { name = "RATE_LIMITER", class_name = "RateLimiter" }, +] + +[[migrations]] +tag = "v1" +new_classes = ["ChatRoom", "RateLimiter"] +``` + +## Queue Bindings + +- Define producers and consumers separately. A Worker can be both a producer and a consumer. + +```toml +# Producer +[[queues.producers]] +queue = "my-task-queue" +binding = "TASK_QUEUE" + +# Consumer +[[queues.consumers]] +queue = "my-task-queue" +max_batch_size = 10 +max_batch_timeout = 30 +max_retries = 3 +dead_letter_queue = "my-dlq" +``` + +## Routes and Custom Domains + +- Use `routes` to map URL patterns to your Worker. Routes require an existing Cloudflare zone (domain). +- Use `custom_domain` for Workers that should be accessible on their own domain without a zone route. +- Use `workers.dev` subdomain (enabled by default) for development and testing. + +```toml +# Route-based (requires zone) +routes = [ + { pattern = "api.example.com/v1/*", zone_name = "example.com" }, + { pattern = "example.com/api/*", zone_name = "example.com" }, +] + +# Or custom domain (no zone config needed) +# routes = [ +# { pattern = "api.example.com", custom_domain = true }, +# ] + +# workers.dev subdomain (default: true) +workers_dev = true +``` + +## Environment-Specific Configuration + +- Use `[env.staging]` and `[env.production]` blocks to override settings per environment. +- Deploy to a specific environment with `wrangler deploy --env staging`. +- Each environment can have its own bindings, routes, variables, and compatibility settings. +- Keep common configuration at the top level and override only what differs per environment. + +```toml +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2025-12-01" + +[vars] +LOG_LEVEL = "info" + +[[kv_namespaces]] +binding = "CACHE" +id = "dev-kv-id" + +# ─── Staging ────────────────────────────────────────────── +[env.staging] +name = "my-worker-staging" +routes = [ + { pattern = "staging-api.example.com/*", zone_name = "example.com" } +] + +[env.staging.vars] +LOG_LEVEL = "debug" + +[[env.staging.kv_namespaces]] +binding = "CACHE" +id = "staging-kv-id" + +# ─── Production ────────────────────────────────────────── +[env.production] +name = "my-worker-production" +routes = [ + { pattern = "api.example.com/*", zone_name = "example.com" } +] + +[[env.production.kv_namespaces]] +binding = "CACHE" +id = "production-kv-id" +``` + +## Cron Triggers + +- Use `[triggers]` to define cron schedules for Workers that run on a timer. +- Cron expressions use UTC timezone. +- The Worker must export a `scheduled` handler to respond to cron triggers. + +```toml +[triggers] +crons = [ + "0 */6 * * *", # Every 6 hours + "0 0 * * 0", # Every Sunday at midnight + "*/15 * * * *", # Every 15 minutes +] +``` + +Handler: + +```ts +export default { + async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise { + switch (event.cron) { + case "0 */6 * * *": + await syncData(env); + break; + case "0 0 * * 0": + await weeklyCleanup(env); + break; + } + }, +}; +``` + +## Limits and Resource Configuration + +- Set `usage_model` to `"bundled"` (default, includes CPU time) or `"unbound"` (pay-per-use, higher CPU limits) depending on workload. +- Configure `logpush` to send Worker logs to an external destination for production monitoring. +- Set `placement` to `{ mode = "smart" }` for Smart Placement, which automatically runs your Worker near your back-end services to reduce latency. + +```toml +# Smart Placement — run near your origin/database +[placement] +mode = "smart" + +# Observability +[observability] +enabled = true +``` + +## Secrets Management + +- **Never put secrets in `wrangler.toml`** — use `wrangler secret put ` to set encrypted secrets. +- Secrets are available as `env.` in your Worker code, just like `[vars]`. +- Use `[vars]` for non-sensitive configuration (feature flags, log levels, public API URLs). +- List secrets with `wrangler secret list` and delete with `wrangler secret delete `. + +```bash +# Set a secret +wrangler secret put API_TOKEN + +# Set a secret for a specific environment +wrangler secret put API_TOKEN --env production +``` + +## Static Assets (Sites / Pages) + +- For Workers serving static assets alongside dynamic logic, configure `[site]` or use Cloudflare Pages instead. +- Use `[assets]` (modern approach) to serve a directory of static files from your Worker. + +```toml +[assets] +directory = "./public" +``` + +## Build Configuration + +- Use `[build]` to customize the build command if your project requires a compilation step before deployment. +- Wrangler uses esbuild by default for TypeScript/JavaScript bundling — most projects don't need a custom build. + +```toml +[build] +command = "npm run build" +watch_dir = "src" +``` + +## Tail Workers (Logging) + +- Use `tail_consumers` to send Worker logs to a Tail Worker for custom log processing, filtering, and forwarding. + +```toml +[[tail_consumers]] +service = "my-log-worker" +``` diff --git a/plugins/cloudflare/rules/cloudflare-workers.mdc b/plugins/cloudflare/rules/cloudflare-workers.mdc new file mode 100644 index 0000000..4e8e859 --- /dev/null +++ b/plugins/cloudflare/rules/cloudflare-workers.mdc @@ -0,0 +1,278 @@ +--- +description: Best practices for Cloudflare Workers and edge computing +globs: + - "src/**/*.ts" + - "src/**/*.js" + - "worker.*" + - "functions/**/*" +alwaysApply: false +--- + +# Cloudflare Workers Best Practices + +## Web Standard APIs + +- **Use Web Standard APIs exclusively.** Workers run on the V8 isolate model, not Node.js. Use `fetch`, `Request`, `Response`, `Headers`, `URL`, `URLSearchParams`, `crypto` (Web Crypto API), `TextEncoder`, `TextDecoder`, `ReadableStream`, `WritableStream`, and `TransformStream`. +- Do **not** use Node.js built-ins (`fs`, `path`, `http`, `child_process`) unless you explicitly enable the `nodejs_compat` compatibility flag in `wrangler.toml`. +- Prefer `URLPattern` for URL routing instead of regex-based routers when possible. + +```ts +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + const path = url.pathname; + + if (path === "/api/data") { + return handleData(request, env, ctx); + } + + return new Response("Not Found", { status: 404 }); + }, +}; +``` + +## Error Handling + +- Always wrap handler logic in `try/catch` and return a proper `Response` object — never let exceptions propagate unhandled. +- Return structured JSON error bodies with a human-readable `error` field and appropriate HTTP status codes (`400`, `401`, `403`, `404`, `500`). +- Log errors with `console.error()` for debugging via `wrangler tail` but never expose internal details (stack traces, binding names, secrets) to clients. + +```ts +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + try { + const data = await request.json(); + if (!data.name) { + return Response.json({ error: "name is required" }, { status: 400 }); + } + + const result = await processData(data, env); + return Response.json(result, { status: 200 }); + } catch (err) { + console.error("Unhandled error:", err); + return Response.json( + { error: "Internal server error" }, + { status: 500 } + ); + } + }, +}; +``` + +## Choosing the Right Storage Binding + +Select the correct Cloudflare storage primitive for each use case: + +| Storage | Best For | Consistency | Limits | +|---|---|---|---| +| **KV** | Read-heavy, eventually consistent data (config, feature flags, cached content) | Eventual (global propagation ~60 s) | 25 MiB values, 512 B keys | +| **D1** | Relational data, complex queries, transactions | Strong (single-region) | SQLite-based, row-level | +| **R2** | Object/file storage (images, videos, backups, logs) | Strong | 5 TB per object, S3-compatible | +| **Durable Objects** | Stateful, real-time coordination (WebSocket servers, counters, sessions) | Strong (single-instance) | Co-located with clients | +| **Queues** | Async/background processing, task pipelines | At-least-once delivery | Batched consumption | +| **Hyperdrive** | Accelerating connections to existing Postgres databases | Pass-through | Connection pooling | + +### KV — Read-Heavy Data + +```ts +// Read from KV (fast, globally cached) +const value = await env.MY_KV.get("config:feature-flags", { type: "json" }); + +// Write to KV (eventually consistent) +await env.MY_KV.put("user:123:preferences", JSON.stringify(prefs), { + expirationTtl: 86400, // 24 hours +}); +``` + +### D1 — Relational Data + +```ts +const { results } = await env.DB.prepare( + "SELECT id, name, email FROM users WHERE active = ? ORDER BY created_at DESC LIMIT ?" +) + .bind(1, 20) + .all(); +``` + +### R2 — Object Storage + +```ts +// Upload a file +await env.BUCKET.put(`uploads/${crypto.randomUUID()}.png`, request.body, { + httpMetadata: { contentType: "image/png" }, +}); + +// Download a file +const object = await env.BUCKET.get("uploads/photo.png"); +if (!object) return new Response("Not Found", { status: 404 }); +return new Response(object.body, { + headers: { "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream" }, +}); +``` + +### Durable Objects — Stateful Workloads + +- Use Durable Objects when you need strongly consistent, single-threaded state (e.g., WebSocket rooms, counters, collaborative editing, rate limiters). +- Each Durable Object instance is globally unique and processes requests sequentially. +- Use the `storage` API inside the object for persistent, transactional key-value storage. + +```ts +export class ChatRoom implements DurableObject { + private sessions: WebSocket[] = []; + + constructor(private state: DurableObjectState, private env: Env) {} + + async fetch(request: Request): Promise { + const pair = new WebSocketPair(); + this.state.acceptWebSocket(pair[1]); + this.sessions.push(pair[1]); + return new Response(null, { status: 101, webSocket: pair[0] }); + } + + async webSocketMessage(ws: WebSocket, message: string) { + for (const session of this.sessions) { + if (session !== ws) session.send(message); + } + } +} +``` + +### Queues — Async Processing + +```ts +// Producer: send a message +await env.MY_QUEUE.send({ type: "email", to: "user@example.com", template: "welcome" }); + +// Consumer: process messages in batches +export default { + async queue(batch: MessageBatch, env: Env): Promise { + for (const message of batch.messages) { + await sendEmail(message.body, env); + message.ack(); + } + }, +}; +``` + +## CPU Time Limits + +- Workers have strict CPU time limits: **10 ms on the free plan** and **30 s on the paid plan** (Workers Paid / Workers for Platforms). +- CPU time does **not** include time spent waiting on I/O (`fetch`, KV reads, D1 queries). Only actual computation counts. +- Keep Workers small and focused. Offload heavy computation to Queues or Durable Objects. +- Profile CPU usage with `wrangler dev --local` and the Chrome DevTools profiler. + +## Background Tasks with `waitUntil()` + +- Use `ctx.waitUntil()` (from the `ExecutionContext`) to run background work after the response is sent — analytics, logging, cache warming, webhook deliveries. +- `waitUntil()` extends the Worker's lifetime but does **not** block the response. +- Never put critical business logic in `waitUntil()` — if the runtime terminates early, the promise may not complete. + +```ts +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const result = await handleRequest(request, env); + + // Fire-and-forget: log request analytics + ctx.waitUntil(logToAnalytics(request, result, env)); + + return Response.json(result); + }, +}; +``` + +## CORS Configuration + +- Handle `OPTIONS` preflight requests explicitly. +- Never use `Access-Control-Allow-Origin: *` for authenticated endpoints. +- Set `Access-Control-Max-Age` to reduce preflight frequency. + +```ts +const corsHeaders = { + "Access-Control-Allow-Origin": "https://yourdomain.com", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400", +}; + +function handleOptions(request: Request): Response { + return new Response(null, { status: 204, headers: corsHeaders }); +} + +function addCorsHeaders(response: Response): Response { + const newResponse = new Response(response.body, response); + Object.entries(corsHeaders).forEach(([key, value]) => { + newResponse.headers.set(key, value); + }); + return newResponse; +} +``` + +## Wrangler Configuration + +- Always use `wrangler.toml` (or `wrangler.jsonc`) as the single source of truth for Worker configuration. +- Define all bindings (KV, R2, D1, Durable Objects, Queues, secrets) in `wrangler.toml` — never hardcode binding names or IDs. +- Use `[vars]` for non-secret configuration and `wrangler secret put` for secrets. +- Set `compatibility_date` to the latest stable date to access new APIs while ensuring reproducible behavior. + +```toml +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2025-12-01" + +[vars] +ENVIRONMENT = "production" + +[[kv_namespaces]] +binding = "MY_KV" +id = "abc123" + +[[r2_buckets]] +binding = "BUCKET" +bucket_name = "my-bucket" + +[[d1_databases]] +binding = "DB" +database_name = "my-database" +database_id = "def456" +``` + +## Environment Bindings + +- Access all external resources (KV, R2, D1, Durable Objects, Queues, secrets, environment variables) through the `env` parameter — never use global variables or `process.env`. +- Type your `Env` interface to get full TypeScript IntelliSense and catch binding errors at compile time. + +```ts +interface Env { + MY_KV: KVNamespace; + BUCKET: R2Bucket; + DB: D1Database; + MY_QUEUE: Queue; + CHAT_ROOM: DurableObjectNamespace; + API_SECRET: string; + ENVIRONMENT: string; +} +``` + +## Module Worker Syntax + +- **Always use ES module syntax** (the modern default). Service worker syntax is legacy and lacks access to environment bindings, Durable Objects, and Queues. + +```ts +// ✅ Module syntax (recommended) +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + return new Response("Hello from module Worker!"); + }, +}; + +// ❌ Service worker syntax (legacy — avoid) +// addEventListener("fetch", (event) => { ... }); +``` + +## Performance Tips + +- Return responses as early as possible — avoid unnecessary `await` chains. +- Use `Cache API` (`caches.default`) for caching expensive upstream responses at the edge. +- Use `HTMLRewriter` for efficient streaming HTML transformations without buffering the entire document. +- Prefer `Response.json()` over manually setting `Content-Type` and `JSON.stringify()`. +- Minimize global scope code — it runs on every cold start across all isolates. diff --git a/plugins/cloudflare/scripts/deploy-worker.sh b/plugins/cloudflare/scripts/deploy-worker.sh new file mode 100755 index 0000000..0a70afb --- /dev/null +++ b/plugins/cloudflare/scripts/deploy-worker.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash +# +# deploy-worker.sh — Deploy a Cloudflare Worker +# +# Usage: +# ./scripts/deploy-worker.sh # Deploy to default environment +# ./scripts/deploy-worker.sh --env production # Deploy to production +# ./scripts/deploy-worker.sh --dry-run # Validate without deploying +# ./scripts/deploy-worker.sh --help # Show help +# +# Environment variables: +# CLOUDFLARE_API_TOKEN — Authentication token (required in CI) +# CLOUDFLARE_ACCOUNT_ID — Cloudflare account ID +# + +set -euo pipefail + +# ─── Colors ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ─── Helpers ────────────────────────────────────────────────────────────────── +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +success() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +usage() { + cat </dev/null; then + warn "Wrangler CLI not found. Installing..." + npm install -g wrangler +fi + +WRANGLER_VERSION=$(wrangler --version 2>/dev/null | head -1) +info "Wrangler CLI version: ${WRANGLER_VERSION}" + +# Check for wrangler.toml +if [[ ! -f "wrangler.toml" ]] && [[ ! -f "wrangler.jsonc" ]]; then + error "No wrangler.toml or wrangler.jsonc found in the current directory." + error "Please run this script from the project root." + exit 1 +fi + +# Extract Worker name from wrangler.toml +WORKER_NAME="unknown" +if [[ -f "wrangler.toml" ]]; then + WORKER_NAME=$(grep -m1 '^name' wrangler.toml | sed 's/name *= *"\(.*\)"/\1/' || echo "unknown") +fi +info "Worker name: ${WORKER_NAME}" + +# Check for authentication in CI +if [[ -n "${CI:-}" ]]; then + if [[ -z "${CLOUDFLARE_API_TOKEN:-}" ]]; then + error "CLOUDFLARE_API_TOKEN is required in CI environments." + error "Set it as a secret in your CI provider." + exit 1 + fi +fi + +success "Preflight checks passed." + +# ─── TypeScript Check ───────────────────────────────────────────────────────── +if [[ -f "tsconfig.json" ]]; then + info "Running TypeScript type check..." + if npx tsc --noEmit 2>/dev/null; then + success "TypeScript check passed." + else + warn "TypeScript errors detected. The build may still succeed if errors are non-blocking." + fi +fi + +# ─── Dry Run ────────────────────────────────────────────────────────────────── +if [[ "${DRY_RUN}" == true ]]; then + info "Dry run mode — validating build without deploying..." + # shellcheck disable=SC2086 + wrangler deploy --dry-run --outdir /tmp/worker-build ${ENV_FLAG} || { + error "Build validation failed." + exit 1 + } + success "Build validation passed. No deployment was created." + exit 0 +fi + +# ─── Apply D1 Migrations ───────────────────────────────────────────────────── +if [[ "${MIGRATE}" == true ]]; then + info "Applying D1 migrations..." + + # Find D1 database names from wrangler.toml + if [[ -f "wrangler.toml" ]]; then + DB_NAMES=$(grep 'database_name' wrangler.toml | sed 's/.*= *"\(.*\)"/\1/' || true) + if [[ -n "${DB_NAMES}" ]]; then + while IFS= read -r db_name; do + info "Applying migrations for D1 database: ${db_name}" + # shellcheck disable=SC2086 + wrangler d1 migrations apply "${db_name}" --remote ${ENV_FLAG} || { + error "Failed to apply migrations for ${db_name}." + exit 1 + } + success "Migrations applied for ${db_name}." + done <<< "${DB_NAMES}" + else + warn "No D1 databases found in wrangler.toml. Skipping migrations." + fi + fi +fi + +# ─── Deploy ─────────────────────────────────────────────────────────────────── +DEPLOY_ENV="default" +if [[ -n "${ENV_FLAG}" ]]; then + DEPLOY_ENV=$(echo "${ENV_FLAG}" | awk '{print $2}') +fi + +info "Starting deployment to '${DEPLOY_ENV}' environment..." + +# shellcheck disable=SC2086 +DEPLOY_OUTPUT=$(wrangler deploy ${ENV_FLAG} 2>&1) || { + error "Deployment failed." + echo "${DEPLOY_OUTPUT}" + exit 1 +} + +# Extract the deployment URL from output +DEPLOYMENT_URL=$(echo "${DEPLOY_OUTPUT}" | grep -oE 'https://[^ ]+\.workers\.dev' | head -1 || true) +if [[ -z "${DEPLOYMENT_URL}" ]]; then + DEPLOYMENT_URL=$(echo "${DEPLOY_OUTPUT}" | grep -oE 'https://[^ ]+' | head -1 || true) +fi + +success "Deployment complete!" +if [[ -n "${DEPLOYMENT_URL}" ]]; then + info "URL: ${DEPLOYMENT_URL}" +fi + +# ─── Post-Deploy Health Check ───────────────────────────────────────────────── +if [[ -n "${DEPLOYMENT_URL}" ]]; then + info "Running post-deployment health check..." + + MAX_RETRIES=5 + RETRY_DELAY=3 + + for i in $(seq 1 ${MAX_RETRIES}); do + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${DEPLOYMENT_URL}" 2>/dev/null || echo "000") + + if [[ "${HTTP_STATUS}" == "200" ]]; then + success "Health check passed (HTTP ${HTTP_STATUS})." + break + elif [[ "${HTTP_STATUS}" =~ ^3[0-9]{2}$ ]]; then + success "Health check passed (HTTP ${HTTP_STATUS} — redirect detected)." + break + else + if [[ ${i} -lt ${MAX_RETRIES} ]]; then + warn "Health check attempt ${i}/${MAX_RETRIES} returned HTTP ${HTTP_STATUS}. Retrying in ${RETRY_DELAY}s..." + sleep ${RETRY_DELAY} + else + warn "Health check failed after ${MAX_RETRIES} attempts (HTTP ${HTTP_STATUS})." + warn "The Worker may still be propagating. Check: ${DEPLOYMENT_URL}" + fi + fi + done +fi + +# ─── Tail Logs ──────────────────────────────────────────────────────────────── +if [[ "${TAIL}" == true ]]; then + info "Starting live log stream (Ctrl+C to stop)..." + # shellcheck disable=SC2086 + wrangler tail ${ENV_FLAG} +fi + +# ─── Summary ────────────────────────────────────────────────────────────────── +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ Deployment Summary ║" +echo "╠══════════════════════════════════════════════════════════════╣" +printf "║ Worker: %-43s ║\n" "${WORKER_NAME}" +printf "║ Environment: %-43s ║\n" "${DEPLOY_ENV}" +if [[ -n "${DEPLOYMENT_URL}" ]]; then +printf "║ URL: %-43s ║\n" "${DEPLOYMENT_URL}" +fi +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" diff --git a/plugins/cloudflare/skills/create-worker/SKILL.md b/plugins/cloudflare/skills/create-worker/SKILL.md new file mode 100644 index 0000000..67ce6da --- /dev/null +++ b/plugins/cloudflare/skills/create-worker/SKILL.md @@ -0,0 +1,428 @@ +# Skill: Create a Cloudflare Worker + +## Description + +Step-by-step guide for creating a new Cloudflare Worker from scratch, including project setup, TypeScript configuration, bindings, local development, and deployment. + +## Prerequisites + +- Node.js 18+ installed +- A Cloudflare account (https://dash.cloudflare.com/sign-up) +- Wrangler CLI (`npm install -g wrangler`) + +## Steps + +### Step 1: Install Wrangler CLI + +Install the Wrangler CLI globally: + +```bash +npm install -g wrangler +``` + +Verify the installation: + +```bash +wrangler --version +``` + +### Step 2: Authenticate + +Log in to your Cloudflare account: + +```bash +wrangler login +``` + +This opens a browser window for OAuth authentication. For CI/CD, use an API token: + +```bash +export CLOUDFLARE_API_TOKEN="your-api-token" +``` + +Verify authentication: + +```bash +wrangler whoami +``` + +### Step 3: Create a New Project + +Scaffold a new Worker project: + +```bash +wrangler init my-worker +``` + +Follow the prompts to select: +- **Type**: "Hello World" Worker, scheduled handler, or Durable Object +- **Language**: TypeScript (recommended) or JavaScript +- **Package manager**: npm, pnpm, or yarn + +This creates the following project structure: + +``` +my-worker/ +├── src/ +│ └── index.ts # Worker entry point +├── test/ +│ └── index.spec.ts # Vitest test file +├── wrangler.toml # Wrangler configuration +├── tsconfig.json # TypeScript configuration +├── package.json # Dependencies +└── vitest.config.ts # Test configuration +``` + +Alternatively, create a project from a template: + +```bash +npm create cloudflare@latest my-worker -- --template "cloudflare/workers-sdk/templates/worker-typescript" +``` + +### Step 4: Configure wrangler.toml + +Edit `wrangler.toml` with your project settings: + +```toml +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2025-12-01" +compatibility_flags = ["nodejs_compat"] + +# Enable workers.dev subdomain for development +workers_dev = true + +# Non-secret environment variables +[vars] +ENVIRONMENT = "development" +API_VERSION = "v1" + +# Smart Placement — run near your back-end services +[placement] +mode = "smart" + +# Observability +[observability] +enabled = true +``` + +### Step 5: Write Your Worker (Module Syntax) + +Workers use ES module syntax with typed environment bindings: + +```ts +// src/index.ts +export interface Env { + ENVIRONMENT: string; + API_VERSION: string; +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + + // Simple router + switch (url.pathname) { + case "/": + return new Response("Hello from Cloudflare Workers!", { + headers: { "Content-Type": "text/plain" }, + }); + + case "/api/health": + return Response.json({ + status: "healthy", + environment: env.ENVIRONMENT, + version: env.API_VERSION, + timestamp: new Date().toISOString(), + }); + + default: + return Response.json( + { error: "Not Found" }, + { status: 404 } + ); + } + }, +}; +``` + +### Step 6: Add Bindings (KV, R2, D1) + +Create the resources you need: + +```bash +# Create a KV namespace +wrangler kv namespace create "CACHE" +# Output: Created namespace with id "abc123..." + +# Create an R2 bucket +wrangler r2 bucket create "my-assets" + +# Create a D1 database +wrangler d1 create "my-database" +# Output: Created database with id "def456..." +``` + +Add bindings to `wrangler.toml`: + +```toml +[[kv_namespaces]] +binding = "CACHE" +id = "abc123..." # Use the ID from the create command + +[[r2_buckets]] +binding = "ASSETS" +bucket_name = "my-assets" + +[[d1_databases]] +binding = "DB" +database_name = "my-database" +database_id = "def456..." # Use the ID from the create command +``` + +Update your `Env` interface: + +```ts +export interface Env { + CACHE: KVNamespace; + ASSETS: R2Bucket; + DB: D1Database; + ENVIRONMENT: string; + API_VERSION: string; +} +``` + +Generate TypeScript types from your bindings: + +```bash +wrangler types +``` + +### Step 7: Add Secrets + +Set encrypted secrets (never put these in `wrangler.toml`): + +```bash +wrangler secret put API_TOKEN +# Enter secret value when prompted + +wrangler secret put DATABASE_URL +``` + +Add secrets to your `Env` interface: + +```ts +export interface Env { + API_TOKEN: string; + DATABASE_URL: string; + // ... other bindings +} +``` + +### Step 8: Local Development + +Start the local development server: + +```bash +wrangler dev +``` + +This starts a local server (default: `http://localhost:8787`) with: +- Hot reloading on file changes +- Local KV, R2, and D1 emulation +- `console.log` output in the terminal + +Test with real Cloudflare bindings: + +```bash +wrangler dev --remote +``` + +Test your endpoints: + +```bash +curl http://localhost:8787/ +curl http://localhost:8787/api/health +``` + +### Step 9: Write Tests + +Use Vitest with the Cloudflare Workers test utilities: + +```ts +// test/index.spec.ts +import { env, createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; +import { describe, it, expect } from "vitest"; +import worker from "../src/index"; + +describe("Worker", () => { + it("responds with hello message on /", async () => { + const request = new Request("http://localhost/"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const text = await response.text(); + expect(text).toBe("Hello from Cloudflare Workers!"); + }); + + it("returns health check on /api/health", async () => { + const request = new Request("http://localhost/api/health"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.status).toBe("healthy"); + }); + + it("returns 404 for unknown routes", async () => { + const request = new Request("http://localhost/unknown"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(404); + }); +}); +``` + +Run tests: + +```bash +npx vitest run +``` + +### Step 10: Deploy + +Deploy to Cloudflare: + +```bash +# Deploy to workers.dev (preview) +wrangler deploy + +# Deploy to a specific environment +wrangler deploy --env production +``` + +Verify the deployment: + +```bash +# List recent deployments +wrangler deployments list + +# Stream live logs +wrangler tail +``` + +Test your deployed Worker: + +```bash +curl https://my-worker..workers.dev/ +curl https://my-worker..workers.dev/api/health +``` + +### Step 11: Set Up Custom Domain (Optional) + +Add a custom domain via the Cloudflare Dashboard or in `wrangler.toml`: + +```toml +routes = [ + { pattern = "api.yourdomain.com/*", zone_name = "yourdomain.com" } +] +``` + +Or use a custom domain (no zone configuration needed): + +```toml +routes = [ + { pattern = "api.yourdomain.com", custom_domain = true } +] +``` + +Deploy with the new route: + +```bash +wrangler deploy +``` + +### Step 12: Set Up CI/CD (Optional) + +GitHub Actions workflow for automated deployment: + +```yaml +# .github/workflows/deploy.yml +name: Deploy Worker +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npx vitest run + - uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + command: deploy +``` + +## Project Structure (Full Example) + +``` +my-worker/ +├── src/ +│ ├── index.ts # Worker entry point and router +│ ├── handlers/ +│ │ ├── api.ts # API route handlers +│ │ └── assets.ts # Static asset handlers +│ ├── middleware/ +│ │ ├── auth.ts # Authentication middleware +│ │ └── cors.ts # CORS middleware +│ ├── services/ +│ │ ├── database.ts # D1 query helpers +│ │ └── storage.ts # R2/KV helpers +│ └── types.ts # Env interface and shared types +├── migrations/ +│ └── 0001_initial.sql # D1 database migrations +├── test/ +│ └── index.spec.ts # Tests +├── wrangler.toml # Wrangler configuration +├── tsconfig.json +├── package.json +└── vitest.config.ts +``` + +## Troubleshooting + +### "Binding not found" Errors + +- Verify the binding name in `wrangler.toml` exactly matches the property name in your `Env` interface. +- Run `wrangler types` to regenerate TypeScript types from your configuration. +- Ensure you're using `env.` (module syntax), not global variables. + +### CPU Time Exceeded + +- Profile with `wrangler dev --local` and Chrome DevTools. +- Move heavy computation to a Queue consumer or Durable Object. +- Use `ctx.waitUntil()` for non-critical background work. + +### "Module not found" Errors + +- Ensure the `main` field in `wrangler.toml` points to the correct entry file. +- Check that `tsconfig.json` includes the `src` directory. +- Run `wrangler types` to generate Cloudflare-specific type definitions. + +### Local Development Issues + +- Use `wrangler dev --local` for fully local emulation (no Cloudflare account needed). +- Use `wrangler dev --remote` when you need real Cloudflare bindings. +- Check that ports aren't conflicting (default: 8787). diff --git a/plugins/cloudflare/skills/setup-d1-database/SKILL.md b/plugins/cloudflare/skills/setup-d1-database/SKILL.md new file mode 100644 index 0000000..554ac28 --- /dev/null +++ b/plugins/cloudflare/skills/setup-d1-database/SKILL.md @@ -0,0 +1,455 @@ +# Skill: Set Up a Cloudflare D1 Database + +## Description + +Step-by-step guide for creating a Cloudflare D1 database, writing migrations, designing schemas, and querying from Workers. D1 is Cloudflare's native serverless SQL database built on SQLite. + +## Prerequisites + +- Wrangler CLI installed (`npm install -g wrangler`) +- Authenticated with Cloudflare (`wrangler login`) +- An existing Worker project (see **Create a Worker** skill) + +## Steps + +### Step 1: Create a D1 Database + +```bash +# Create a new D1 database +wrangler d1 create my-app-db + +# Output: +# ✅ Successfully created DB 'my-app-db' +# +# [[d1_databases]] +# binding = "DB" +# database_name = "my-app-db" +# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +``` + +Copy the output and add it to your `wrangler.toml`: + +```toml +[[d1_databases]] +binding = "DB" +database_name = "my-app-db" +database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +``` + +### Step 2: Create a Migrations Directory + +```bash +mkdir -p migrations +``` + +Migrations are numbered SQL files that run in order. Create your first migration: + +```bash +# Using wrangler (recommended) +wrangler d1 migrations create my-app-db create_initial_tables + +# Output: Created migration: migrations/0001_create_initial_tables.sql +``` + +### Step 3: Write the Initial Schema + +Edit `migrations/0001_create_initial_tables.sql`: + +```sql +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + email TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + avatar_url TEXT, + role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Create index on email for fast lookups +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); + +-- Posts table +CREATE TABLE IF NOT EXISTS posts ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + content TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')), + published_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Indexes for common queries +CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id); +CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug); +CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status); +CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(status, published_at DESC) + WHERE status = 'published'; + +-- Tags table +CREATE TABLE IF NOT EXISTS tags ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + name TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Post-tag relationship (many-to-many) +CREATE TABLE IF NOT EXISTS post_tags ( + post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, + tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (post_id, tag_id) +); + +CREATE INDEX IF NOT EXISTS idx_post_tags_tag_id ON post_tags(tag_id); +``` + +### Step 4: Apply Migrations Locally + +Apply migrations to the local D1 database for development: + +```bash +# Apply all pending migrations locally +wrangler d1 migrations apply my-app-db --local + +# Verify the tables were created +wrangler d1 execute my-app-db --local --command "SELECT name FROM sqlite_master WHERE type='table'" +``` + +### Step 5: Seed Development Data (Optional) + +Create a seed file `migrations/seed.sql`: + +```sql +-- Seed users +INSERT INTO users (id, email, name, role) VALUES + ('user_001', 'alice@example.com', 'Alice', 'admin'), + ('user_002', 'bob@example.com', 'Bob', 'user'), + ('user_003', 'carol@example.com', 'Carol', 'moderator'); + +-- Seed tags +INSERT INTO tags (id, name) VALUES + ('tag_001', 'typescript'), + ('tag_002', 'cloudflare'), + ('tag_003', 'workers'); + +-- Seed posts +INSERT INTO posts (id, user_id, title, slug, content, status, published_at) VALUES + ('post_001', 'user_001', 'Getting Started with D1', 'getting-started-with-d1', + 'D1 is Cloudflare''s serverless SQL database...', 'published', datetime('now')), + ('post_002', 'user_002', 'Building APIs with Workers', 'building-apis-with-workers', + 'Cloudflare Workers make it easy to build APIs...', 'published', datetime('now')), + ('post_003', 'user_001', 'Draft Post', 'draft-post', + 'This is a draft...', 'draft', NULL); + +-- Seed post-tag relationships +INSERT INTO post_tags (post_id, tag_id) VALUES + ('post_001', 'tag_002'), + ('post_001', 'tag_003'), + ('post_002', 'tag_001'), + ('post_002', 'tag_003'); +``` + +Apply the seed data: + +```bash +wrangler d1 execute my-app-db --local --file migrations/seed.sql +``` + +### Step 6: Query D1 from Your Worker + +Update your Worker to query the database: + +```ts +// src/index.ts +export interface Env { + DB: D1Database; +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + + try { + switch (url.pathname) { + case "/api/posts": + return handleListPosts(request, env); + case "/api/posts/create": + return handleCreatePost(request, env); + default: + // Dynamic route: /api/posts/:slug + const slugMatch = url.pathname.match(/^\/api\/posts\/(.+)$/); + if (slugMatch) { + return handleGetPost(slugMatch[1], env); + } + return Response.json({ error: "Not Found" }, { status: 404 }); + } + } catch (err) { + console.error("Database error:", err); + return Response.json({ error: "Internal server error" }, { status: 500 }); + } + }, +}; +``` + +### Step 7: Implement Query Helpers + +Create reusable database query functions: + +```ts +// src/services/database.ts + +// List published posts with pagination +export async function listPosts( + db: D1Database, + page: number = 1, + limit: number = 20 +): Promise<{ posts: any[]; total: number }> { + const offset = (page - 1) * limit; + + const [postsResult, countResult] = await Promise.all([ + db + .prepare( + `SELECT p.id, p.title, p.slug, p.status, p.published_at, p.created_at, + u.name AS author_name, u.avatar_url AS author_avatar + FROM posts p + JOIN users u ON p.user_id = u.id + WHERE p.status = 'published' + ORDER BY p.published_at DESC + LIMIT ? OFFSET ?` + ) + .bind(limit, offset) + .all(), + db + .prepare("SELECT COUNT(*) AS total FROM posts WHERE status = 'published'") + .first<{ total: number }>(), + ]); + + return { + posts: postsResult.results, + total: countResult?.total ?? 0, + }; +} + +// Get a single post by slug with tags +export async function getPostBySlug( + db: D1Database, + slug: string +): Promise { + const post = await db + .prepare( + `SELECT p.*, u.name AS author_name, u.avatar_url AS author_avatar + FROM posts p + JOIN users u ON p.user_id = u.id + WHERE p.slug = ?` + ) + .bind(slug) + .first(); + + if (!post) return null; + + const tags = await db + .prepare( + `SELECT t.name FROM tags t + JOIN post_tags pt ON t.id = pt.tag_id + WHERE pt.post_id = ?` + ) + .bind(post.id) + .all(); + + return { ...post, tags: tags.results.map((t: any) => t.name) }; +} + +// Create a new post +export async function createPost( + db: D1Database, + data: { userId: string; title: string; slug: string; content: string; tagIds?: string[] } +): Promise { + const id = crypto.randomUUID().replace(/-/g, ""); + + // Use a batch for transactional insert + const statements = [ + db + .prepare( + "INSERT INTO posts (id, user_id, title, slug, content) VALUES (?, ?, ?, ?, ?)" + ) + .bind(id, data.userId, data.title, data.slug, data.content), + ]; + + // Add tag associations + if (data.tagIds?.length) { + for (const tagId of data.tagIds) { + statements.push( + db + .prepare("INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)") + .bind(id, tagId) + ); + } + } + + await db.batch(statements); + + return getPostBySlug(db, data.slug); +} + +// Full-text search using LIKE (for simple cases) +export async function searchPosts( + db: D1Database, + query: string +): Promise { + const results = await db + .prepare( + `SELECT p.id, p.title, p.slug, p.published_at + FROM posts p + WHERE p.status = 'published' + AND (p.title LIKE ? OR p.content LIKE ?) + ORDER BY p.published_at DESC + LIMIT 20` + ) + .bind(`%${query}%`, `%${query}%`) + .all(); + + return results.results; +} +``` + +### Step 8: Create Additional Migrations + +As your schema evolves, create new migrations — never edit applied migrations: + +```bash +wrangler d1 migrations create my-app-db add_comments_table +``` + +Edit `migrations/0002_add_comments_table.sql`: + +```sql +CREATE TABLE IF NOT EXISTS comments ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + body TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_comments_post_id ON comments(post_id); +CREATE INDEX IF NOT EXISTS idx_comments_user_id ON comments(user_id); + +-- Add comment count to posts for denormalization +ALTER TABLE posts ADD COLUMN comment_count INTEGER NOT NULL DEFAULT 0; +``` + +Apply the new migration: + +```bash +# Locally +wrangler d1 migrations apply my-app-db --local + +# Remotely (production) +wrangler d1 migrations apply my-app-db --remote +``` + +### Step 9: Deploy to Production + +Apply migrations to the remote (production) database: + +```bash +# Check pending migrations +wrangler d1 migrations list my-app-db --remote + +# Apply migrations to production +wrangler d1 migrations apply my-app-db --remote + +# Deploy the Worker +wrangler deploy +``` + +### Step 10: Monitor and Debug + +```bash +# Query the remote database directly +wrangler d1 execute my-app-db --remote --command "SELECT COUNT(*) FROM posts" + +# Export the database for backup +wrangler d1 export my-app-db --remote --output backup.sql + +# View D1 metrics in the Cloudflare Dashboard +# Dashboard → Workers & Pages → D1 → my-app-db → Metrics +``` + +## D1 Best Practices + +### Use Parameterized Queries + +Always use `.bind()` for user input — never concatenate strings into SQL: + +```ts +// ✅ Safe — parameterized +const result = await db + .prepare("SELECT * FROM users WHERE email = ?") + .bind(email) + .first(); + +// ❌ Dangerous — SQL injection risk +const result = await db + .prepare(`SELECT * FROM users WHERE email = '${email}'`) + .first(); +``` + +### Use Batch for Transactions + +D1's `.batch()` executes multiple statements in a single transaction: + +```ts +const results = await db.batch([ + db.prepare("INSERT INTO users (id, email, name) VALUES (?, ?, ?)").bind(id, email, name), + db.prepare("INSERT INTO user_settings (user_id, theme) VALUES (?, ?)").bind(id, "dark"), +]); +``` + +### Use Appropriate Data Types + +D1 is built on SQLite — use SQLite-compatible types: + +| Concept | Recommended Type | Notes | +|---|---|---| +| Primary key | `TEXT` with `randomblob` or UUID | SQLite has no native UUID type | +| Timestamps | `TEXT` with `datetime('now')` | ISO 8601 format, stored as text | +| Boolean | `INTEGER` (0/1) | SQLite has no native boolean | +| JSON | `TEXT` | Store as serialized JSON, parse in Worker | +| Enums | `TEXT` with `CHECK` constraint | Enforced at the database level | + +### Index Strategy + +- Index columns used in `WHERE`, `JOIN`, and `ORDER BY` clauses. +- Use partial indexes (`WHERE` clause on index) for filtered queries. +- Don't over-index — each index adds write overhead. + +```sql +-- Partial index: only index published posts +CREATE INDEX idx_posts_published ON posts(published_at DESC) + WHERE status = 'published'; + +-- Composite index for common query patterns +CREATE INDEX idx_posts_user_status ON posts(user_id, status); +``` + +## Common Pitfalls + +- **Editing applied migrations**: Never modify a migration that has been applied. Create a new migration instead. +- **Missing indexes on foreign keys**: SQLite does not auto-index foreign key columns. Always add explicit indexes. +- **Large result sets**: D1 has row limits per query. Use `LIMIT` and `OFFSET` for pagination, or cursor-based pagination for large datasets. +- **Not using `.batch()`**: Multiple related writes should use `.batch()` for atomicity and performance. +- **Forgetting `--local` vs. `--remote`**: Always test migrations locally first with `--local` before applying to production with `--remote`. + +## Verification + +1. Run `wrangler d1 migrations apply my-app-db --local` — all migrations apply cleanly. +2. Query tables via `wrangler d1 execute my-app-db --local --command "SELECT * FROM users"`. +3. Start `wrangler dev` and test API endpoints against the local D1 database. +4. Run `wrangler d1 migrations apply my-app-db --remote` — migrations apply to production. +5. Deploy with `wrangler deploy` and verify live queries via `curl` or the browser.