Cloudflare Deployment Plan
Deploy the monorepo to Cloudflare Pages, Workers, and Cron Triggers with Neon Postgres
This document outlines the deployment strategy for migrating the 2026-supaseat monorepo to Cloudflare's edge infrastructure.
Current Architecture
| Component | Technology | Port |
|---|---|---|
Frontend (apps/web) | Next.js 16 (App Router) | 3001 |
Documentation (apps/fumadocs) | Next.js 16 + Fumadocs | 4000 |
Backend (apps/server) | Hono API on Bun | 3000 |
| Database | Neon Postgres 18 (serverless) | - |
| Auth | Better Auth | - |
| FX Providers | ECB, 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/cloudflare1.2 Cloudflare Account Setup
- Create Cloudflare account at dash.cloudflare.com
- Run
npx wrangler loginto authenticate - 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-apiRepeat the same commands with
--name supaseat-api-stagingwhen seeding the staging worker.
2.5 Deploy Worker
cd apps/server
npx wrangler deployPhase 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/cloudflare3.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:cfOption B: Git Integration
- Dashboard > Workers & Pages > Create > Create Worker
- Connect GitHub repository
- Configure build settings:
- Build command:
cd apps/web && bun run build:cf - Deploy command:
cd apps/web && npx opennextjs-cloudflare deploy
- Build command:
Option C: Manual Wrangler Deploy
cd apps/web
bun run build:cf
npx wrangler deployPhase 4: Documentation Site (Fumadocs on Workers with OpenNext)
4.1 Install OpenNext Adapter
cd apps/fumadocs
bun add @opennextjs/cloudflare4.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:cfPhase 5: Environment Configuration
5.1 Environment Variables Reference
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | Neon Postgres connection string |
BETTER_AUTH_SECRET | Yes | Auth secret (min 32 chars) |
BETTER_AUTH_URL | Yes | Auth base URL |
CORS_ORIGIN | Yes | Allowed browser origins (comma separated) |
NODE_ENV | No | development / production |
ECB_ENABLED | No | Enable ECB provider (default: true) |
FRED_API_KEY | No | FRED API key for USD rates |
OXR_APP_ID | No | OpenExchangeRates app ID |
DEBUG_IMPORT | No | Debug 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-idCreate apps/web/.dev.vars:
NEXT_PUBLIC_SERVER_URL=http://localhost:30005.3 Environment Variables by Environment
| Variable | Development | Staging | Production |
|---|---|---|---|
CORS_ORIGIN | http://localhost:3001 | https://staging.supaseat.pages.dev,https://supaseat-web-preview.supaseat-950.workers.dev | https://app.supaseat.com,https://supaseat.com |
BETTER_AUTH_URL | http://localhost:3000 | https://supaseat-api-staging.workers.dev | https://api.supaseat.com |
NEXT_PUBLIC_SERVER_URL | http://localhost:3000 | https://supaseat-api-staging.workers.dev | https://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: deploy6.2 Required GitHub Secrets
| Secret | Description |
|---|---|
CLOUDFLARE_API_TOKEN | API token with Workers/Pages edit permissions |
CLOUDFLARE_ACCOUNT_ID | Your 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.com7.2 Web Domain (Pages)
- Dashboard > Pages > supaseat-web > Custom domains
- Add domain:
supaseat.comandwww.supaseat.com
7.3 Docs Domain (Pages)
- Dashboard > Pages > supaseat-docs > Custom domains
- 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 error8.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.jsonccreated with cron triggers -
scheduled.tshandler created for FX jobs - Server entry point exports
fetchandscheduled - 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/cloudflareinstalled -
open-next.config.tscreated -
wrangler.jsoncconfigured with assets binding -
next.config.tsupdated for Cloudflare Images - Build succeeds with
bun run build:cf - Worker deployed with
bun run deploy:cf
Documentation Site (apps/fumadocs)
-
@opennextjs/cloudflareinstalled -
open-next.config.tscreated -
wrangler.jsoncconfigured 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
| Provider | Cron | UTC Time | Reason |
|---|---|---|---|
| ECB | 0 6 * * * | 6:00 AM | After ECB publishes (~4 PM CET = 3 PM UTC) |
| FRED | 0 11 * * * | 11:00 AM | After FRED updates (~5 PM ET = 10 PM UTC previous day) |
| OXR | 0 7 * * * | 7:00 AM | Backup/comprehensive rates |
Note: All times are UTC. Cron triggers run globally during underutilized periods.
Troubleshooting
Common Issues
| Issue | Solution |
|---|---|
| Neon connection timeout | Check DATABASE_URL includes ?sslmode=require |
| Cron not triggering | Wait 15 min for global propagation; check dashboard |
| FRED returns no data | Weekend/holiday - job handles 3-day lookback |
| "Module not found" | Ensure nodejs_compat flag is set |
| Auth not working | Verify 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 rollbackCost Estimation
Free Tier Limits
| Resource | Free Limit |
|---|---|
| Workers Requests | 100k/day |
| Workers CPU | 10ms/request |
| Cron Triggers | 3/worker |
| Pages Requests | Unlimited |
| Pages Builds | 500/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