Skip to main content
Deploy Featul to Cloudflare Workers for global edge distribution with low latency. This guide covers deployment using Wrangler CLI.
Cloudflare deployment is experimental. Vercel is the recommended production platform. See Deploy to Vercel.

Prerequisites

Before deploying to Cloudflare:
1

Cloudflare Account

Sign up at cloudflare.com and note your Account ID from the dashboard.
2

Install Wrangler

npm install -g wrangler
# or
bun add -g wrangler
Wrangler is included as a dev dependency in apps/app/package.json:
{
  "devDependencies": {
    "wrangler": "^4.53.0"
  }
}
3

Authenticate Wrangler

wrangler login
This opens a browser to authenticate with your Cloudflare account.
4

Prepare Database

Provision a PostgreSQL database accessible from Cloudflare Workers. See Database Setup.Recommended: Neon (serverless PostgreSQL over HTTP)

Cloudflare Workers Setup

Next.js on Cloudflare Workers requires specific configuration and has some limitations compared to Node.js runtime.

Create wrangler.toml

Create wrangler.toml in apps/app/:
name = "featul-app"
main = "./.worker-next/index.mjs"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]

[build]
command = "next build"

[env.production]
name = "featul-app-production"
route = { pattern = "app.yourdomain.com/*", zone_name = "yourdomain.com" }

[env.production.vars]
NEXT_PUBLIC_APP_URL = "https://app.yourdomain.com"
AUTH_COOKIE_DOMAIN = "yourdomain.com"
PASSKEY_RP_ID = "yourdomain.com"
PASSKEY_RP_NAME = "Featul"
PASSKEY_ORIGIN = "https://app.yourdomain.com"

[env.staging]
name = "featul-app-staging"
route = { pattern = "staging.yourdomain.com/*", zone_name = "yourdomain.com" }
Do not include secrets in wrangler.toml. Use wrangler secret to set sensitive values.

Configure Next.js for Workers

Update apps/app/next.config.ts to support Cloudflare:
const nextConfig = {
  // Existing config...
  
  // Enable standalone output for Cloudflare
  output: 'standalone',
  
  // Experimental Cloudflare support
  experimental: {
    runtime: 'experimental-edge',
  },
}
Cloudflare Workers has a 1MB script size limit. Large dependencies may cause deployment failures.

Environment Variables

Set environment variables using Wrangler secrets:

Required Secrets

# Database
wrangler secret put DATABASE_URL
# Paste: postgresql://user:password@host/db?sslmode=require

# Authentication
wrangler secret put BETTER_AUTH_SECRET
# Paste: your-secret-min-32-chars (generate with: openssl rand -base64 32)

# Trusted origins
wrangler secret put AUTH_TRUSTED_ORIGINS
# Paste: https://yourdomain.com,https://app.yourdomain.com,https://*.yourdomain.com

Optional Secrets

# Redis
wrangler secret put UPSTASH_REDIS_REST_URL
wrangler secret put UPSTASH_REDIS_REST_TOKEN

# OAuth Providers
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET

# AI Features
wrangler secret put OPENROUTER_API_KEY

# Payments
wrangler secret put POLAR_ACCESS_TOKEN
wrangler secret put POLAR_WEBHOOK_SECRET

# Monitoring
wrangler secret put SENTRY_AUTH_TOKEN

# Encryption
wrangler secret put NOTRA_CREDENTIALS_ENCRYPTION_KEY

Bulk Secret Upload

Create .env.production (do not commit):
DATABASE_URL=postgresql://...
BETTER_AUTH_SECRET=...
AUTH_TRUSTED_ORIGINS=...
# ... all other secrets
Upload all at once:
wrangler secret bulk .env.production

Database Migrations

1

Run Migrations Locally

Cloudflare Workers cannot run migrations during build. Run migrations before deployment:
# Set production DATABASE_URL
export DATABASE_URL="postgresql://user:password@host/db?sslmode=require"

# Run migrations
bun run db:migrate
2

Verify Schema

bun run db:studio
Check all tables are created.

Build and Deploy

1

Build Application

cd apps/app
bun run build
2

Deploy to Production

wrangler deploy --env production
This:
  1. Uploads worker script to Cloudflare
  2. Deploys to production route
  3. Activates new version
3

Verify Deployment

curl https://app.yourdomain.com/api/health

Domain Configuration

Wildcard routing for multi-tenant subdomains requires Cloudflare Workers for Platforms (paid feature).

Add Custom Domain

1

Add Domain to Cloudflare

  1. Go to Cloudflare dashboard
  2. Click Add Site
  3. Enter your domain: yourdomain.com
  4. Follow DNS migration steps
2

Configure DNS Records

Add DNS records in Cloudflare:
Type    Name    Content             Proxy
A       app     192.0.2.1           Proxied (orange cloud)
CNAME   *       app.yourdomain.com  Proxied
The wildcard * CNAME enables workspace subdomains.
3

