This guide moves a Nuxt ↗︎ application from Vercel to Cloudflare Workers. Nuxt selects its Vercel preset automatically on Vercel. On Workers, you set the Nitro cloudflare_module preset instead.
Nuxt 3 and Nuxt 4 run on Nitro v2 (nitropack). Use the Nitro v2 Cloudflare documentation ↗︎, not the Nitro v3 documentation.
- Set
compatibilityDateinnuxt.config.tsto2025-07-17or later. With an earlier date,nuxt devdoes not emulate Cloudflare bindings. - Do not install
nitro-cloudflare-devonnitropack2.12 or later. Nitro includes the same development emulation, and both together start two proxies. - Review the steps in Before you begin that apply to every framework.
Copy this prompt into your coding agent. The agent stops before it deploys.
Migrate this Nuxt app from Vercel to Cloudflare Workers with the Nitro preset cloudflare_module (not cloudflare_pages). Nuxt 3 and 4 run on nitropack v2. Check with `<pm> why nitropack`, and use https://github.com/nitrojs/nitro/blob/v2/docs/2.deploy/20.providers/cloudflare.md, not the Nitro v3 documentation.
0. Install dependencies with the repository's package manager. With pnpm 10 or later, if install fails with ERR_PNPM_IGNORED_BUILDS, run `pnpm approve-builds --all` and commit pnpm-workspace.yaml.
1. Remove Vercel:
- Remove nitro.preset 'vercel', nitro.vercel.*, and @vercel/* packages.
- Port redirects and headers from vercel.json to routeRules, or to public/_redirects and public/_headers. Then delete vercel.json and .vercel/.
- List every routeRules entry with `isr`, which only works on Vercel. Replace each one with prerender: true, swr, or cache-control headers, and tell me about the behavior change.
2. Install wrangler as a dev dependency. Do not add nitro-cloudflare-dev on nitropack 2.12 or later.
3. In nuxt.config.ts, set compatibilityDate to today. It must be 2025-07-17 or later. Add nitro: { preset: 'cloudflare_module', cloudflare: { deployConfig: true, nodeCompat: true } }.
4. Create wrangler.jsonc with only "$schema", "name" (the repository name), "compatibility_date": "<today>", "observability": { "enabled": true }, and any bindings or vars. Do not set "main" or "assets", because Nitro generates them in .output/server/wrangler.json. Nitro also adds no_nodejs_compat_v2 on purpose. Do not add nodejs_compat. A compatibility date of 2026-08-04 or later already enables it. Add .wrangler and .dev.vars* to .gitignore.
5. Read bindings with `event.context.cloudflare.env`. Read environment variables with useRuntimeConfig(event) inside handlers, never at module scope. Run `npx wrangler types`.
6. If @nuxt/image uses the vercel or ipx provider, switch to the cloudflare provider and set image.cloudflare.baseURL to the production origin. ipx depends on sharp, which does not run on Workers.
7. For crons, add triggers.crons to wrangler.jsonc and a Nitro plugin that hooks 'cloudflare:scheduled'.
8. Set these scripts: preview="<pm> run build && wrangler dev", deploy="<pm> run build && wrangler deploy", and cf-typegen="wrangler types".
9. Cloudflare applies every matching _headers rule, so the cache-control values from Nitro stack. Add a nitro:init hook that registers a compiled hook. In it, rewrite .output/public/_headers from nitro.options.routeRules, sorted from least to most specific, with " ! <name>" before each " <name>: <value>" line. If public/_headers exists, merge it in.
10. Run `<pm> run preview --port <port>`. Check SSR pages, API routes, an error page, and images. `curl -I /_nuxt/builds/latest.json` must return exactly "public, max-age=1, immutable". In `nuxt dev`, confirm that a server route can read `event.context.cloudflare.env`.
Rules: work on a new git branch; store local secrets in a gitignored .dev.vars file with placeholder values; never print secret values; do not deploy, log in, or change DNS.
When you finish, list the files you changed, the Vercel features without an equivalent, the secrets I must set with `npx wrangler secret put <NAME>`, and the deploy command.-
Install Wrangler:
npm i -D wrangleryarn add -D wranglerpnpm add -D wranglerbun add -d wrangler -
In
nuxt.config.ts, updatecompatibilityDateand set the Nitro preset:nuxt.config.tsts export default defineNuxtConfig({ compatibilityDate: "2025-07-17", nitro: { preset: "cloudflare_module", cloudflare: { deployConfig: true, nodeCompat: true, }, }, });Use today's date, or any date after
2025-07-17. -
Create a Wrangler configuration file without
mainorassets. Nitro generates both when you build.{ "$schema": "node_modules/wrangler/config-schema.json", "name": "<YOUR_PROJECT_NAME>", // Set this to today's date "compatibility_date": "2026-11-06", "observability": { "enabled": true, }, }"$schema" = "node_modules/wrangler/config-schema.json" name = "<YOUR_PROJECT_NAME>" # Set this to today's date compatibility_date = "2026-11-06" [observability] enabled = true -
Add these scripts to
package.json:package.jsonjson { "scripts": { "preview": "npm run build && wrangler dev", "deploy": "npm run build && wrangler deploy", "cf-typegen": "wrangler types" } } -
Replace any
isrroute rules. If you use@nuxt/image, switch to the Cloudflare provider ↗︎. Read bindings withevent.context.cloudflare.env. -
Add the cache header fix.
-
Build and preview your application locally:
npm run previewyarn run previewpnpm run preview -
Delete
vercel.jsonand deploy your application:npm run deployyarn run deploypnpm run deploy
Cloudflare applies every _headers rule that matches a path. The rules that Nitro generates for /_nuxt/ overlap, so files such as latest.json receive two Cache-Control values. This hook rewrites the generated file so the most specific rule wins:
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
export default defineNuxtConfig({
hooks: {
"nitro:init"(nitro) {
nitro.hooks.hook("compiled", async () => {
const base = nitro.options.baseURL.replace(/\/$/, "");
const rules = Object.entries(nitro.options.routeRules)
.filter(([, rule]) => rule.headers)
.sort(([a], [b]) => a.split("/").length - b.split("/").length)
.map(([path, rule]) =>
[
base + path.replace("/**", "/*"),
...Object.entries(rule.headers!).flatMap(([name, value]) => [
` ! ${name}`,
` ${name}: ${value}`,
]),
].join("\n"),
);
await writeFile(
join(nitro.options.output.publicDir, "_headers"),
rules.join("\n") + "\n",
);
});
},
},
});If your project has a public/_headers file, merge its rules into the generated file instead of overwriting them.
nuxt devhas no bindings. YourcompatibilityDateis earlier than2025-07-17. Check that.nuxt/nitro.jsoncontains"preset": "cloudflare-dev".- The build warns that
mainandassetsare overridden. Removemainandassetsfrom your Wrangler configuration file. wrangler devreportsMissing entry-point to Worker script. Build your application first. Nitro generates the Worker entry file when you build.- The
ASSETSbinding is missing from generated types.wrangler typesreads your root Wrangler configuration file, not the configuration that Nitro generates. You can ignore this.