My App

Cloudflare Deployment

Deploy apps to Cloudflare Workers with custom domains

Last updated: Jan 23, 2026

This guide covers deploying the apps to Cloudflare Workers with custom domains on supaseat.com.

Architecture

AppWorkerCustom DomainProduction URL
apps/serversupaseat-apiapi.supaseat.comhttps://supaseat-api.supaseat-950.workers.dev
apps/websupaseat-webapp.supaseat.comhttps://supaseat-web.supaseat-950.workers.dev
apps/fumadocssupaseat-docsdocs.supaseat.comhttps://supaseat-docs.supaseat-950.workers.dev

Staging/preview environments use -staging or -preview suffixes.

Prerequisites

  1. Cloudflare account with supaseat.com domain
  2. Wrangler CLI authenticated: wrangler login
  3. Dependencies installed: bun install

Environment Bindings

WorkerBindingTypeSource
supaseat-apiDATABASE_URLSecretNeon Postgres connection string
BETTER_AUTH_SECRETSecretBetter Auth secret (min 32 chars)
BETTER_AUTH_URLSecrethttps://api.supaseat.com
CORS_ORIGINSecretComma-separated list (include both https://app.supaseat.com and https://supaseat.com)
NODE_ENVVarDefined in wrangler.jsonc per environment
ECB_ENABLEDVarSet to "true" unless FX sync is paused
OXR_APP_ID (optional)SecretOpen Exchange Rates App ID
FRED_API_KEY (optional)SecretFRED API key
supaseat-webNEXT_PUBLIC_SERVER_URLVarAPI 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-staging

Verify secrets are set:

wrangler secret list
wrangler secret list --env staging

Deploy

Deploy Order

Deploy in order: Server → Web (the web app depends on the API URL).

From the repo root:

bun run deploy:server
bun run deploy:web

Manual Deployment

# 1. Server (Hono API)
cd apps/server && wrangler deploy

# 2. Web (Next.js)
cd apps/web && wrangler deploy

Deploy to Staging

# Server staging
cd apps/server && wrangler deploy --env staging

# Web preview
cd apps/web && wrangler deploy --env preview

Bundle Size Limits

Cloudflare Workers have bundle size limits:

PlanGzip LimitUncompressed Limit
Free3 MB64 MB
Paid10 MB64 MB

Check bundle size with a dry run:

wrangler deploy --outdir bundled/ --dry-run
# Output: Total Upload: 12470.52 KiB / gzip: 2827.70 KiB

Web App Bundle Optimization

The web app must stay under 3MB gzip for the free tier. Key optimizations:

  1. Don't import server-side auth in the frontend - The @2026-supaseat/auth package imports Prisma, adding ~2MB to the bundle.

  2. Use API calls for session checks - Instead of importing auth directly, the web app uses @/lib/auth-server.ts which 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;
    }
  3. Type-only imports - Use import type { ... } for types from server packages to ensure tree-shaking.

  4. Don't include @prisma/client in web dependencies - It's only needed on the server.

Cron Triggers

The API worker has scheduled cron triggers for FX rate fetching:

CronTime (UTC)JobRequires
0 6 * * *06:00ECB rates-
0 7 * * *07:00OXR ratesOXR_APP_ID
0 11 * * *11:00FRED ratesFRED_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)

ScriptDescription
deploy:serverDeploy API to production
deploy:webDeploy web app to production
deploy:docsDeploy docs to production

Server (apps/server/package.json)

ScriptDescription
previewRun locally with Wrangler
deployDeploy to production
deploy:stagingDeploy to staging env

Web (apps/web/package.json)

ScriptDescription
previewBuild + run locally (production config)
preview:stagingBuild + run locally (staging config)
deployBuild + deploy to production
deploy:stagingBuild + deploy to staging env
cf-typegenGenerate CloudflareEnv TypeScript types

Monitoring

Logs

# Tail production logs
wrangler tail supaseat-api

# Tail with filters
wrangler tail supaseat-api --status error

Observability

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":

  1. Check what's in the bundle:

    wrangler deploy --outdir bundled/ --dry-run
    du -sh bundled/*
  2. Common culprits:

    • @prisma/client in frontend dependencies
    • Importing server-side auth module directly
    • Large icon libraries not tree-shaken
    • WASM files from @vercel/og (resvg.wasm ~1.4MB)
  3. Solutions:

    • Use type-only imports: import type { ... }
    • Fetch session from API instead of importing auth
    • Remove unused dependencies

Custom Domain DNS Conflicts

If deployment fails with "A]DNS record already exists" errors, the custom domain has existing DNS records that conflict.

Options:

  1. Remove the route from wrangler.jsonc and configure the custom domain manually in Cloudflare Dashboard
  2. Delete the existing DNS record first
  3. 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 build

Staging 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

On this page