# Deploy Next.js to Cloudflare Workers (via OpenNext)

One-command setup for deploying a Next.js App Router project to Cloudflare Workers using @opennextjs/cloudflare. Handles all config, build scripts, and common pitfalls.

## Usage
Run `/deploy-nextjs-cloudflare` in any Next.js project.

---

$ARGUMENTS

## Instructions

You are setting up Cloudflare Workers deployment for a Next.js App Router project. Follow these steps exactly — every lesson here was learned from real deployment failures.

### Step 1: Install dependencies

```bash
npm install -D @opennextjs/cloudflare wrangler
```

### Step 2: Create `open-next.config.ts` in project root

```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
```

If using R2 for caching, use:
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({ incrementalCache: r2IncrementalCache });
```

### Step 3: Create/update `wrangler.toml`

```toml
name = "PROJECT_NAME"
main = ".open-next/worker.js"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]

[assets]
directory = ".open-next/assets"
binding = "ASSETS"
```

Add any bindings (R2, D1, KV) below the base config.

### Step 4: Configure package.json scripts

⚠️ CRITICAL: The `build` script MUST be `next build`, NOT `opennextjs-cloudflare build`.
If build calls opennextjs-cloudflare, and opennextjs-cloudflare internally calls `npm run build`, you get INFINITE RECURSION that crashes the Cloudflare build server.

```json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "build:cf": "opennextjs-cloudflare build",
    "deploy": "opennextjs-cloudflare deploy",
    "start": "next start"
  }
}
```

### Step 5: Update next.config

If using native Node modules (like better-sqlite3), add to next.config:
```ts
serverExternalPackages: ["better-sqlite3"]
```

### Step 6: Cloudflare Dashboard Configuration

In Cloudflare Dashboard → Workers → your worker → Settings → Builds → Build configuration:

| Setting | Value |
|---------|-------|
| **Build command** | `npx opennextjs-cloudflare build` |
| **Deploy command** | `npx opennextjs-cloudflare deploy` |
| **Root directory** | `/` |

⚠️ Do NOT set build command to `npm run build` — that runs `next build` alone without the OpenNext packaging step.
⚠️ Do NOT set deploy command to `npx wrangler deploy` — wrangler doesn't know about the .open-next output structure.

Also configure:
- **Compatibility flags**: Add `nodejs_compat` (also in wrangler.toml, but belt-and-suspenders)
- **Variables and secrets**: Add your env vars (DATABASE_URL, NEXTAUTH_SECRET, etc.)

### Step 7: Connect Git repository

In the Cloudflare dashboard:
1. Workers → your worker → Settings → Builds
2. Connect your GitHub/GitLab repo
3. Set production branch to `main`
4. Every push to main auto-deploys. PR branches get preview deployments.

No GitHub Actions needed — Cloudflare Workers has native Git integration.

### Step 8: Sync lockfile and push

```bash
npm install  # ensures lockfile is in sync
git add -A
git commit -m "Configure Cloudflare Workers deployment via OpenNext"
git push origin main
```

### Step 9: Set Worker secrets via CLI

Environment variables set in the dashboard Builds section are only available during build.
For RUNTIME secrets, use `wrangler secret put`:

```bash
echo "your-db-url" | npx wrangler secret put DATABASE_URL --name YOUR_WORKER_NAME
echo "your-auth-secret" | npx wrangler secret put NEXTAUTH_SECRET --name YOUR_WORKER_NAME
echo "https://your-domain.workers.dev" | npx wrangler secret put NEXTAUTH_URL --name YOUR_WORKER_NAME
```

### Step 10: Add .open-next to .gitignore

```
# In .gitignore:
.open-next/
```

The `.open-next/` directory is the build output (thousands of files). Never commit it.

### Step 11: Handle RSC boundaries

Cloudflare Workers is strict about React Server Components. Common errors:
- **"Functions cannot be passed directly to Client Components"**: Add `"use client"` to any layout or page that imports MUI components or wraps children in Context providers.
- **better-sqlite3 crash**: Never import `better-sqlite3` in middleware or edge code. Keep middleware simple (i18n only). Do auth checks in route handlers instead.
- **"use client" needed for**: Any file that uses `useTranslations`, MUI components (`Box`, `Button`, etc.), React hooks, or Context providers.

### Step 12: R2 Storage (if using audio/media files)

**CRITICAL WRANGLER GOTCHA**: `wrangler r2 object put` defaults to **LOCAL** storage. Always use `--remote`:
```bash
# WRONG — uploads to local emulator only:
npx wrangler r2 object put "bucket/key" --file="file.mp3"

# CORRECT — uploads to actual Cloudflare R2:
npx wrangler r2 object put "bucket/key" --file="file.mp3" --remote
```

**R2 binding vs S3 API credentials**:
- The `R2_BUCKET` binding in `wrangler.toml` is NOT accessible from Next.js API routes via OpenNext
- For API routes to access R2, you need either:
  1. **Public R2 access** (simplest for previews) — enable via Cloudflare API or dashboard
  2. **S3 API credentials** — create from dashboard at `/r2/api-tokens`, set as Worker secrets

**Enable public R2 access via API**:
```bash
TOKEN=$(grep -oP 'oauth_token = "\K[^"]+' ~/.config/.wrangler/config/default.toml)
ACCOUNT_ID="your-account-id"
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets/BUCKET_NAME/domains/managed" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'
```
This returns a `pub-*.r2.dev` URL. Use it to serve files publicly (good for audio previews).

**Audio preview pattern**:
- API route looks up track's R2 key from DB
- Redirects (302) to the public R2 URL
- Client audio element streams from R2 directly
- No presigned URLs needed for public preview content

### Step 13: Bundle Size Optimization (CF Workers 3MB limit)

The CF Workers free tier enforces a **3MB gzip limit on the server-side JS bundle**. The client-side JS goes to static assets and is NOT counted. Only server-side code counts.

**What blows up the worker bundle:**
- `@mui/icons-material` — even tree-shaken, SVG path data leaks server-side. **Replace with `lucide-react`** (tiny, perfect tree-shaking). Migration: `import SearchIcon from "@mui/icons-material/Search"` → `import { Search as SearchIcon } from "lucide-react"` (keep alias so no JSX changes needed). Note: lucide icons use `size={N}` not `fontSize` or `sx` for sizing.
- `sharp` — native Node binary, **cannot run on CF Workers at all** (silent runtime crash). Replace with **`@cf-wasm/photon`** (purpose-built WASM for CF Workers). Use `exifr` for EXIF orientation. API: `resize(img, w, h, SamplingFilter.Lanczos3)` returns new image; `fliph(img)`/`flipv(img)` mutate in place; `rotate(img, degrees)` returns new image; always call `img.free()` after use.
- `motion` / `framer-motion` — only use `useInView`? Replace with 5-line native `IntersectionObserver` hook. Entire package gone.
- `recharts`, `lamejs`, and other heavy client libs — if they have `"use client"` they won't be in the server bundle. Confirm with bundle analyzer. If accidentally server-side, wrap with `next/dynamic(() => import(...), { ssr: false })`.

**The 3MB architecture rule:**
- Client components (`"use client"`) → go to browser bundle (static assets, not counted)
- Server components → go to worker bundle (counted toward 3MB)
- API routes → go to worker bundle (counted)
- Enforce RSC discipline: data fetching in server components, heavy UI in client components

**If you hit the limit and can't reduce further:**
- Split the app into two workers (each gets its own 3MB budget) — only worth it as a last resort, triples maintenance burden
- Consider Vercel Pro ($20/month) — no bundle size limit, native Next.js support, but requires paid plan for commercial apps. Keep Durable Objects/ticketing on CF.

**Bundle analysis:**
```bash
ANALYZE=true next build  # requires @next/bundle-analyzer configured in next.config
```

### Common Pitfalls Checklist

- [ ] `open-next.config.ts` exists in project root (NOT in src/)
- [ ] `wrangler.toml` has `main = ".open-next/worker.js"` and `[assets] directory = ".open-next/assets"`
- [ ] `package.json` build script is `next build` (NOT `opennextjs-cloudflare build`)
- [ ] Cloudflare build command is `npx opennextjs-cloudflare build` (NOT `npm run build`)
- [ ] Cloudflare deploy command is `npx opennextjs-cloudflare deploy` (NOT `npx wrangler deploy`)
- [ ] `nodejs_compat` flag is set
- [ ] `package-lock.json` is committed and in sync with `package.json`
- [ ] Build-time env vars are in Cloudflare dashboard Builds section
- [ ] Runtime secrets are set via `wrangler secret put` (NOT just dashboard)
- [ ] `.open-next/` is in `.gitignore`
- [ ] Layouts/pages that use MUI or providers have `"use client"`
- [ ] Middleware does NOT import `better-sqlite3` or heavy Node modules
- [ ] Auth checks are in route handlers, NOT middleware (on Workers)

### Verification

After deployment, verify:
1. Visit your `*.workers.dev` URL
2. Check that locale routing works (e.g., `/he`, `/en`)
3. Check API routes (e.g., `/api/tracks`)
4. Check the Cloudflare dashboard logs for any runtime errors

If you see "hello world" instead of your app, the deploy command is wrong (it deployed a bare worker, not the OpenNext output).
