Cloudflare Deployment
Deploy apps to Cloudflare Workers with custom domains
This guide covers deploying the apps to Cloudflare Workers with custom domains on supaseat.com.
Architecture
| App | Worker | Custom Domain | Production URL |
|---|---|---|---|
apps/server | supaseat-api | api.supaseat.com | https://supaseat-api.supaseat-950.workers.dev |
apps/web | supaseat-web | app.supaseat.com | https://supaseat-web.supaseat-950.workers.dev |
apps/fumadocs | supaseat-docs | docs.supaseat.com | https://supaseat-docs.supaseat-950.workers.dev |
Staging/preview environments use -staging or -preview suffixes.
Prerequisites
- Cloudflare account with
supaseat.comdomain - Wrangler CLI authenticated:
wrangler login - Dependencies installed:
bun install
Environment Bindings
| Worker | Binding | Type | Source |
|---|---|---|---|
supaseat-api | DATABASE_URL | Secret | Neon Postgres connection string |
BETTER_AUTH_SECRET | Secret | Better Auth secret (min 32 chars) | |
BETTER_AUTH_URL | Secret | https://api.supaseat.com | |
CORS_ORIGIN | Secret | Comma-separated list (include both https://app.supaseat.com and https://supaseat.com) | |
NODE_ENV | Var | Defined in wrangler.jsonc per environment | |
ECB_ENABLED | Var | Set to "true" unless FX sync is paused | |
OXR_APP_ID (optional) | Secret | Open Exchange Rates App ID | |
FRED_API_KEY (optional) | Secret | FRED API key | |
supaseat-web | NEXT_PUBLIC_SERVER_URL | Var | API origin for browser requests |
All secrets live in Cloudflare via wrangler secret put .... Non-sensitive vars stay in wrangler.jsonc so they deploy with the worker.
Configure Secrets
Set secrets for both production and staging environments:
cd apps/server
# Production secrets
wrangler secret put DATABASE_URL --name supaseat-api
wrangler secret put BETTER_AUTH_SECRET --name supaseat-api
wrangler secret put BETTER_AUTH_URL --name supaseat-api # https://api.supaseat.com
wrangler secret put CORS_ORIGIN --name supaseat-api # https://app.supaseat.com,https://supaseat.com
wrangler secret put OXR_APP_ID --name supaseat-api # Optional
wrangler secret put FRED_API_KEY --name supaseat-api # Optional
# Staging secrets (target the staging worker name)
wrangler secret put DATABASE_URL --name supaseat-api-staging
wrangler secret put BETTER_AUTH_SECRET --name supaseat-api-staging
wrangler secret put BETTER_AUTH_URL --name supaseat-api-staging
wrangler secret put CORS_ORIGIN --name supaseat-api-staging # https://staging.supaseat.pages.dev,https://supaseat-web-preview.supaseat-950.workers.dev
wrangler secret put OXR_APP_ID --name supaseat-api-staging
wrangler secret put FRED_API_KEY --name supaseat-api-stagingVerify secrets are set:
wrangler secret list
wrangler secret list --env stagingDeploy
Deploy Order
Deploy in order: Server → Web (the web app depends on the API URL).
Using Turbo (recommended)
From the repo root:
bun run deploy:server
bun run deploy:webManual Deployment
# 1. Server (Hono API)
cd apps/server && wrangler deploy
# 2. Web (Next.js)
cd apps/web && wrangler deployDeploy to Staging
# Server staging
cd apps/server && wrangler deploy --env staging
# Web preview
cd apps/web && wrangler deploy --env previewBundle Size Limits
Cloudflare Workers have bundle size limits:
| Plan | Gzip Limit | Uncompressed Limit |
|---|---|---|
| Free | 3 MB | 64 MB |
| Paid | 10 MB | 64 MB |
Check bundle size with a dry run:
wrangler deploy --outdir bundled/ --dry-run
# Output: Total Upload: 12470.52 KiB / gzip: 2827.70 KiBWeb App Bundle Optimization
The web app must stay under 3MB gzip for the free tier. Key optimizations:
-
Don't import server-side auth in the frontend - The
@2026-supaseat/authpackage imports Prisma, adding ~2MB to the bundle. -
Use API calls for session checks - Instead of importing
authdirectly, the web app uses@/lib/auth-server.tswhich fetches the session from the API:// apps/web/src/lib/auth-server.ts export async function getSession(): Promise<Session | null> { const response = await fetch(`${env.NEXT_PUBLIC_SERVER_URL}/api/auth/get-session`, { headers: { cookie: headersList.get("cookie") ?? "" }, cache: "no-store", }); return response.ok ? response.json() : null; } -
Type-only imports - Use
import type { ... }for types from server packages to ensure tree-shaking. -
Don't include
@prisma/clientin web dependencies - It's only needed on the server.
Cron Triggers
The API worker has scheduled cron triggers for FX rate fetching:
| Cron | Time (UTC) | Job | Requires |
|---|---|---|---|
0 6 * * * | 06:00 | ECB rates | - |
0 7 * * * | 07:00 | OXR rates | OXR_APP_ID |
0 11 * * * | 11:00 | FRED rates | FRED_API_KEY |
Note: The free tier allows 5 cron triggers total across all workers. Production uses 3, so staging has crons disabled (triggers.crons: [] in the staging env config).
View triggers in Cloudflare Dashboard: Workers → supaseat-api → Triggers
Available Scripts
Root (package.json)
| Script | Description |
|---|---|
deploy:server | Deploy API to production |
deploy:web | Deploy web app to production |
deploy:docs | Deploy docs to production |
Server (apps/server/package.json)
| Script | Description |
|---|---|
preview | Run locally with Wrangler |
deploy | Deploy to production |
deploy:staging | Deploy to staging env |
Web (apps/web/package.json)
| Script | Description |
|---|---|
preview | Build + run locally (production config) |
preview:staging | Build + run locally (staging config) |
deploy | Build + deploy to production |
deploy:staging | Build + deploy to staging env |
cf-typegen | Generate CloudflareEnv TypeScript types |
Monitoring
Logs
# Tail production logs
wrangler tail supaseat-api
# Tail with filters
wrangler tail supaseat-api --status errorObservability
All workers have observability enabled. View metrics in Cloudflare Dashboard: Workers → [worker-name] → Metrics
Verification Checklist
After deployment, verify:
-
https://supaseat-api.supaseat-950.workers.dev/returns "OK" -
https://supaseat-web.supaseat-950.workers.dev/loads the web app - Auth flow works (sign up, sign in, protected routes)
- API calls from web to server work correctly
- Cron triggers appear in dashboard
Troubleshooting
Environment Validation Errors at Bundle Time
If you see validation errors during wrangler deploy like "Invalid environment variables", it's because the env validation runs at bundle time when secrets aren't available.
Fix: The packages/env/src/server.ts uses skipValidation: true for the initial module load. Validation happens at runtime when loadServerEnv() is called with the actual bindings.
Bundle Too Large
If you see "Your Worker exceeded the size limit of 3 MiB":
-
Check what's in the bundle:
wrangler deploy --outdir bundled/ --dry-run du -sh bundled/* -
Common culprits:
@prisma/clientin frontend dependencies- Importing server-side auth module directly
- Large icon libraries not tree-shaken
- WASM files from
@vercel/og(resvg.wasm ~1.4MB)
-
Solutions:
- Use type-only imports:
import type { ... } - Fetch session from API instead of importing auth
- Remove unused dependencies
- Use type-only imports:
Custom Domain DNS Conflicts
If deployment fails with "A]DNS record already exists" errors, the custom domain has existing DNS records that conflict.
Options:
- Remove the route from
wrangler.jsoncand configure the custom domain manually in Cloudflare Dashboard - Delete the existing DNS record first
- Use the Workers route without custom domain initially
OpenNext Build Issues
If the build command bunx @opennextjs/cloudflare build fails:
# Use the binary directly
./node_modules/.bin/opennextjs-cloudflare build
# Or clear cache and rebuild
rm -rf .open-next .next
./node_modules/.bin/opennextjs-cloudflare buildStaging Secrets Not Set
If staging deployment works but the app fails at runtime, secrets may not be set for the staging environment:
# Check staging secrets
wrangler secret list --env staging
# Set missing secrets
wrangler secret put SECRET_NAME --env staging