-
Notifications
You must be signed in to change notification settings - Fork 984
feat: Add Cloudflare deployment configuration #1293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add Cloudflare deployment configuration #1293
Conversation
Complete deployment setup for Cloudflare infrastructure without Docker: Web App (Cloudflare Pages): - Add @opennextjs/cloudflare and wrangler dependencies - Create wrangler.jsonc with Hyperdrive and R2 bindings - Add open-next.config.ts for OpenNext adapter - Implement Cloudflare Images loader - Update next.config.mjs for Cloudflare compatibility - Remove Turbopack flag (not compatible with OpenNext) - Add Cloudflare-specific build and deploy scripts Tasks Service (Deno Deploy): - Create Deno-compatible version using Hono framework - Implement FFmpeg audio merging for Deno runtime - Add deno.json and deployctl.json configuration - FFmpeg works natively on Deno Deploy (vs Workers limitation) Infrastructure: - Cloudflare R2 for S3-compatible storage (zero egress fees) - Cloudflare Hyperdrive for MySQL database connection pooling - Cloudflare Pages for Next.js hosting with edge distribution - Deno Deploy for tasks service and web-cluster (Effect.js support) Documentation: - CLOUDFLARE_README.md: Complete deployment overview - apps/web/CLOUDFLARE_DEPLOYMENT.md: Detailed step-by-step guide - .env.cloudflare.example: Environment variable template - deploy-cloudflare.sh: Automated deployment script Benefits: - 50-75% cost reduction vs Railway/Docker - 98% cost savings on video streaming (R2 zero egress) - Global edge network distribution - Automatic SSL/TLS and DDoS protection - No Docker required 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
WalkthroughThis pull request adds comprehensive Cloudflare deployment infrastructure for Cap, including configuration files for Cloudflare Workers and Wrangler, a new Deno-based Tasks service for audio segment merging via FFmpeg, deployment automation scripts, documentation, and web app integrations for Cloudflare environments. Changes
Sequence DiagramsequenceDiagram
participant Client
participant Tasks as Tasks Service<br/>(Deno)
participant FFmpeg
participant FileSystem as Local FS
participant UploadURL as Upload URL<br/>(R2/Storage)
Client->>Tasks: POST /api/v1/merge-audio-segments<br/>(segments[], uploadUrl, videoId)
Tasks->>Tasks: Validate request body
alt Validation fails
Tasks-->>Client: 400 Bad Request
else Validation passes
Tasks->>FileSystem: Create output directory
Tasks->>FFmpeg: Execute concatenation command<br/>(segments → MP3)
alt FFmpeg succeeds
FFmpeg-->>FileSystem: Output MP3 file
Tasks->>FileSystem: Read MP3 into buffer
Tasks->>UploadURL: HTTP PUT audio/mpeg<br/>(buffer)
alt Upload succeeds
UploadURL-->>Tasks: 200 OK
Tasks->>FileSystem: Clean up local file
Tasks-->>Client: {"response": "COMPLETE"}
else Upload fails
Tasks-->>Client: 500 Upload failed
end
else FFmpeg fails
FFmpeg-->>Tasks: Non-zero exit
Tasks-->>Client: 500 FFmpeg error
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🧹 Nitpick comments (9)
apps/web/wrangler.jsonc (1)
5-5: Update compatibility_date to a more recent date.The compatibility_date is set to "2025-05-05", which is several months old. Cloudflare recommends using a recent date to access the latest features and bug fixes.
- "compatibility_date": "2025-05-05", + "compatibility_date": "2025-10-25",apps/tasks/src/index-deno.ts (3)
64-82: Stream file upload instead of loading entire file into memory.Loading the entire merged audio file into memory can cause out-of-memory errors for large files. Consider streaming the upload.
- const buffer = await Deno.readFile(filePath); + const file = await Deno.open(filePath, { read: true }); + + let uploadResponse; + try { - const uploadResponse = await fetch(body.uploadUrl, { + uploadResponse = await fetch(body.uploadUrl, { method: "PUT", - body: buffer, + body: file.readable, headers: { "Content-Type": "audio/mpeg", }, }); + } finally { + file.close(); + }
66-82: Add timeout and retry logic for upload.The upload can fail or hang indefinitely without a timeout. Consider adding timeout and basic retry logic for transient failures.
const UPLOAD_TIMEOUT = 60000; // 1 minute const MAX_RETRIES = 3; let uploadResponse: Response | undefined; let lastError: Error | undefined; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT); uploadResponse = await fetch(body.uploadUrl, { method: "PUT", body: buffer, headers: { "Content-Type": "audio/mpeg", }, signal: controller.signal, }); clearTimeout(timeoutId); if (uploadResponse.ok) { break; } lastError = new Error(`Upload failed with status ${uploadResponse.status}`); } catch (error) { lastError = error as Error; console.error(`Upload attempt ${attempt + 1} failed:`, error); if (attempt < MAX_RETRIES - 1) { await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1))); } } } if (!uploadResponse?.ok) { console.error("Upload failed after retries:", lastError); return c.json({ response: "FAILED", error: "Upload failed" }, 500); }
24-28: Log directory creation errors.Silently catching errors during directory creation can hide permission issues or disk space problems.
try { await Deno.mkdir(outputDir, { recursive: true }); } catch (error) { + console.warn("Failed to create output directory:", error); }deploy-cloudflare.sh (1)
69-74: Consider validating Hyperdrive ID format.The Hyperdrive ID input only checks for empty value but doesn't validate format. Hyperdrive IDs follow a specific format. Adding basic validation would catch typos early and prevent cryptic deployment failures downstream.
Example validation:
# Hyperdrive IDs are typically hex strings, often 16+ characters if ! [[ $HYPERDRIVE_ID =~ ^[a-z0-9-]+$ ]] || [ ${#HYPERDRIVE_ID} -lt 8 ]; then echo -e "${RED}Error: Invalid Hyperdrive ID format${NC}" exit 1 fi.env.cloudflare.example (1)
1-69: Optional: Consider alphabetical ordering within environment sections for linter compliance.The dotenv-linter flagged 12 UnorderedKey warnings due to non-alphabetical ordering (e.g., NEXTAUTH_SECRET before NODE_ENV). While the current grouping by functional area (Core, Database, R2, etc.) is arguably more maintainable than pure alphabetical order, bringing keys into alphabetical order within each section would satisfy the linter and improve consistency.
This is a low-priority quality-of-life improvement for a template file.
CLOUDFLARE_README.md (1)
1-50: Markdown formatting: Address linter warnings for documentation consistency.The markdownlint-cli2 flagged several issues:
- Bare URLs (MD034): URLs like line 129 should be wrapped in link format:
[URL description](https://...)- Emphasis as headings (MD036): Lines 160, 167, 173, etc. use
**text:**but should use### Headingformat- Missing language spec (MD040): Some code blocks lack a language specifier (e.g.,
```bashinstead of```)These are minor style issues but addressing them improves documentation consistency and renders better in markdown viewers.
apps/web/CLOUDFLARE_DEPLOYMENT.md (2)
229-243: Clarify database migration guidance for Cloudflare context.Line 231 mentions "when using the Docker build," which is confusing for users following a Cloudflare-specific deployment guide. Restructure this section to clarify:
- For Docker deployments: automatic on startup
- For Cloudflare deployments: manual via provided commands
Example revision:
## Step 8: Database Migrations **For Docker deployments**: Migrations run automatically on startup. **For Cloudflare deployments**, run migrations manually using one of these approaches: 1. Via pnpm: ```bash cd packages/database pnpm db:push
- Via API endpoint (if migration endpoint is exposed):
curl -X POST https://cap.so/api/selfhosted/migrations--- `1-50`: **Markdown formatting: Address linter warnings (same as CLOUDFLARE_README.md).** This file has the same markdown linting issues as CLOUDFLARE_README.md: - Bare URLs should be wrapped: `[description](URL)` - Emphasis used as headings (e.g., line 186 `**Prerequisites**` should be `### Prerequisites`) - Code blocks should specify language (e.g., ` ```bash ` instead of ` ``` `) Consider doing a documentation style pass across both files to ensure consistency. </blockquote></details> </blockquote></details> <details> <summary>📜 Review details</summary> **Configuration used**: CodeRabbit UI **Review profile**: CHILL **Plan**: Pro <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 85c5d69e833336970a105772085c8149771ec597 and f921e32890f44b0e2cf8e108be78ef26629804dc. </details> <details> <summary>⛔ Files ignored due to path filters (1)</summary> * `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml` </details> <details> <summary>📒 Files selected for processing (13)</summary> * `.env.cloudflare.example` (1 hunks) * `CLOUDFLARE_README.md` (1 hunks) * `apps/tasks/deno.json` (1 hunks) * `apps/tasks/deployctl.json` (1 hunks) * `apps/tasks/src/index-deno.ts` (1 hunks) * `apps/web/.gitignore` (1 hunks) * `apps/web/CLOUDFLARE_DEPLOYMENT.md` (1 hunks) * `apps/web/image-loader.ts` (1 hunks) * `apps/web/next.config.mjs` (2 hunks) * `apps/web/open-next.config.ts` (1 hunks) * `apps/web/package.json` (3 hunks) * `apps/web/wrangler.jsonc` (1 hunks) * `deploy-cloudflare.sh` (1 hunks) </details> <details> <summary>🧰 Additional context used</summary> <details> <summary>📓 Path-based instructions (4)</summary> <details> <summary>**/*.{ts,tsx}</summary> **📄 CodeRabbit inference engine (AGENTS.md)** > `**/*.{ts,tsx}`: Use a 2-space indent for TypeScript code. > Use Biome for formatting and linting TypeScript/JavaScript files by running `pnpm format`. > > Use strict TypeScript and avoid any; leverage shared types Files: - `apps/web/image-loader.ts` - `apps/web/open-next.config.ts` - `apps/tasks/src/index-deno.ts` </details> <details> <summary>**/*.{ts,tsx,js,jsx}</summary> **📄 CodeRabbit inference engine (AGENTS.md)** > `**/*.{ts,tsx,js,jsx}`: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., `user-menu.tsx`). > Use PascalCase for React/Solid components. Files: - `apps/web/image-loader.ts` - `apps/web/open-next.config.ts` - `apps/tasks/src/index-deno.ts` </details> <details> <summary>apps/web/**/*.{ts,tsx,js,jsx}</summary> **📄 CodeRabbit inference engine (AGENTS.md)** > On the client, always use `useEffectQuery` or `useEffectMutation` from `@/lib/EffectRuntime`; never call `EffectRuntime.run*` directly in components. Files: - `apps/web/image-loader.ts` - `apps/web/open-next.config.ts` </details> <details> <summary>apps/web/**/*.{ts,tsx}</summary> **📄 CodeRabbit inference engine (CLAUDE.md)** > `apps/web/**/*.{ts,tsx}`: Use TanStack Query v5 for all client-side server state and fetching in the web app > Mutations should call Server Actions directly and perform targeted cache updates with setQueryData/setQueriesData > Run server-side effects via the ManagedRuntime from apps/web/lib/server.ts using EffectRuntime.runPromise/runPromiseExit; do not create runtimes ad hoc > Client code should use helpers from apps/web/lib/EffectRuntime.ts (useEffectQuery, useEffectMutation, useRpcClient); never call ManagedRuntime.make inside components Files: - `apps/web/image-loader.ts` - `apps/web/open-next.config.ts` </details> </details><details> <summary>🪛 dotenv-linter (4.0.0)</summary> <details> <summary>.env.cloudflare.example</summary> [warning] 7-7: [UnorderedKey] The NEXTAUTH_SECRET key should go before the NODE_ENV key (UnorderedKey) --- [warning] 8-8: [UnorderedKey] The NEXTAUTH_URL key should go before the NODE_ENV key (UnorderedKey) --- [warning] 12-12: [UnorderedKey] The DATABASE_ENCRYPTION_KEY key should go before the DATABASE_URL key (UnorderedKey) --- [warning] 18-18: [UnorderedKey] The CAP_AWS_ACCESS_KEY key should go before the CAP_AWS_BUCKET key (UnorderedKey) --- [warning] 21-21: [UnorderedKey] The CAP_AWS_ENDPOINT key should go before the CAP_AWS_REGION key (UnorderedKey) --- [warning] 23-23: [UnorderedKey] The CAP_AWS_BUCKET_URL key should go before the CAP_AWS_ENDPOINT key (UnorderedKey) --- [warning] 25-25: [UnorderedKey] The S3_INTERNAL_ENDPOINT key should go before the S3_PUBLIC_ENDPOINT key (UnorderedKey) --- [warning] 26-26: [UnorderedKey] The S3_PATH_STYLE key should go before the S3_PUBLIC_ENDPOINT key (UnorderedKey) --- [warning] 31-31: [UnorderedKey] The DEEPGRAM_API_KEY key should go before the GROQ_API_KEY key (UnorderedKey) --- [warning] 39-39: [UnorderedKey] The NEXT_PUBLIC_POSTHOG_HOST key should go before the NEXT_PUBLIC_POSTHOG_KEY key (UnorderedKey) --- [warning] 46-46: [UnorderedKey] The WORKOS_API_KEY key should go before the WORKOS_CLIENT_ID key (UnorderedKey) --- [warning] 50-50: [UnorderedKey] The STRIPE_SECRET_KEY_LIVE key should go before the STRIPE_SECRET_KEY_TEST key (UnorderedKey) --- [warning] 59-59: [UnorderedKey] The AXIOM_DATASET key should go before the AXIOM_DOMAIN key (UnorderedKey) </details> </details> <details> <summary>🪛 markdownlint-cli2 (0.18.1)</summary> <details> <summary>apps/web/CLOUDFLARE_DEPLOYMENT.md</summary> 129-129: Bare URL used (MD034, no-bare-urls) --- 160-160: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 167-167: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 173-173: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 181-181: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 186-186: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 187-187: Bare URL used (MD034, no-bare-urls) --- 188-188: Bare URL used (MD034, no-bare-urls) --- 195-195: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 237-237: Bare URL used (MD034, no-bare-urls) --- 238-238: Bare URL used (MD034, no-bare-urls) --- 269-269: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 275-275: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 281-281: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 286-286: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 297-297: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 302-302: Emphasis used instead of a heading (MD036, no-emphasis-as-heading) --- 341-341: Bare URL used (MD034, no-bare-urls) --- 342-342: Bare URL used (MD034, no-bare-urls) --- 346-346: Fenced code blocks should have a language specified (MD040, fenced-code-language) </details> </details> </details> <details> <summary>🔇 Additional comments (11)</summary><blockquote> <details> <summary>apps/web/.gitignore (1)</summary><blockquote> `38-42`: **LGTM! Appropriate Cloudflare build artifacts excluded.** The gitignore entries correctly exclude OpenNext build outputs, Wrangler tooling directories, and generated Cloudflare environment files. </blockquote></details> <details> <summary>apps/web/open-next.config.ts (1)</summary><blockquote> `1-5`: **LGTM! Minimal OpenNext Cloudflare configuration is appropriate.** The standard incremental cache setting is a sensible default for Cloudflare deployment. </blockquote></details> <details> <summary>apps/web/image-loader.ts (1)</summary><blockquote> `1-19`: **LGTM! Image loader correctly implements Cloudflare Images integration.** The implementation appropriately: - Bypasses transformation in development - Normalizes paths and constructs valid Cloudflare Images URLs - Uses sensible defaults (quality=75, format=auto) </blockquote></details> <details> <summary>apps/web/next.config.mjs (2)</summary><blockquote> `13-16`: **LGTM! Cloudflare development initialization is appropriately gated.** The conditional import and initialization only runs when explicitly enabled via ENABLE_CLOUDFLARE_DEV, preventing unintended behavior in other environments. --- `46-47`: **LGTM! Custom image loader configuration is correct.** The loader configuration properly references the Cloudflare image loader implementation at ./image-loader.ts. </blockquote></details> <details> <summary>apps/tasks/deployctl.json (1)</summary><blockquote> `1-5`: **LGTM! Deno Deploy configuration is correct.** The deployment config appropriately specifies the entrypoint and includes the necessary source files. </blockquote></details> <details> <summary>apps/web/wrangler.jsonc (1)</summary><blockquote> `17-17`: **No action required—deployment automation already handles Hyperdrive ID replacement.** The deploy-cloudflare.sh script explicitly checks for the placeholder "your-hyperdrive-id-here" in wrangler.jsonc and replaces it with the value from the $HYPERDRIVE_ID environment variable using sed. The script includes proper error handling and user feedback, confirming that deployment automation clearly manages this placeholder replacement. </blockquote></details> <details> <summary>apps/web/package.json (3)</summary><blockquote> `7-8`: **Turbopack removal aligns with Next.js 15 recommendations.** Build commands now use standard `next build` without `--turbopack`. This is appropriate given that Turbopack for production builds remains in active development; development mode (`next dev --turbo`) is the stable offering in Next.js 15. --- `10-14`: **New Cloudflare-specific scripts are well-named and properly configured.** The scripts follow established naming conventions (`build:cloudflare`, `preview:cloudflare`, `deploy:cloudflare`) and correctly invoke the `@opennextjs/cloudflare` adapter. The `cf-typegen` script for generating Cloudflare environment types is a useful addition for development ergonomics. --- `139-139`: **Verify wrangler placement: should it be devDependency-only?** Line 158 adds `wrangler` to `devDependencies`, which is correct since it's used during build and deployment steps. However, per the AI summary, wrangler also appears in the dependencies section. Wrangler is a CLI tool used at build/deploy time, not a runtime dependency. Confirm it should not be listed in the main `dependencies` section to avoid inflating the production bundle. Also applies to: 158-158 </blockquote></details> <details> <summary>deploy-cloudflare.sh (1)</summary><blockquote> `14-28`: **Prerequisite validation is well-implemented.** The `check_command()` function provides clear error messaging and early exit behavior, ensuring all required tools are available before proceeding with the deployment workflow. </blockquote></details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| "dev": "deno run --allow-all --watch src/index-deno.ts", | ||
| "start": "deno run --allow-all src/index-deno.ts" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace --allow-all with specific permissions (principle of least privilege).
Using --allow-all grants all permissions, which is a security risk. Specify only the permissions actually needed by the application.
"tasks": {
- "dev": "deno run --allow-all --watch src/index-deno.ts",
- "start": "deno run --allow-all src/index-deno.ts"
+ "dev": "deno run --allow-net --allow-read --allow-write --allow-run=ffmpeg --allow-env --watch src/index-deno.ts",
+ "start": "deno run --allow-net --allow-read --allow-write --allow-run=ffmpeg --allow-env src/index-deno.ts"
},These permissions grant:
--allow-net: Network access for HTTP server and fetch--allow-read: Read files (merged audio output)--allow-write: Write files (temporary output directory)--allow-run=ffmpeg: Execute only FFmpeg binary--allow-env: Access environment variables (PORT)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "dev": "deno run --allow-all --watch src/index-deno.ts", | |
| "start": "deno run --allow-all src/index-deno.ts" | |
| "dev": "deno run --allow-net --allow-read --allow-write --allow-run=ffmpeg --allow-env --watch src/index-deno.ts", | |
| "start": "deno run --allow-net --allow-read --allow-write --allow-run=ffmpeg --allow-env src/index-deno.ts" |
🤖 Prompt for AI Agents
In apps/tasks/deno.json around lines 3 to 4, the scripts use --allow-all which
is overly permissive; replace it with the minimal required permissions: use
--allow-net --allow-read --allow-write --allow-run=ffmpeg --allow-env for both
the "dev" and "start" commands so the app only has network, file read/write,
execution of ffmpeg, and environment access.
|
|
||
| const app = new Hono(); | ||
|
|
||
| app.use("*", cors()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restrict CORS to specific origins.
The wildcard CORS policy (cors() with no options) allows requests from any origin, which is a security risk for a production API.
-app.use("*", cors());
+app.use("*", cors({
+ origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
+ credentials: true,
+}));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| app.use("*", cors()); | |
| app.use("*", cors({ | |
| origin: process.env.ALLOWED_ORIGINS?.split(',') || [], | |
| credentials: true, | |
| })); |
🤖 Prompt for AI Agents
In apps/tasks/src/index-deno.ts around line 6, the code mounts a wildcard CORS
middleware (app.use("*", cors())) which allows any origin; replace it with a
CORS configuration that restricts allowed origins to a safe list. Update the
middleware to read allowed origins from configuration or environment, validate
and normalize that list, and pass it into cors({ origin: (requestOrigin) =>
allowList.includes(requestOrigin) }) or equivalent option supported by the CORS
library so only specified origins are permitted; ensure preflight and
credentials options match your API requirements.
| if ( | ||
| !body.segments || | ||
| body.segments.length === 0 || | ||
| !body.uploadUrl || | ||
| !body.videoId | ||
| ) { | ||
| return c.json({ response: "FAILED" }, 400); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comprehensive input validation.
The current validation only checks for presence but doesn't validate:
- URL format and protocol of segments and uploadUrl
- Array size limits for segments
- String format and sanitization for videoId
This creates security risks including command injection and path traversal.
if (
!body.segments ||
body.segments.length === 0 ||
!body.uploadUrl ||
!body.videoId
) {
return c.json({ response: "FAILED" }, 400);
}
+
+ // Validate segments array size
+ if (body.segments.length > 100) {
+ return c.json({ response: "FAILED", error: "Too many segments" }, 400);
+ }
+
+ // Validate all segment URLs
+ const urlPattern = /^https?:\/\/.+/;
+ for (const url of body.segments) {
+ if (typeof url !== 'string' || !urlPattern.test(url)) {
+ return c.json({ response: "FAILED", error: "Invalid segment URL" }, 400);
+ }
+ }
+
+ // Validate upload URL
+ if (typeof body.uploadUrl !== 'string' || !urlPattern.test(body.uploadUrl)) {
+ return c.json({ response: "FAILED", error: "Invalid upload URL" }, 400);
+ }
+
+ // Sanitize videoId to prevent path traversal
+ if (typeof body.videoId !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(body.videoId)) {
+ return c.json({ response: "FAILED", error: "Invalid videoId" }, 400);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if ( | |
| !body.segments || | |
| body.segments.length === 0 || | |
| !body.uploadUrl || | |
| !body.videoId | |
| ) { | |
| return c.json({ response: "FAILED" }, 400); | |
| } | |
| if ( | |
| !body.segments || | |
| body.segments.length === 0 || | |
| !body.uploadUrl || | |
| !body.videoId | |
| ) { | |
| return c.json({ response: "FAILED" }, 400); | |
| } | |
| // Validate segments array size | |
| if (body.segments.length > 100) { | |
| return c.json({ response: "FAILED", error: "Too many segments" }, 400); | |
| } | |
| // Validate all segment URLs | |
| const urlPattern = /^https?:\/\/.+/; | |
| for (const url of body.segments) { | |
| if (typeof url !== 'string' || !urlPattern.test(url)) { | |
| return c.json({ response: "FAILED", error: "Invalid segment URL" }, 400); | |
| } | |
| } | |
| // Validate upload URL | |
| if (typeof body.uploadUrl !== 'string' || !urlPattern.test(body.uploadUrl)) { | |
| return c.json({ response: "FAILED", error: "Invalid upload URL" }, 400); | |
| } | |
| // Sanitize videoId to prevent path traversal | |
| if (typeof body.videoId !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(body.videoId)) { | |
| return c.json({ response: "FAILED", error: "Invalid videoId" }, 400); | |
| } |
🤖 Prompt for AI Agents
In apps/tasks/src/index-deno.ts around lines 15 to 22, the current input check
only ensures presence of segments, uploadUrl, and videoId; add comprehensive
validation: verify body.segments is an array with a reasonable length limit
(e.g., 1..100) and each segment is a string matching a strict URL pattern
(require https?:// and valid hostname/path), validate body.uploadUrl as a
well-formed absolute URL with allowed protocols (https and optionally http),
ensure body.videoId matches your expected pattern (e.g., alphanumeric, length
bounds, no path separators) and sanitize/normalize it, reject or trim unexpected
properties, and return 400 with a clear error message when validation fails; use
well-tested URL parsing utilities or a schema validator (zod/joi/validator) to
enforce rules and centralize validation logic.
| const command = new Deno.Command("ffmpeg", { | ||
| args: ffmpegArgs, | ||
| stdout: "piped", | ||
| stderr: "piped", | ||
| }); | ||
|
|
||
| const process = command.spawn(); | ||
| const { code } = await process.status; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consume piped stdout/stderr to prevent deadlock.
When stdout and stderr are piped but never read, FFmpeg can block when the pipe buffers fill up, causing a deadlock. Either consume the streams or pipe to "inherit".
const command = new Deno.Command("ffmpeg", {
args: ffmpegArgs,
- stdout: "piped",
- stderr: "piped",
+ stdout: "inherit",
+ stderr: "inherit",
});
const process = command.spawn();
const { code } = await process.status;Alternatively, if you need to capture output:
const command = new Deno.Command("ffmpeg", {
args: ffmpegArgs,
stdout: "piped",
stderr: "piped",
});
const { code, stdout, stderr } = await command.output();
if (code !== 0) {
console.error("FFmpeg failed:", new TextDecoder().decode(stderr));
return c.json({ response: "FAILED" }, 500);
}🤖 Prompt for AI Agents
In apps/tasks/src/index-deno.ts around lines 48 to 55, stdout and stderr are set
to "piped" for the spawned ffmpeg process but never consumed, risking a deadlock
when pipe buffers fill; either change stdout/stderr to "inherit" to let the
child write directly to the parent, or read the pipes (e.g., use
command.output() to get code, stdout, stderr or await process.output()/readable
streams) and handle/log stderr on non‑zero exit before returning the response.
| const command = new Deno.Command("ffmpeg", { | ||
| args: ffmpegArgs, | ||
| stdout: "piped", | ||
| stderr: "piped", | ||
| }); | ||
|
|
||
| const process = command.spawn(); | ||
| const { code } = await process.status; | ||
|
|
||
| if (code !== 0) { | ||
| console.error("FFmpeg failed with code:", code); | ||
| return c.json({ response: "FAILED" }, 500); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add timeout for FFmpeg execution.
Long-running FFmpeg processes can exhaust server resources. Add a timeout to prevent runaway processes.
+ const FFMPEG_TIMEOUT = 300000; // 5 minutes
+
+ const timeoutPromise = new Promise((_, reject) => {
+ setTimeout(() => reject(new Error("FFmpeg timeout")), FFMPEG_TIMEOUT);
+ });
+
+ const processPromise = (async () => {
- const process = command.spawn();
- const { code } = await process.status;
+ const process = command.spawn();
+ const { code } = await process.status;
+ return code;
+ })();
+
+ let code: number;
+ try {
+ code = await Promise.race([processPromise, timeoutPromise]);
+ } catch (error) {
+ console.error("FFmpeg timeout or error:", error);
+ return c.json({ response: "FAILED", error: "Processing timeout" }, 500);
+ }
if (code !== 0) {
console.error("FFmpeg failed with code:", code);
return c.json({ response: "FAILED" }, 500);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const command = new Deno.Command("ffmpeg", { | |
| args: ffmpegArgs, | |
| stdout: "piped", | |
| stderr: "piped", | |
| }); | |
| const process = command.spawn(); | |
| const { code } = await process.status; | |
| if (code !== 0) { | |
| console.error("FFmpeg failed with code:", code); | |
| return c.json({ response: "FAILED" }, 500); | |
| } | |
| const command = new Deno.Command("ffmpeg", { | |
| args: ffmpegArgs, | |
| stdout: "piped", | |
| stderr: "piped", | |
| }); | |
| const FFMPEG_TIMEOUT = 300000; // 5 minutes | |
| const timeoutPromise = new Promise((_, reject) => { | |
| setTimeout(() => reject(new Error("FFmpeg timeout")), FFMPEG_TIMEOUT); | |
| }); | |
| const processPromise = (async () => { | |
| const process = command.spawn(); | |
| const { code } = await process.status; | |
| return code; | |
| })(); | |
| let code: number; | |
| try { | |
| code = await Promise.race([processPromise, timeoutPromise]); | |
| } catch (error) { | |
| console.error("FFmpeg timeout or error:", error); | |
| return c.json({ response: "FAILED", error: "Processing timeout" }, 500); | |
| } | |
| if (code !== 0) { | |
| console.error("FFmpeg failed with code:", code); | |
| return c.json({ response: "FAILED" }, 500); | |
| } |
🤖 Prompt for AI Agents
In apps/tasks/src/index-deno.ts around lines 48 to 60, the spawned FFmpeg
process currently waits indefinitely for process.status; add a timeout to avoid
runaway processes by racing the process.status promise against a timeout Promise
(configurable, e.g. 30s), and if the timeout wins kill the child (process.kill
or process.kill("SIGKILL")), await process.status to clean up streams,
collect/close stdout/stderr, and return an appropriate error response (e.g. 504
or FAILED) so resources are freed; ensure the timeout is cleared if the process
finishes first.
| ## Step 5: Set Up Tasks Service on Cloudflare Containers | ||
|
|
||
| The tasks service requires FFmpeg, so it must run on Cloudflare Containers (not standard Workers). | ||
|
|
||
| ### Create Dockerfile for Tasks Service | ||
|
|
||
| See `apps/tasks/Dockerfile.cloudflare` (created by setup script). | ||
|
|
||
| ### Deploy to Cloudflare Containers | ||
|
|
||
| ```bash | ||
| cd apps/tasks | ||
|
|
||
| # Build and deploy | ||
| wrangler deploy | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Inconsistent tasks service deployment instructions across documentation.
Step 5 states the tasks service must run on "Cloudflare Containers" (line 163) but the deployment command at line 175 uses wrangler deploy, which is for Workers, not Containers. This conflicts with:
deploy-cloudflare.shwhich usesdeployctlfor tasks (line 116)CLOUDFLARE_README.mdwhich usespnpm build:cloudflare+pnpm deploy:cloudflarefor the web app
Additionally, the reference to apps/tasks/Dockerfile.cloudflare (line 167) is unclear—it's unclear whether Deno Deploy or Cloudflare Containers is the intended platform, and the Dockerfile may not exist.
Clarify whether tasks should deploy to:
- Deno Deploy (as per deploy-cloudflare.sh line 116 via
deployctl) - Cloudflare Containers (requires containerization and different CLI)
- Cloudflare Workers (standard workers deployment)
Once clarified, align all three documentation files (CLOUDFLARE_README.md, CLOUDFLARE_DEPLOYMENT.md, deploy-cloudflare.sh) to use consistent instructions.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
167-167: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
173-173: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🤖 Prompt for AI Agents
In apps/web/CLOUDFLARE_DEPLOYMENT.md around lines 161 to 177, the instructions
conflict about where the tasks service should run (it says "Cloudflare
Containers" but uses `wrangler deploy`) and reference a potentially missing
`apps/tasks/Dockerfile.cloudflare`; update this section to clearly state the
intended target (pick one of: Deno Deploy, Cloudflare Containers, or Cloudflare
Workers), confirm whether `apps/tasks/Dockerfile.cloudflare` exists or
remove/replace the reference, and change the deploy commands to the correct CLI
(e.g., use `deployctl` for Deno Deploy, container build + Cloudflare Containers
CLI for Containers, or `wrangler deploy` for Workers). After that, make matching
edits in CLOUDFLARE_README.md and deploy-cloudflare.sh so all three artifacts
use the same platform, commands, and file references.
| cd apps/web | ||
|
|
||
| if grep -q "\"id\": \"your-hyperdrive-id-here\"" wrangler.jsonc; then | ||
| sed -i.bak "s/your-hyperdrive-id-here/$HYPERDRIVE_ID/g" wrangler.jsonc | ||
| rm wrangler.jsonc.bak | ||
| echo -e "${GREEN}✓ Updated wrangler.jsonc${NC}" | ||
| else | ||
| echo -e "${YELLOW}Hyperdrive already configured or not found in template${NC}" | ||
| fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: sed command will fail on macOS; platform-specific compatibility issue.
Line 81 uses sed -i.bak, which is GNU sed syntax. BSD sed (default on macOS) requires sed -i ''. This script will fail on macOS systems during Hyperdrive configuration.
Additionally, no validation is performed to confirm the placeholder exists in wrangler.jsonc before sed executes. If the file has already been updated or the placeholder is missing, sed will silently succeed without making any changes, leading to deployment failures.
Apply this diff to fix the platform compatibility and add validation:
-if grep -q "\"id\": \"your-hyperdrive-id-here\"" wrangler.jsonc; then
- sed -i.bak "s/your-hyperdrive-id-here/$HYPERDRIVE_ID/g" wrangler.jsonc
- rm wrangler.jsonc.bak
- echo -e "${GREEN}✓ Updated wrangler.jsonc${NC}"
-else
- echo -e "${YELLOW}Hyperdrive already configured or not found in template${NC}"
-fi
+if [ ! -f "wrangler.jsonc" ]; then
+ echo -e "${RED}Error: wrangler.jsonc not found in $(pwd)${NC}"
+ exit 1
+fi
+
+if grep -q "your-hyperdrive-id-here" wrangler.jsonc; then
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "s/your-hyperdrive-id-here/$HYPERDRIVE_ID/g" wrangler.jsonc
+ else
+ sed -i "s/your-hyperdrive-id-here/$HYPERDRIVE_ID/g" wrangler.jsonc
+ fi
+ echo -e "${GREEN}✓ Updated wrangler.jsonc${NC}"
+else
+ echo -e "${YELLOW}Hyperdrive placeholder not found in wrangler.jsonc${NC}"
+fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cd apps/web | |
| if grep -q "\"id\": \"your-hyperdrive-id-here\"" wrangler.jsonc; then | |
| sed -i.bak "s/your-hyperdrive-id-here/$HYPERDRIVE_ID/g" wrangler.jsonc | |
| rm wrangler.jsonc.bak | |
| echo -e "${GREEN}✓ Updated wrangler.jsonc${NC}" | |
| else | |
| echo -e "${YELLOW}Hyperdrive already configured or not found in template${NC}" | |
| fi | |
| cd apps/web | |
| if [ ! -f "wrangler.jsonc" ]; then | |
| echo -e "${RED}Error: wrangler.jsonc not found in $(pwd)${NC}" | |
| exit 1 | |
| fi | |
| if grep -q "your-hyperdrive-id-here" wrangler.jsonc; then | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| sed -i '' "s/your-hyperdrive-id-here/$HYPERDRIVE_ID/g" wrangler.jsonc | |
| else | |
| sed -i "s/your-hyperdrive-id-here/$HYPERDRIVE_ID/g" wrangler.jsonc | |
| fi | |
| echo -e "${GREEN}✓ Updated wrangler.jsonc${NC}" | |
| else | |
| echo -e "${YELLOW}Hyperdrive placeholder not found in wrangler.jsonc${NC}" | |
| fi |
🤖 Prompt for AI Agents
In deploy-cloudflare.sh around lines 78-86, the sed invocation is not
macOS-compatible and there's insufficient validation around the placeholder
replacement; replace the platform-specific sed -i.bak with a portable approach
(use a temp file: create a mktemp, run sed without -i writing to the temp file,
then mv the temp back) or use perl -pi -e which works cross-platform, and keep
the existing grep check but add a post-replacement validation step that re-greps
for the placeholder and errors/exit nonzero if the placeholder still exists or
the file is missing to prevent silent failures.
| deployctl deploy \ | ||
| --project=cap-tasks \ | ||
| --prod \ | ||
| src/index-deno.ts || echo -e "${YELLOW}Note: First time deployment may require manual project creation in Deno Deploy dashboard${NC}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error handling bypass: deployctl failures may go unnoticed.
Lines 119 and 130 use || to suppress deployctl errors and log a warning instead of exiting. This violates the set -e error-exit policy and allows deployment to continue even if critical steps fail. Users may not realize their tasks or web-cluster services failed to deploy.
Apply this diff to restore proper error handling:
-deployctl deploy \
- --project=cap-tasks \
- --prod \
- src/index-deno.ts || echo -e "${YELLOW}Note: First time deployment may require manual project creation in Deno Deploy dashboard${NC}"
+if ! deployctl deploy \
+ --project=cap-tasks \
+ --prod \
+ src/index-deno.ts; then
+ echo -e "${YELLOW}Note: First time deployment may require manual project creation in Deno Deploy dashboard${NC}"
+ echo -e "${YELLOW}Check https://dash.deno.com/projects/cap-tasks for details${NC}"
+else
+ echo -e "${GREEN}✓ Tasks service deployment initiated${NC}"
+fiRepeat the same pattern for the web-cluster deployment at line 127.
Also applies to: 127-130
🤖 Prompt for AI Agents
In deploy-cloudflare.sh around lines 116-119 and 127-130, the deployctl calls
use "|| echo ..." which suppresses errors and violates the set -e policy; remove
the trailing "|| echo -e ..." and instead ensure failures cause the script to
exit with a non-zero status (for example wrap the deployctl call so that on
failure you print the diagnostic message and then exit 1, or use the short form:
deployctl ... || { echo "Note: ..."; exit 1; } ), apply the same change to both
the src/index-deno.ts and web-cluster deployctl invocations so deploy errors are
not ignored.
Complete deployment setup for Cloudflare infrastructure without Docker:
Web App (Cloudflare Pages):
Tasks Service (Deno Deploy):
Infrastructure:
Documentation:
Benefits:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores