My App

Cloudflare Deployment Plan

Deploy the monorepo to Cloudflare Pages, Workers, and Cron Triggers with Neon Postgres

Last updated: Jan 23, 2026

This document outlines the deployment strategy for migrating the 2026-supaseat monorepo to Cloudflare's edge infrastructure.

Current Architecture

ComponentTechnologyPort
Frontend (apps/web)Next.js 16 (App Router)3001
Documentation (apps/fumadocs)Next.js 16 + Fumadocs4000
Backend (apps/server)Hono API on Bun3000
DatabaseNeon Postgres 18 (serverless)-
AuthBetter Auth-
FX ProvidersECB, FRED, OpenExchangeRates-

Target Cloudflare Architecture

                    ┌───────────────────────────────────────────────────────────┐
                    │                    Cloudflare Edge                         │
                    │                                                            │
   Users ──────────►│  ┌────────────┐  ┌────────────┐  ┌─────────────────────┐  │
                    │  │  Workers   │  │  Workers   │  │   Workers           │  │
                    │  │  (Web App) │  │  (Docs)    │  │   (Hono API)        │  │
                    │  │  OpenNext  │  │  OpenNext  │  │   apps/server       │  │
                    │  └────────────┘  └────────────┘  └──────────┬──────────┘  │
                    │                                             │             │
                    │  ┌──────────────────────────────────────────┤             │
                    │  │         Cron Triggers (Daily)            │             │
                    │  │  ┌─────────┐ ┌─────────┐ ┌─────────┐     │             │
                    │  │  │  ECB    │ │  FRED   │ │   OXR   │     │             │
                    │  │  │ 6AM UTC │ │ 11AM UTC│ │ 7AM UTC │     │             │
                    │  │  └────┬────┘ └────┬────┘ └────┬────┘     │             │
                    │  └───────┼───────────┼──────────┼───────────┘             │
                    │          └───────────┼──────────┘                         │
                    └──────────────────────┼────────────────────────────────────┘

                                   ┌───────▼───────┐
                                   │ Neon Postgres │
                                   │   Serverless  │
                                   │  (us-east-1)  │
                                   └───────────────┘

Phase 1: Prerequisites & Setup

1.1 Install Dependencies

bun add -D wrangler @opennextjs/cloudflare

1.2 Cloudflare Account Setup

  1. Create Cloudflare account at dash.cloudflare.com
  2. Run npx wrangler login to authenticate
  3. Add custom domain (optional but recommended)

1.3 Neon Postgres Compatibility

The codebase already uses @neondatabase/serverless with Prisma adapter, which is compatible with Cloudflare Workers:

// packages/db/src/index.ts - Already configured
import { neonConfig, Pool } from "@neondatabase/serverless";
import { PrismaNeon } from "@prisma/adapter-neon";

No Hyperdrive needed - Neon's serverless driver handles connection pooling at the edge.


Phase 2: Backend Deployment (Hono Worker)

2.1 Create Worker Configuration

Create apps/server/wrangler.jsonc:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "supaseat-api",
  "main": "src/index.ts",
  "compatibility_date": "2025-01-20",
  "compatibility_flags": ["nodejs_compat"],

  // Environment variables (non-sensitive)
  "vars": {
    "NODE_ENV": "production",
    "ECB_ENABLED": "true",
  },

  // Cron triggers for FX rate fetching
  "triggers": {
    "crons": [
      "0 6 * * *", // ECB: 6 AM UTC daily (after ECB publishes ~4 PM CET)
      "0 11 * * *", // FRED: 11 AM UTC daily (6 AM ET, after FRED updates ~5 PM ET)
      "0 7 * * *", // OXR: 7 AM UTC daily (backup/comprehensive rates)
    ],
  },

  // Environment overrides
  "env": {
    "staging": {
      "name": "supaseat-api-staging",
      "vars": {
        "NODE_ENV": "staging",
      },
    },
  },
}

2.2 Create Scheduled Handler

Create apps/server/src/scheduled.ts:

