Migration Checklist
Step-by-step guide to moving your Next.js app from Vercel to Cloudflare.
High Effort
This is a significant migration. Validate compatibility, test thoroughly, and have a rollback plan. Not every app should migrate.
A practical checklist for migrating from Vercel to Cloudflare. Not every app should migrate—validate first.
Should You Migrate?
Cloudflare is not a drop-in replacement. Migrate only if the benefits outweigh the engineering cost.
Good Candidates
- Static-heavy sites with occasional dynamic routes
- Read-heavy APIs with simple compute
- Sites where cold starts hurt UX
- High-traffic sites where Vercel costs explode
Poor Candidates
- Heavy use of
fs, native modules, or Node.js APIs - Complex ISR patterns
- Tight integration with Vercel features (Analytics, Flags, etc.)
- Small teams without bandwidth for migration work
Pre-Migration Audit
Audit Dependencies
# Find problematic packages
npx depcheck
# Check for native modules
grep -r "node-gyp\|prebuild" node_modules/*/package.json | head -20
# Common blockers:
# - sharp (image processing)
# - bcrypt (use bcryptjs)
# - better-sqlite3 (use D1)
# - canvas (limited alternatives)Audit Node.js API Usage
# File system usage
grep -rn "from 'fs'" src/ app/ pages/
grep -rn "require('fs')" src/ app/ pages/
# Buffer usage
grep -rn "Buffer\." src/ app/ pages/
# Crypto
grep -rn "require('crypto')" src/ app/ pages/
grep -rn "from 'crypto'" src/ app/ pages/
# process.env at runtime (not build time)
grep -rn "process\.env\." src/ app/ pages/ | grep -v ".env"Audit API Routes
Every API route needs to be edge-compatible:
// Check each route in app/api/
// ✅ Safe patterns
export const runtime = 'edge';
export async function GET(request: Request) {
// Web APIs only
}
// ❌ Unsafe patterns
import fs from 'fs';
import { Pool } from 'pg';Audit Caching Strategy
Document your current caching:
// Find ISR usage
grep -rn "revalidate:" pages/ app/
grep -rn "getStaticProps" pages/
// Find fetch caching
grep -rn "next: {" src/ app/Plan replacements for each pattern.
Migration Steps
Set Up OpenNext
npm install @opennextjs/cloudflare wrangler --save-devCreate open-next.config.ts:
import type { OpenNextConfig } from "@opennextjs/cloudflare";
const config: OpenNextConfig = {
buildCommand: "npx next build",
};
export default config;Configure Wrangler
Create wrangler.toml:
name = "my-nextjs-app"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
NODE_ENV = "production"
# Add KV namespaces for caching
[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-namespace-id"
# Add D1 if using database
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "your-d1-database-id"Replace Incompatible Packages
# bcrypt → bcryptjs
npm uninstall bcrypt
npm install bcryptjs
# Database driver
npm uninstall pg
npm install @neondatabase/serverless
# or use D1 directlyUpdate imports:
// Before
import bcrypt from 'bcrypt';
// After
import bcrypt from 'bcryptjs';Update Environment Variables
// Before: process.env everywhere
const apiKey = process.env.API_KEY;
// After: env binding in API routes
export async function GET(request: Request, { env }) {
const apiKey = env.API_KEY;
}
// For client-side, still use NEXT_PUBLIC_
const publicKey = process.env.NEXT_PUBLIC_STRIPE_KEY;Add secrets to Cloudflare:
npx wrangler secret put API_KEY
npx wrangler secret put DATABASE_URLHandle Image Optimization
Next.js Image component needs configuration:
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.js',
},
};export default function cloudflareLoader({ src, width, quality }) {
const params = [`width=${width}`];
if (quality) params.push(`quality=${quality}`);
// Use Cloudflare Images
return `https://your-account.cloudflareimages.com/${src}?${params.join('&')}`;
// Or use a simple CDN approach
// return `${src}?w=${width}&q=${quality || 75}`;
}Implement Caching Layer
Replace Next.js caching with Cloudflare primitives:
// lib/cache.ts
export async function getCached<T>(
env: Env,
key: string,
fetcher: () => Promise<T>,
ttl: number = 60
): Promise<T> {
const cached = await env.CACHE.get(key, 'json');
if (cached) {
return cached as T;
}
const fresh = await fetcher();
await env.CACHE.put(key, JSON.stringify(fresh), {
expirationTtl: ttl
});
return fresh;
}Test Locally
# Build for Cloudflare
npx opennext build
# Test with Wrangler
npx wrangler dev
# Check for runtime errors
# - Missing APIs
# - Import failures
# - Timeout issuesDeploy
# Deploy to Cloudflare
npx wrangler deploy
# Set up custom domain in Cloudflare dashboard
# DNS → Add CNAME recordPost-Migration Checklist
Verify page rendering All pages should render correctly without console errors.
Test API routes Verify all endpoints return expected responses with correct status codes.
Check image handling Images should load and resize properly through your new image solution.
Test authentication flows Login, logout, session persistence, and protected routes must all work.
Verify database connectivity All queries should succeed. Watch for connection timeout issues.
Validate caching behavior Check cache headers, ISR replacement, and invalidation triggers.
Test error handling 404, 500, and custom error pages should display correctly.
Verify redirects and headers All redirects work, security headers are set, CORS is configured.
Rollback Plan
Keep Vercel deployment active during migration:
# Vercel stays on main domain during testing
main.example.com → Vercel
# Cloudflare on subdomain for testing
cf.example.com → Cloudflare
# Flip DNS when ready
# Keep Vercel project for 30 days as rollbackCommon Issues
"X is not defined"
Missing Web API or Node.js API. Check compatibility flags:
# wrangler.toml
compatibility_flags = ["nodejs_compat"]"Maximum call stack exceeded"
Usually a circular dependency or infinite recursion in SSR. Check:
- Circular imports
- useEffect running on server
- Recursive component rendering
"Script too large"
Worker size limit (1MB free, 10MB paid). Solutions:
- Code split aggressively
- Move large dependencies to external services
- Use dynamic imports
Slow cold starts (shouldn't happen)
If seeing cold starts, you might be on Workers Free (has limits). Check your plan.
With migration complete, monitor costs and performance. The switch should show immediate improvements in cold start latency and potentially significant cost savings.