Vercel to Cloudflare migration: a step-by-step guide for 2026

A practical Vercel to Cloudflare migration guide: Workers or Pages, what to check first, the changes Workers need, Supabase for Postgres, moving your domain, and cutting over without downtime.

· 12 min read · The Orunbase team

A Vercel to Cloudflare migration comes down to five changes: build the app with a Cloudflare adapter instead of Vercel's, swap any Vercel storage for its Cloudflare or Supabase equivalent, move your environment variables into Worker variables and secrets, deploy from your own CI, and point your domain at the Worker. For most apps that is a day's work, and much of it is configuration rather than code. This guide goes through each step, with the things that break and how to fix them.

It is written from porting more than fifty templates from Vercel's gallery to Workers. Where the advice is ours rather than Cloudflare's, it says so. Cloudflare also publishes a Vercel-to-Workers migration guide with a per-framework section, which is worth keeping open beside this one.

Want it done for you? Orunbase runs the migrated app on your own Cloudflare account, with a Vercel-style workflow: start from one of more than fifty Vercel templates already ported to Workers, or bring your own app, and every merge to main deploys stage, then prod. Steps 2 to 8 below are what it sets up for you.

Before you start: what does your app use?

Spend ten minutes finding out what your app depends on before you change anything. Search the repository for:

  • vercel.json: rewrites, redirects, headers and crons.
  • @vercel/* packages: @vercel/blob, @vercel/kv, @vercel/postgres, @vercel/edge-config or @vercel/global-config, @vercel/og, @vercel/analytics.
  • process.env.VERCEL_*: code that reads Vercel's system variables, such as VERCEL_URL or VERCEL_PROJECT_PRODUCTION_URL.
  • export const runtime = "edge": routes that ask for Vercel's Edge runtime.
  • Database clients: pg, postgres, Prisma, Drizzle, @neondatabase/serverless, @supabase/supabase-js.
  • Long-running work: routes that set a large maxDuration, or do heavy processing inside a request.

Then list your environment variables with vercel env ls and pull their values for each environment with vercel env pull. You will need them later.

Cloudflare Workers or Cloudflare Pages?

Cloudflare has two products that can host a site from a Git repository, and search results still send people to guides for both. For a migration, choose Workers. Cloudflare's own docs now call Workers its primary platform for building applications and say to start new projects there.

A Worker serves your server code and your static files from one deployment (Workers Static Assets), and it can use every Cloudflare binding: R2, Workers KV, D1, Durable Objects, Queues, Hyperdrive and Cron Triggers. Pages still works, and a purely static site already on it has no reason to move, but there is no reason to start a migration there. Everything below assumes Workers.

Step 1: choose the adapter for your framework

Workers run your app through your framework's own Cloudflare adapter. Every major framework has one:

FrameworkOn Cloudflare Workers
Next.jsvinext (Cloudflare's recommendation for new apps, in beta) or OpenNext (@opennextjs/cloudflare)
Astro@astrojs/cloudflare
NuxtNitro's cloudflare_module preset
SvelteKit@sveltejs/adapter-cloudflare
React Router (and Remix, after upgrading to React Router 7)@cloudflare/vite-plugin
Hono, and other fetch-based APIsRuns natively
Static sites and SPAsWorkers Static Assets, no adapter needed

For everything except Next.js the choice is made for you, and the framework's Cloudflare guide covers the setup in a few lines.

Next.js: OpenNext or vinext?

Next.js has two paths, and the choice matters:

  • OpenNext adapts the output of next build to run on Workers. It is mature, supports the App Router and the Pages Router, server actions, streaming, middleware and incremental static regeneration (ISR), and is what every Next.js template we have ported uses. It requires the Node.js runtime, so remove export const runtime = "edge" from your routes, and it does not support Next.js's newer Node.js middleware.
  • vinext is Cloudflare's reimplementation of the Next.js API on Vite. Cloudflare's docs now recommend it for new Next.js apps on Workers, and npx vinext check reports how much of your app it supports. It is still in beta, and some features, such as partial prerendering, are in progress.

Our advice: for an existing production app, start with OpenNext, because it runs your real next build. Run npx vinext check as well, and revisit vinext when it leaves beta.

Step 2: configure Next.js with OpenNext

Install the adapter and Wrangler, Cloudflare's CLI:

pnpm add @opennextjs/cloudflare
pnpm add -D wrangler

Add a wrangler.jsonc at the app's root. This is the smallest useful one, with an R2 bucket for Next.js's cache:

{
  "name": "my-app",
  "main": ".open-next/worker.js",
  "compatibility_date": "2026-11-01",
  "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
  "assets": { "directory": ".open-next/assets", "binding": "ASSETS" },
  "observability": { "enabled": true },
  "r2_buckets": [{ "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "my-app-cache" }],
  "images": { "binding": "IMAGES" }
}

And an open-next.config.ts that tells OpenNext to keep its incremental cache in that bucket:

import { defineCloudflareConfig } from "@opennextjs/cloudflare/config";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";

export default defineCloudflareConfig({ incrementalCache: r2IncrementalCache });

Build with opennextjs-cloudflare build, preview locally with opennextjs-cloudflare preview, and deploy with opennextjs-cloudflare deploy.

Three Next.js features need a little more:

  • ISR and revalidateTag. Pages revalidated on a timer need a queue, and on-demand revalidation (revalidateTag, revalidatePath) needs a tag cache. OpenNext provides both as Durable Objects you add to open-next.config.ts. Our port of Next.js Commerce uses R2 for the cache, a Durable Object queue and a sharded Durable Object tag cache, and its config is a good template.
  • next/image. Images are optimised by Cloudflare Images through the IMAGES binding above. The first 5,000 unique transformations a month are free.
  • Bindings in code. Environment variables and secrets are available on process.env as before. To reach a binding such as an R2 bucket or a KV namespace, use getCloudflareContext().env.

Step 3: replace Vercel's storage

Each Vercel storage product has a direct equivalent on Cloudflare, and Postgres has one on Supabase:

On VercelOn Cloudflare and SupabaseNotes
Vercel BlobR2Ten GB a month free, no egress charges
KV (Upstash Redis)Workers KVEventually consistent; for counters and rate limits, use a Durable Object. Upstash's REST client also works on Workers if you would rather keep it
Postgres (Neon or Supabase)Supabase Postgres through HyperdriveSee step 5
Edge Config, now Global ConfigWorkers KV, or plain vars in the Wrangler config
SQLite or TursoD1
Cron JobsCron TriggersSee step 4

Moving the data itself is usually a one-off script: list and copy the objects from Blob to R2, or export and import the keys. Do it before the cutover, then run it once more just before you switch.

Step 4: move environment variables, secrets and cron

Workers have two kinds of configuration:

  • Variables (vars in wrangler.jsonc) for values that are not secret: site URLs, feature switches, public keys.
  • Secrets for everything else, set with wrangler secret put NAME, or in bulk from a JSON file with wrangler secret bulk. Locally, put them in a .dev.vars file that git ignores.

Replace Vercel's system variables as you go. VERCEL_URL and VERCEL_PROJECT_PRODUCTION_URL usually become a SITE_URL variable you set for each environment.

Cron jobs move from vercel.json to the Worker's own configuration:

"triggers": { "crons": ["0 3 * * *"] }

A Cron Trigger calls the Worker's scheduled handler rather than a URL. The simplest port keeps your existing cron route and has the scheduled handler call it, with the same secret your Vercel cron used to send. Cron runs are billed like any other request.

Step 5: connect Supabase Postgres through Hyperdrive

If your app uses Postgres, Supabase is the closest match to what Vercel's Marketplace gave you: Postgres, plus auth and storage if you want them, with a free plan for development and a Pro plan from $25 a month.

From a Worker there are two ways in, and most apps use both:

  • SQL, through Hyperdrive. Hyperdrive is Cloudflare's connection pooler and query cache. Your Worker talks to Hyperdrive with pg, postgres.js, Drizzle or Prisma, and Hyperdrive keeps warm connections to the database. Point it at Supabase's direct connection (db.<project-ref>.supabase.co, port 5432), not Supabase's own pooler: Hyperdrive does the pooling. This is the setup Orunbase itself runs on in production.
  • The Supabase API, through supabase-js. Auth, storage and row-level-security queries go over HTTPS, which works from any Worker.

Hyperdrive is available on the Workers Free plan, with 100,000 queries a day. Remember that Workers run close to your users while the database stays in one region, so keep the number of round trips per request low, and put the Supabase project in the region where most of your users are.

Step 6: know the Workers limits that bite

Most apps never meet these, but each one has caught at least one of our ports:

  • CPU time on the free plan. Workers Free allows 10 ms of CPU per request, which is not enough for server-rendering a Next.js page. Use Workers Paid ($5 a month minimum) for any server-rendered app; it allows 30 seconds by default and up to five minutes.
  • Memory. Each Worker isolate has 128 MB. Large in-memory work (image processing, big JSON documents) belongs in a queue, R2 or a dedicated service.
  • Startup time. A Worker's global scope must run within one second. Move expensive set-up into the request, and keep heavy browser-only libraries out of the server bundle.
  • Size. A Worker can be up to 64 MiB uncompressed on both plans, since Cloudflare removed its old compressed-size limit in September 2026. Static assets are limited to 20,000 files per version on Free and 100,000 on Paid, and 25 MiB a file.
  • No filesystem at request time. Code that reads files with fs while serving a request fails. Import the data at build time, serve it as a static asset, or keep it in KV or R2.
  • Subrequests. A request can make 50 outgoing fetches on Free and 10,000 on Paid.
  • No default User-Agent. fetch from a Worker sends none, and some APIs, GitHub's among them, reject requests without one. Set it yourself.

Step 7: set up environments and CI

On Vercel, every push deployed somewhere. On Workers, you decide where. A common shape is one Worker per environment (my-app-stage and my-app-prod), each with its own variables, secrets and resources, where a merge to main deploys stage, and prod follows once stage is healthy.

You can deploy with Cloudflare's Workers Builds, which connects to your repository and builds on Cloudflare (3,000 build minutes a month free, with a 20-minute build timeout), or from GitHub Actions in your own repository, where a job can run for up to six hours. Either way the deploy step is wrangler deploy with an API token scoped to Workers, kept as a CI secret.

For each environment you will also want:

  • Its R2 buckets, KV namespaces and Hyperdrive config created before the app deploys, ideally with Terraform.
  • Database migrations run before the code that needs them.
  • A smoke test after the deploy, so a broken stage stops prod.

Step 8: move the domain and cut over without downtime

Custom Domains on Workers need the domain's DNS on Cloudflare. Moving the DNS first, while the records still point at Vercel, means the switch to the Worker is a single record change later.

If the domain is registered with Vercel

  1. Add the domain to Cloudflare. Cloudflare imports the DNS records it can find and gives you two nameservers. Check its list against Vercel's, including MX and TXT records and any subdomains that point at other services.
  2. Point the nameservers at Cloudflare. In Vercel, open the domain's settings and set custom nameservers in the Nameservers section. Once they change, Vercel's DNS editor no longer applies, and you manage records in Cloudflare.
  3. Keep the Vercel records as DNS only (the grey cloud) until the cutover. Vercel's docs warn that proxied records can break its domain verification.
  4. Wait for the zone to show Active in Cloudflare. It usually takes minutes, sometimes up to 24 hours.
  5. Transfer the registration, if you want to. Using Cloudflare's nameservers does not require moving the registration. To move it, choose the transfer-out option for the domain in Vercel's Domains tab to get an authorization code, then enter it on Cloudflare's Transfer Domains page. The domain must have been registered or transferred at least 60 days earlier, a transfer adds a year to the registration at Cloudflare's at-cost price, and it can take up to 10 days.

If the domain is registered elsewhere

Add it to Cloudflare and change the nameservers at your registrar. The rest is the same.

Cut over

  1. Test on workers.dev first. Every Worker gets a *.workers.dev address. Run your full test pass there.
  2. Lower the TTL on the records that point at Vercel a day before.
  3. Switch the hostname to the Worker. Remove the record that points at Vercel and attach the hostname to the production Worker as a Custom Domain. Cloudflare creates the DNS record and the certificate.
  4. Keep the Vercel project running for a week. If something is wrong, pointing the record back is a single change.
  5. Remove the project from Vercel once traffic, logs and bills on Cloudflare look right.

Or skip the plumbing with Orunbase

Steps 2 to 8 are the same for every app, and that is the part Orunbase does for you. Start from one of the Vercel templates already ported to Cloudflare, or bring your own app, and you get:

  • the adapter, Wrangler config and caches set up for your framework;
  • R2, KV, D1, Hyperdrive and Supabase created per environment by Terraform in your repository;
  • CI in your own GitHub Actions that checks every pull request, deploys stage on merge, smoke-tests it, then deploys prod;
  • credentials minted for each deploy from your workspace's Cloudflare and Supabase connections, never stored in the repository;
  • every run recorded, with logs, in the console.

The app runs in your own Cloudflare and Supabase accounts, which bill you directly.

Checklist

  • List what the app uses: vercel.json, @vercel/* packages, VERCEL_* variables, edge routes, database clients.
  • Pull every environment's variables with vercel env pull.
  • Pick the adapter; for Next.js, OpenNext for an existing app, and run npx vinext check.
  • Remove runtime = "edge"; set up R2 (and Durable Objects if you revalidate) for ISR; bind Cloudflare Images.
  • Map Blob to R2, KV to Workers KV, Postgres to Supabase through Hyperdrive, cron to Cron Triggers.
  • Move variables to vars and secrets to wrangler secret.
  • Use Workers Paid for anything server-rendered.
  • Set up stage and prod, with a smoke test between them.
  • Test on workers.dev, move DNS, attach the domain, keep Vercel for a week.

Vercel to Cloudflare migration FAQ

How long does a Vercel to Cloudflare migration take?

For a typical Next.js, Astro, Nuxt or SvelteKit app, about a day, most of it configuration and testing. Add a day or two if you move data out of Vercel Blob, KV or Postgres. Transferring the domain's registration can take up to 10 days, but it runs in the background and the site does not wait for it.

Should I migrate to Cloudflare Pages or Cloudflare Workers?

Workers. Cloudflare recommends Workers for new projects, it serves server code and static files from one deployment, and it supports every Cloudflare binding. Pages remains fine for a static site that is already there.

Do I have to rewrite my Next.js app?

No. With OpenNext, your app is still built by next build, and the changes are mostly configuration: a Wrangler file, an R2 cache for incremental static regeneration, Cloudflare Images for next/image, and removing runtime = "edge" from routes. Code changes are usually limited to Vercel-specific packages and reading files at request time.

Is Cloudflare cheaper than Vercel?

Usually, when most of your bill is data transfer, static assets or functions that wait on other services, because Cloudflare does not charge egress on Workers or R2 and bills Workers on CPU time. A small app inside Vercel's included usage saves little. The line-by-line comparison shows each item.

How do I move a domain from Vercel to Cloudflare?

Add the domain to Cloudflare, set Cloudflare's nameservers in the domain's Nameservers section in Vercel, and wait for the zone to become active. To move the registration as well, get an authorization code from Vercel's Domains tab and enter it on Cloudflare's Transfer Domains page; the domain must be at least 60 days past its last registration or transfer.

Will the site go down during the migration?

No, if you move DNS to Cloudflare while the records still point at Vercel, test the Worker on its workers.dev address, and only then switch the hostname to the Worker. Keep the Vercel project running for a week so you can switch back.

Can someone do the migration for me?

Yes. Orunbase sets up the adapter, the Cloudflare and Supabase resources, CI in your own GitHub Actions, and stage and prod deploys on every merge, on your own Cloudflare account. You can start from a ported Vercel template or bring your own app.