import type { ScheduledController, ExecutionContext } from "@cloudflare/workers-types";
import { fetchEcbRates } from "@2026-supaseat/api/services/fx/provider-ecb";
import { fetchDailyFxRates } from "./jobs/fx-daily";
import { fetchOxrRates } from "@2026-supaseat/api/services/fx/provider-oxr";

interface Env {
  DATABASE_URL: string;
  FRED_API_KEY: string;
  OXR_APP_ID: string;
  ECB_ENABLED: string;
}

export async function scheduled(
  controller: ScheduledController,
  env: Env,
  ctx: ExecutionContext,
): Promise<void> {
  const cron = controller.cron;

  console.log(`[cron] Triggered: ${cron} at ${new Date(controller.scheduledTime).toISOString()}`);

  try {
    switch (cron) {
      case "0 6 * * *": // ECB
        if (env.ECB_ENABLED === "true") {
          ctx.waitUntil(fetchEcbRates());
          console.log("[cron] ECB fetch queued");
        }
        break;

      case "0 11 * * *": // FRED
        if (env.FRED_API_KEY) {
          ctx.waitUntil(fetchDailyFxRates());
          console.log("[cron] FRED fetch queued");
        }
        break;

      case "0 7 * * *": // OXR
        if (env.OXR_APP_ID) {
          ctx.waitUntil(fetchOxrRates(env.OXR_APP_ID));
          console.log("[cron] OXR fetch queued");
        }
        break;

      default:
        console.log(`[cron] Unknown schedule: ${cron}`);
    }
  } catch (error) {
    console.error(`[cron] Error:`, error);
  }
}

2.3 Update Server Entry Point

Update apps/server/src/index.ts to export both fetch and scheduled:

import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { scheduled } from "./scheduled";

interface Env {
  DATABASE_URL: string;
  BETTER_AUTH_SECRET: string;
  BETTER_AUTH_URL: string;
  CORS_ORIGIN: string;
  NODE_ENV: string;
  ECB_ENABLED: string;
  OXR_APP_ID?: string;
  FRED_API_KEY?: string;
}

const app = new Hono<{ Bindings: Env }>();

app.use("*", logger());
app.use(
  "*",
  cors({
    origin: (_, c) => c.env.CORS_ORIGIN,
    credentials: true,
  }),
);

// ... existing routes (auth, rpc, etc.)

app.get("/", (c) => c.text("OK"));

export default {
  fetch: app.fetch,
  scheduled,
};

2.4 Set Secrets

cd apps/server

# Required secrets
echo "your-neon-connection-string" | npx wrangler secret put DATABASE_URL --name supaseat-api
echo "your-auth-secret-min-32-chars" | npx wrangler secret put BETTER_AUTH_SECRET --name supaseat-api
echo "https://api.supaseat.com" | npx wrangler secret put BETTER_AUTH_URL --name supaseat-api
echo "https://app.supaseat.com,https://supaseat.com" | npx wrangler secret put CORS_ORIGIN --name supaseat-api

# FX API keys
echo "your-fred-api-key" | npx wrangler secret put FRED_API_KEY --name supaseat-api
echo "your-oxr-app-id" | npx wrangler secret put OXR_APP_ID --name supaseat-api

Repeat the same commands with --name supaseat-api-staging when seeding the staging worker.

2.5 Deploy Worker

cd apps/server
npx wrangler deploy

Phase 3: Frontend Deployment (Next.js on Workers with OpenNext)

OpenNext is the recommended way to deploy Next.js to Cloudflare. It builds directly for Cloudflare Workers with better feature support than @cloudflare/next-on-pages.

3.1 Install OpenNext Adapter

cd apps/web
bun add @opennextjs/cloudflare

3.2 Create OpenNext Configuration

Create apps/web/open-next.config.ts:

import { defineCloudflareConfig } from "@opennextjs/cloudflare";

export default defineCloudflareConfig({
  // Default configuration - no KV/R2 cache for simplicity
  // Add incrementalCache and tagCache overrides if needed for ISR
});

3.3 Create Wrangler Configuration

Create apps/web/wrangler.jsonc:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "supaseat-web",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-01-20",
  "compatibility_flags": ["nodejs_compat"],

  // Static assets binding (required)
  "assets": {
    "binding": "ASSETS",
    "directory": ".open-next/assets",
  },

  // Image optimization (optional - uses Cloudflare Images)
  "images": {
    "binding": "IMAGES",
  },

  // Observability
  "observability": {
    "enabled": true,
  },

  // Environment variables
  "vars": {
    "NEXT_PUBLIC_SERVER_URL": "https://supaseat-api.workers.dev",
  },

  // Environment overrides
  "env": {
    "preview": {
      "name": "supaseat-web-preview",
      "vars": {
        "NEXT_PUBLIC_SERVER_URL": "https://supaseat-api-staging.workers.dev",
      },
    },
  },
}

3.4 Update Next.js Configuration

Update apps/web/next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // OpenNext handles the output format automatically
  images: {
    // Use Cloudflare Images binding for optimization
    remotePatterns: [
      {
        protocol: "https",
        hostname: "**",
      },
    ],
  },
  // Existing config...
};

export default nextConfig;

3.5 Add Build Scripts

Update apps/web/package.json:

{
  "scripts": {
    "build": "next build",
    "build:cf": "npx opennextjs-cloudflare build",
    "deploy:cf": "npx opennextjs-cloudflare deploy",
    "preview:cf": "npx wrangler dev"
  }
}

3.6 Deploy to Cloudflare Workers

Option A: Direct Deploy (Recommended)

cd apps/web
bun run build:cf
bun run deploy:cf

Option B: Git Integration

  1. Dashboard > Workers & Pages > Create > Create Worker
  2. Connect GitHub repository
  3. Configure build settings:
    • Build command: cd apps/web && bun run build:cf
    • Deploy command: cd apps/web && npx opennextjs-cloudflare deploy

Option C: Manual Wrangler Deploy

cd apps/web
bun run build:cf
npx wrangler deploy

Phase 4: Documentation Site (Fumadocs on Workers with OpenNext)

4.1 Install OpenNext Adapter

cd apps/fumadocs
bun add @opennextjs/cloudflare

4.2 Create OpenNext Configuration

Create apps/fumadocs/open-next.config.ts:

import { defineCloudflareConfig } from "@opennextjs/cloudflare";

export default defineCloudflareConfig({
  // Default configuration for static docs site
});

4.3 Create Wrangler Configuration

Create apps/fumadocs/wrangler.jsonc:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "supaseat-docs",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-01-20",
  "compatibility_flags": ["nodejs_compat"],

  // Static assets binding (required)
  "assets": {
    "binding": "ASSETS",
    "directory": ".open-next/assets",
  },

  // Observability
  "observability": {
    "enabled": true,
  },
}

4.4 Update Next.js Configuration

Update apps/fumadocs/next.config.ts:

import { createMDX } from "fumadocs-mdx/next";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // OpenNext handles the output format automatically
  images: {
    unoptimized: true, // Docs typically don't need image optimization
  },
};

const withMDX = createMDX();
export default withMDX(nextConfig);

4.5 Add Build Scripts

Update apps/fumadocs/package.json:

{
  "scripts": {
    "build": "next build",
    "build:cf": "npx opennextjs-cloudflare build",
    "deploy:cf": "npx opennextjs-cloudflare deploy",
    "preview:cf": "npx wrangler dev"
  }
}

4.6 Deploy to Cloudflare Workers

cd apps/fumadocs
bun run build:cf
bun run deploy:cf

Phase 5: Environment Configuration

5.1 Environment Variables Reference

VariableRequiredDescription
DATABASE_URLYesNeon Postgres connection string
BETTER_AUTH_SECRETYesAuth secret (min 32 chars)
BETTER_AUTH_URLYesAuth base URL
CORS_ORIGINYesAllowed browser origins (comma separated)
NODE_ENVNodevelopment / production
ECB_ENABLEDNoEnable ECB provider (default: true)
FRED_API_KEYNoFRED API key for USD rates
OXR_APP_IDNoOpenExchangeRates app ID
DEBUG_IMPORTNoDebug import wizard

5.2 Local Development (.dev.vars)

Create apps/server/.dev.vars:

DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/supaseat?sslmode=require
BETTER_AUTH_SECRET=your-32-char-minimum-secret-here
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:3001
ECB_ENABLED=true
FRED_API_KEY=your-fred-api-key
OXR_APP_ID=your-oxr-app-id

Create apps/web/.dev.vars:

NEXT_PUBLIC_SERVER_URL=http://localhost:3000

5.3 Environment Variables by Environment

VariableDevelopmentStagingProduction
CORS_ORIGINhttp://localhost:3001https://staging.supaseat.pages.dev,https://supaseat-web-preview.supaseat-950.workers.devhttps://app.supaseat.com,https://supaseat.com
BETTER_AUTH_URLhttp://localhost:3000https://supaseat-api-staging.workers.devhttps://api.supaseat.com
NEXT_PUBLIC_SERVER_URLhttp://localhost:3000https://supaseat-api-staging.workers.devhttps://api.supaseat.com

Phase 6: CI/CD Pipeline

6.1 GitHub Actions Workflow

Create .github/workflows/deploy.yml:

name: Deploy to Cloudflare

on:
  push:
    branches: [main, staging]
  pull_request:
    branches: [main]

jobs:
  deploy-api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install

      - name: Deploy API Worker
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: apps/server
          command: deploy

  deploy-web:
    runs-on: ubuntu-latest
    needs: deploy-api
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install

      - name: Build Next.js with OpenNext
        working-directory: apps/web
        run: bun run build:cf

      - name: Deploy to Cloudflare Workers
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: apps/web
          command: deploy

  deploy-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install

      - name: Build Fumadocs with OpenNext
        working-directory: apps/fumadocs
        run: bun run build:cf

      - name: Deploy Docs to Cloudflare Workers
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: apps/fumadocs
          command: deploy

6.2 Required GitHub Secrets

SecretDescription
CLOUDFLARE_API_TOKENAPI token with Workers/Pages edit permissions
CLOUDFLARE_ACCOUNT_IDYour Cloudflare account ID

Phase 7: Custom Domain Setup

7.1 API Domain (Workers)

# In Cloudflare Dashboard: Workers > supaseat-api > Triggers > Custom Domains
# Add: api.supaseat.com

7.2 Web Domain (Pages)

  1. Dashboard > Pages > supaseat-web > Custom domains
  2. Add domain: supaseat.com and www.supaseat.com

7.3 Docs Domain (Pages)

  1. Dashboard > Pages > supaseat-docs > Custom domains
  2. Add domain: docs.supaseat.com

Phase 8: Monitoring & Observability

8.1 Enable Observability

Update wrangler.jsonc:

{
  "observability": {
    "enabled": true,
    "head_sampling_rate": 0.1,
  },
}

8.2 Log Tailing

# Real-time logs
npx wrangler tail supaseat-api

# Filter cron executions
npx wrangler tail supaseat-api --search "cron"

# Filter errors only
npx wrangler tail supaseat-api --status error

8.3 Test Cron Triggers Locally

npx wrangler dev

# Trigger ECB cron
curl "http://localhost:8787/__scheduled?cron=0+6+*+*+*"

# Trigger FRED cron
curl "http://localhost:8787/__scheduled?cron=0+11+*+*+*"

# Trigger OXR cron
curl "http://localhost:8787/__scheduled?cron=0+7+*+*+*"

Deployment Checklist

Pre-Deployment

  • Cloudflare account created and verified
  • Wrangler CLI installed and authenticated (wrangler login)
  • Neon Postgres database accessible
  • FRED API key obtained from FRED
  • OXR App ID obtained from OpenExchangeRates

Backend Deployment (apps/server)

  • wrangler.jsonc created with cron triggers
  • scheduled.ts handler created for FX jobs
  • Server entry point exports fetch and scheduled
  • All secrets set via wrangler secret put:
    • DATABASE_URL
    • BETTER_AUTH_SECRET
    • BETTER_AUTH_URL
    • CORS_ORIGIN
    • FRED_API_KEY
    • OXR_APP_ID
  • Worker deployed and tested
  • Cron triggers verified in dashboard

Frontend Deployment (apps/web)

  • @opennextjs/cloudflare installed
  • open-next.config.ts created
  • wrangler.jsonc configured with assets binding
  • next.config.ts updated for Cloudflare Images
  • Build succeeds with bun run build:cf
  • Worker deployed with bun run deploy:cf

Documentation Site (apps/fumadocs)

  • @opennextjs/cloudflare installed
  • open-next.config.ts created
  • wrangler.jsonc configured with assets binding
  • Build succeeds with bun run build:cf
  • Worker deployed with bun run deploy:cf

Post-Deployment

  • API endpoints responding
  • Auth flow working
  • Database queries executing
  • Cron jobs executing (check logs next day)
  • FX rates being stored in database
  • Custom domains configured
  • SSL certificates active

FX Rate Cron Schedule

ProviderCronUTC TimeReason
ECB0 6 * * *6:00 AMAfter ECB publishes (~4 PM CET = 3 PM UTC)
FRED0 11 * * *11:00 AMAfter FRED updates (~5 PM ET = 10 PM UTC previous day)
OXR0 7 * * *7:00 AMBackup/comprehensive rates

Note: All times are UTC. Cron triggers run globally during underutilized periods.


Troubleshooting

Common Issues

IssueSolution
Neon connection timeoutCheck DATABASE_URL includes ?sslmode=require
Cron not triggeringWait 15 min for global propagation; check dashboard
FRED returns no dataWeekend/holiday - job handles 3-day lookback
"Module not found"Ensure nodejs_compat flag is set
Auth not workingVerify BETTER_AUTH_SECRET matches across envs

Useful Commands

# Check Worker status
npx wrangler deployments list

# View cron schedules
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/supaseat-api/schedules" \
  -H "Authorization: Bearer {api_token}"

# Validate config
npx wrangler deploy --dry-run

# Rollback deployment
npx wrangler rollback

Cost Estimation

Free Tier Limits

ResourceFree Limit
Workers Requests100k/day
Workers CPU10ms/request
Cron Triggers3/worker
Pages RequestsUnlimited
Pages Builds500/month

Estimated Monthly Cost (Production)

  • Workers Paid Plan: $5/month base (for 50ms CPU limit)
  • Additional requests: ~$0.50/million
  • Neon Postgres: Separate billing (free tier available)

Estimated Cloudflare total: $5-15/month


Resources

On this page

Current ArchitectureTarget Cloudflare ArchitecturePhase 1: Prerequisites & Setup1.1 Install Dependencies1.2 Cloudflare Account Setup1.3 Neon Postgres CompatibilityPhase 2: Backend Deployment (Hono Worker)2.1 Create Worker Configuration2.2 Create Scheduled Handler2.3 Update Server Entry Point2.4 Set Secrets2.5 Deploy WorkerPhase 3: Frontend Deployment (Next.js on Workers with OpenNext)3.1 Install OpenNext Adapter3.2 Create OpenNext Configuration3.3 Create Wrangler Configuration3.4 Update Next.js Configuration3.5 Add Build Scripts3.6 Deploy to Cloudflare WorkersPhase 4: Documentation Site (Fumadocs on Workers with OpenNext)4.1 Install OpenNext Adapter4.2 Create OpenNext Configuration4.3 Create Wrangler Configuration4.4 Update Next.js Configuration4.5 Add Build Scripts4.6 Deploy to Cloudflare WorkersPhase 5: Environment Configuration5.1 Environment Variables Reference5.2 Local Development (.dev.vars)5.3 Environment Variables by EnvironmentPhase 6: CI/CD Pipeline6.1 GitHub Actions Workflow6.2 Required GitHub SecretsPhase 7: Custom Domain Setup7.1 API Domain (Workers)7.2 Web Domain (Pages)7.3 Docs Domain (Pages)Phase 8: Monitoring & Observability8.1 Enable Observability8.2 Log Tailing8.3 Test Cron Triggers LocallyDeployment ChecklistPre-DeploymentBackend Deployment (apps/server)Frontend Deployment (apps/web)Documentation Site (apps/fumadocs)Post-DeploymentFX Rate Cron ScheduleTroubleshootingCommon IssuesUseful CommandsCost EstimationFree Tier LimitsEstimated Monthly Cost (Production)Resources