Add Routes in wrangler.toml

[env.production]
routes = [
  { pattern = "app.yourdomain.com/*", zone_name = "yourdomain.com" },
  { pattern = "*.yourdomain.com/*", zone_name = "yourdomain.com" }
]
4

Deploy with Routes

wrangler deploy --env production

SSL Configuration

Cloudflare automatically provides:
  • Universal SSL certificate (Free)
  • Automatic HTTPS rewrites
  • TLS 1.3 support
For wildcard SSL:
  1. Go to SSL/TLS > Edge Certificates
  2. Enable Universal SSL
  3. Wait for certificate issuance (up to 24 hours)

Cloudflare-Specific Features

D1 Database (Alternative to PostgreSQL)

Cloudflare D1 is SQLite-based. Featul uses PostgreSQL. Migration to D1 requires schema changes.
To use D1 instead of PostgreSQL:
  1. Create D1 database:
wrangler d1 create featul-db
  1. Add to wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "featul-db"
database_id = "your-database-id"
  1. Migrate Drizzle schema to D1 (requires custom work)

KV Storage (Alternative to Redis)

Use Cloudflare KV for caching:
wrangler kv:namespace create "FEATUL_CACHE"
Add to wrangler.toml:
[[kv_namespaces]]
binding = "CACHE"
id = "your-namespace-id"

R2 Storage (File Uploads)

For file uploads, use Cloudflare R2:
wrangler r2 bucket create featul-uploads
Add to wrangler.toml:
[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "featul-uploads"

Monitoring and Logs

View Logs

# Tail production logs
wrangler tail --env production

# Filter by status code
wrangler tail --env production --status 500

Analytics

Cloudflare Workers provides built-in analytics:
  1. Go to Workers & Pages > featul-app
  2. Click Analytics tab
  3. View:
    • Requests per second
    • Success rate
    • CPU time
    • Duration

Error Tracking

Integrate Sentry for error tracking:
// apps/app/instrumentation.ts
import * as Sentry from '@sentry/nextjs'

export function register() {
  if (process.env.NEXT_RUNTIME === 'edge') {
    Sentry.init({
      dsn: process.env.SENTRY_DSN,
      environment: process.env.VERCEL_ENV || 'development',
    })
  }
}

Limitations

Cloudflare Workers has some limitations compared to Node.js:
Be aware of these limitations when deploying to Cloudflare.
  • CPU Time: 50ms per request (Free), 50s (Paid)
  • Memory: 128MB per request
  • Script Size: 1MB compressed (Free), 5MB (Paid)
  • Subrequests: 50 per request (Free), 1000 (Paid)
  • Node.js APIs: Limited compatibility (enable nodejs_compat flag)
  • WebSockets: Not supported in all plans
For Featul specifically:
  • Drizzle ORM works with Neon HTTP driver
  • File uploads require R2 integration
  • Session storage requires KV or external Redis
  • Some Node.js dependencies may not work

CI/CD with GitHub Actions

Automate deployments with GitHub Actions:
# .github/workflows/deploy-cloudflare.yml
name: Deploy to Cloudflare

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: oven-sh/setup-bun@v1
        with:
          bun-version: 1.2.21
      
      - name: Install dependencies
        run: bun install
      
      - name: Build
        run: bun run build --filter=app
      
      - name: Deploy to Cloudflare
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: deploy --env production
          workingDirectory: apps/app
Add secrets to GitHub:
  • CLOUDFLARE_API_TOKEN: From Cloudflare dashboard > API Tokens
  • CLOUDFLARE_ACCOUNT_ID: From Cloudflare dashboard

Troubleshooting

Deployment Fails: “Script too large”

Error: Script size exceeds 1MB limit
Solution:
  • Upgrade to Workers Paid plan (5MB limit)
  • Reduce bundle size:
    # Analyze bundle
    ANALYZE=true bun run build
    
  • Remove unused dependencies
  • Enable tree-shaking in next.config.ts

Database Connection Timeout

Error: Connection timeout
Solution:
  • Use Neon serverless with HTTP driver (not WebSocket)
  • Ensure DATABASE_URL uses pooled connection
  • Check Cloudflare outbound connections are allowed

Node.js API Not Supported

Error: Cannot find module 'fs'
Solution:
  • Add nodejs_compat to wrangler.toml:
    compatibility_flags = ["nodejs_compat"]
    
  • Or replace with edge-compatible alternatives

Wildcard Subdomain Not Working

Solution:
  • Verify wildcard DNS: CNAME * app.yourdomain.com
  • Check route pattern in wrangler.toml: *.yourdomain.com/*
  • For production: Consider Workers for Platforms

Next Steps

Vercel Deployment

Deploy to Vercel (recommended platform)

Environment Variables

Complete environment variable reference

Database Setup

Configure PostgreSQL and migrations

Build docs developers (and LLMs) love