I had been running more than 60 Next.js projects on Vercel Pro. By migrating the organization-facing apps to Cloudflare Pages, I was able to downgrade to the Hobby (free) plan.
Background
The Problem with Vercel Pro
Vercel's Hobby plan is for non-commercial, personal use only. If you are hosting an application for an organization, you need to be on Pro ($20/month and up).
Of my 60+ projects, only 2 were organization-facing. Moving those 2 to Cloudflare would let the rest run on the Hobby plan as personal projects.
What Was Migrated
- A multilingual search app (Japanese/English) built with Next.js + next-intl + Elasticsearch
- A IIIF image viewer app
Both were running on custom domains.
The Next.js 16 Obstacle — proxy.ts Not Supported
The first attempt to migrate directly to Cloudflare Pages failed at the final stage of the build:
Node.js middleware is not currently supported.
Consider switching to Edge Middleware.
In Next.js 16, the conventional middleware.ts was renamed to proxy.ts and locked to the Node.js runtime. As of @opennextjs/cloudflare v1.18.0, the new proxy.ts is not yet supported (opennextjs/opennextjs-cloudflare#962).
Solution: Downgrade to Next.js 15
# package.json
"next": "^15.3.1", # 16.x → 15.x
"next-intl": "^3.26.5", # 4.x → 3.x (Next.js 15 compatible version)
"eslint-config-next": "^15.3.1",
At the same time, proxy.ts was renamed back to middleware.ts and the export name was changed from proxy to middleware.
Deploying to Cloudflare Pages
1. Install Dependencies
npm install --save-dev @opennextjs/cloudflare wrangler
2. Create Configuration Files
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
// wrangler.jsonc
{
"name": "my-app",
"pages_build_output_dir": ".open-next/assets",
"compatibility_date": "2025-04-01",
"compatibility_flags": ["nodejs_compat"]
}
3. Build
npx @opennextjs/cloudflare build
4. Deploy to Pages (The Key Part)
The build output from @opennextjs/cloudflare is structured for Workers, but deploying to Pages requires the following steps:
# Create the Pages project
npx wrangler pages project create my-app --production-branch main
# Copy _worker.js (the Pages Worker entry point)
cp .open-next/worker.js .open-next/_worker.js
# Copy the assets directory contents to the .open-next root (for static file serving)
cp -r .open-next/assets/* .open-next/
# Deploy the entire .open-next directory
npx wrangler pages deploy .open-next --project-name my-app --branch main
Three things to get right:
- Copy
_worker.js— Pages recognizes_worker.jsas the Worker entry point - Expand the assets — Files inside
assets/(like_next/static/) must be copied to the.open-next/root or CSS and JS will return 404 - Deploy the entire
.open-nextdirectory — Deploying onlyassets/omits Worker dependency files likeserver-functions/
5. Use _routes.json to Exclude Static Files from the Worker
Without this, static assets like CSS and JavaScript are passed to the Worker (the Next.js server) and return 404.
// .open-next/_routes.json
{
"version": 1,
"include": ["/*"],
"exclude": [
"/_next/static/*",
"/favicon.ico",
"/apple-icon.png",
"/icon.png",
"/manifest.json",
"/sitemap.xml",
"/robots.txt",
"/images/*",
"/*.webp",
"/*.png",
"/*.jpg",
"/*.css",
"/*.js"
]
}
Paths listed in exclude are served directly by Pages' static file delivery. Everything else is handled by _worker.js (the Next.js server).
6. Set Environment Variables
# Server-side secrets are set as secrets
npx wrangler pages secret put ES_URL --project-name my-app
npx wrangler pages secret put ES_USER --project-name my-app
npx wrangler pages secret put ES_PASSWORD --project-name my-app
NEXT_PUBLIC_* variables are embedded at build time, so they should be set in .env.local before building.
Safe Experimentation with git worktree
To test the Cloudflare migration without affecting the existing Vercel deployment, git worktree was used:
# Create an experimental worktree from the develop branch
git worktree add ../my-app-cloudflare -b feature/cloudflare-migration develop
This allows testing Cloudflare-specific changes in a separate directory while the original repository continues deploying to Vercel unchanged.
Workers vs. Pages — Why Pages Was Chosen
| Item | Workers | Pages |
|---|---|---|
| Custom domain | Cloudflare DNS zone required | CNAME only is sufficient |
| GitHub integration | Manual setup | Easy from the dashboard |
| Preview deployments | Must configure manually | Automatic per PR |
| Requests (free tier) | 100k/day | Unlimited |
Deployment was first attempted on Workers to verify behavior, but it turned out that Workers requires a Cloudflare DNS zone for custom domains. Since the existing DNS (Sakura, Route 53, etc.) was to be kept as-is, the target was switched to Pages.
What to Watch Out for on Vercel Hobby
Post-downgrade restrictions:
| Item | Pro | Hobby |
|---|---|---|
| Bandwidth | 1 TB/month | 100 GB/month |
| Serverless function execution time | 900 seconds | 60 seconds |
| SSR execution region | Selectable | iad1 (US East) only |
| Team use | Allowed | Individual only |
| Commercial use | Allowed | Non-commercial only |
The SSR region restriction is particularly worth noting. On the Hobby plan, SSR requests from Japan travel across the Pacific and back, adding latency. By contrast, Cloudflare Pages (Workers) runs at edge locations worldwide including Tokyo, so SSR response times are faster on Cloudflare.
Migration Suitability Guide
| Condition | Suitability |
|---|---|
| Next.js 15 or below + API Routes only | ✅ Straightforward |
| Next.js 15 or below + Edge Middleware | ✅ Supported |
| Next.js 16 + proxy.ts | ❌ Not yet supported (downgrade to 15 to proceed) |
| Internationalization libraries like next-intl | ✅ Works via middleware.ts |
| TCP-based libraries like Elasticsearch | ⚠️ May require rewriting to use fetch |
| Native modules like sharp | ❌ Does not work in the Workers environment |
Summary
- Vercel Hobby is for non-commercial personal use only; Pro is required for any organization app
- Moving just the organization apps to Cloudflare Pages allows the rest to run on Hobby
- Next.js 16's
proxy.tsis not yet supported by opennextjs-cloudflare; downgrading to 15 makes migration possible - Deploying to Pages requires copying
_worker.js, expanding the assets directory, and placing_routes.json - If you want to manage custom domains with existing DNS, use Pages rather than Workers
- Using git worktree to experiment safely before committing to the migration is recommended


Comments
…