# Migrate a TanStack Start app from Cloudflare to Vercel

**Author:** Ben Sabic

---

Moving a TanStack Start app from Cloudflare to Vercel mostly means swapping the deployment layer. On Vercel, TanStack Start runs on [Vercel Functions](https://vercel.com/docs/functions) with [Fluid compute](https://vercel.com/fluid) enabled by default, so your app automatically scales up and down with traffic.

This guide walks you through the full migration. You'll swap `@cloudflare/vite-plugin` for the [Nitro Vite plugin](https://vercel.com/kb/guide/nitro-vite-plugin), delete your Wrangler configuration, and move Cloudflare storage to its Vercel equivalents (e.g., R2 to Vercel Blob). It also covers recreating environment variables, mapping Cron Triggers and Queues to Vercel Cron Jobs and Vercel Queues, and deploying with Git or the `vercel` CLI.

## Prerequisites

Before you begin, make sure you have:

- A working TanStack Start app
  
- A [Vercel account](https://vercel.com/signup)
  
- Vercel CLI installed (`npm i -g vercel`)
  
- Node.js 20 or later
  

## Migrate with an agent

If you use an AI coding agent like Claude Code or Cursor, you can have it handle most of the migration for you and provide expert guidance. Install the [Vercel Plugin](https://vercel.com/docs/agent-resources/vercel-plugin) to provide your agent with Vercel-specific context, then add the companion skill for this guide.

**Install the Vercel Plugin:**

`npx plugins add vercel/vercel-plugin`

**Add the TanStack Start migration skill:**

`npx skills add vercel-labs/vercel-kb-skills --skill tanstack-start-cloudflare-to-vercel`

With both in place, ask your agent to migrate your TanStack Start app from Cloudflare to Vercel. Your agent will follow the migration steps and apply Vercel's recommended patterns for Vercel Functions, storage solutions, environment variables, and more.

## What changes when you move your project

On Cloudflare, TanStack Start runs as a Worker, and your server code interacts with Cloudflare Developer Platform services via [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/).

On Vercel, the same application runs on Vercel Functions using the Nitro Vite plugin, retrieves configuration from environment variables, and connects to [storage providers in the Vercel Marketplace](https://vercel.com/docs/marketplace-storage) via native integrations.

The table below maps each Cloudflare component to its Vercel counterpart.

| Cloudflare                          | Vercel                                                                                                                                                           |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Workers runtime                     | Vercel Functions (Fluid compute)                                                                                                                                 |
| `@cloudflare/vite-plugin`           | Nitro Vite plugin (`nitro/vite`)                                                                                                                                 |
| `wrangler.jsonc` or `wrangler.toml` | `vercel.json` (optional) and `nitro.config.ts`                                                                                                                   |
| `wrangler deploy`                   | Git push or the `vercel` CLI                                                                                                                                     |
| `env` from `cloudflare:workers`     | `process.env`                                                                                                                                                    |
| R2 (object storage)                 | [Vercel Blob](https://vercel.com/storage/blob)                                                                                                                   |
| Workers KV                          | [Redis from the Vercel Marketplace](https://vercel.com/marketplace?search=Redis), or [Edge Config](https://vercel.com/storage/edge-config) for read-heavy config |
| D1 (SQL)                            | [Postgres from the Vercel Marketplace](https://vercel.com/marketplace?search=postgres)                                                                           |
| Cron Triggers                       | [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs)                                                                                                            |
| Queues                              | [Vercel Queues](https://vercel.com/docs/queues)                                                                                                                  |
| Workflows                           | [Vercel Workflows](https://vercel.com/workflows)                                                                                                                 |
| Durable Objects                     | No direct equivalent. Use a DB or Redis for shared state                                                                                                         |
| Workers AI (`env.AI`)               | [Vercel AI Gateway](https://vercel.com/ai-gateway) with [AI SDK](https://ai-sdk.dev/)                                                                            |

## Steps

### 1\. Replace the Cloudflare Vite plugin with Nitro

Vercel deploys TanStack Start using Nitro, which compiles your app into Vercel Functions. Install Nitro in your project root:

`npm i nitro`

Then update `vite.config.ts` to replace the Cloudflare plugin with the Nitro plugin:

`import { defineConfig } from 'vite'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import viteReact from '@vitejs/plugin-react'; export default defineConfig({ plugins: [tanstackStart(), nitro(), viteReact()], });` Remove the `cloudflare()` plugin and its `viteEnvironment` option. Nitro detects Vercel during a Vercel build and applies the `vercel` preset without any extra configuration. ### 2\. Remove the Wrangler configuration Delete the Cloudflare-specific files and dependencies that no longer apply on Vercel: - Remove `wrangler.jsonc` (or `wrangler.toml`).    - Run `npm uninstall @cloudflare/vite-plugin wrangler`.    - Remove any `compatibility_date` or `compatibility_flags` settings. Vercel Functions run on Node.js, so the `nodejs_compat` flag has no equivalent.    If you used a custom server entrypoint (`src/server.ts`) to export Durable Objects or handle Queue and Cron events, see [step six](#6.-migrate-scheduled-tasks-and-queues) and the mapping table for the Vercel approach to those features.

### 3\. Update your build scripts

Replace the Wrangler deploy script in `package.json` with the standard Vite commands. Vercel runs the build for you, so you no longer need a `deploy` script that calls `wrangler`:

`{ "scripts": { "dev": "vite dev", "build": "vite build" } }`

You can also remove the `cf-typegen` script that generates types from your Wrangler configuration. Vercel auto-detects TanStack Start during import and sets the build command and output directory, so the remaining scripts mainly support local development.

### 4\. Replace Cloudflare bindings with Vercel storage

This is the core code change. On Cloudflare you import `env` from `cloudflare:workers` and call bindings such as `env.MY_BUCKET`. On Vercel, you read connection details from `process.env` and talk to each store through its SDK. Remove every `import { env } from "cloudflare:workers"` statement and replace the binding calls.

For example, an R2 upload on Cloudflare looks like this:

`// Before (Cloudflare R2) import { createServerFn } from '@tanstack/react-start'; import { env } from 'cloudflare:workers'; const uploadFile = createServerFn({ method: 'POST' }) .validator((data: { key: string; content: string }) => data) .handler(async ({ data }) => { await env.MY_BUCKET.put(data.key, data.content); return { success: true }; });`

On Vercel, the same upload uses Vercel Blob:

`// After (Vercel Blob) import { createServerFn } from '@tanstack/react-start'; import { put } from '@vercel/blob'; const uploadFile = createServerFn({ method: 'POST' }) .validator((data: { key: string; content: string }) => data) .handler(async ({ data }) => { const blob = await put(data.key, data.content, { access: 'public' }); return { url: blob.url }; });`

Install the Blob SDK with `npm i @vercel/blob`, then create a Blob store from the [Storage page](https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fstores) in your Vercel dashboard. For authentication, connect the store to your project from its Projects tab. Vercel then adds a `BLOB_STORE_ID` and a short-lived `VERCEL_OIDC_TOKEN` that it rotates automatically. The SDK pairs the two automatically, so the `put()` call above needs no token in your code. This [OIDC approach](https://vercel.com/docs/vercel-blob/using-blob-sdk#oidc-tokens-recommended) is recommended over the long-lived `BLOB_READ_WRITE_TOKEN`, which you'd use only for code that runs outside Vercel.

Map your other Cloudflare storage the same way:

- **Workers KV** becomes a Redis integration (e.g., [Upstash Redis](https://vercel.com/marketplace/upstash/upstash-kv)) from the Vercel Marketplace for caching and session data, or Edge Config for small, read-heavy configuration.
  
- **D1** becomes a Postgres database (e.g, [Neon](https://vercel.com/marketplace/neon)) from the Vercel Marketplace.
  
- **Durable Objects** have no direct equivalent; add shared state in a database or Redis.
  

When you provision storage from the Marketplace, Vercel adds the connection string and credentials as environment variables, which your code reads from `process.env`.

### 5\. Move your environment variables and secrets

Recreate your Cloudflare secrets and variables as Vercel environment variables. Cloudflare stores these in `wrangler.jsonc` vars and Wrangler secrets, while Vercel stores them per environment (production, preview, and development) in project settings.

Add each variable to your [project's environment variables](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fsettings%2Fenvironment-variables), or from the CLI:

`vercel env add DATABASE_URL production`

To run your app locally with the same values, link the project and pull the variables into a local `.env` file:

`vercel link vercel env pull`

`vercel env pull` writes a `.env` file with your [development environment variables](https://vercel.com/docs/environment-variables#development-environment-variables). Vercel supports up to 64 KB of environment variables per deployment across all variables combined.

### 6\. Migrate scheduled tasks and queues

If your Worker used [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or Queues, Nitro maps both to Vercel features at build time. Skip this step if your app doesn't use them.

For scheduled work, define Nitro scheduled tasks in `nitro.config.ts`. Nitro converts them into Vercel Cron Jobs during the build, so you don't write any `vercel.json` cron configuration by hand:

`import { defineConfig } from 'nitro'; export default defineConfig({ experimental: { tasks: true, }, scheduledTasks: { // Run the cms:update task every hour '0 * * * *': ['cms:update'], }, });` To secure the generated cron endpoint, set a `CRON_SECRET` environment variable in your Vercel project. When `CRON_SECRET` is set, Nitro validates the `Authorization` header on every cron invocation. For message processing, replace Cloudflare Queues with Vercel Queues. Define your topics under the `vercel.queues` key in `nitro.config.ts`: `import { defineConfig } from 'nitro'; export default defineConfig({ vercel: { queues: { triggers: [{ topic: 'orders' }], }, }, });` Handle incoming messages with the `vercel:queue` hook in a Nitro plugin under `server/plugins/`: ``export default defineNitroPlugin((nitro) => { nitro.hooks.hook('vercel:queue', ({ message, metadata }) => { console.log(`[${metadata.topicName}] Message ${metadata.messageId}:`, message); }); });`` To produce messages, install `@vercel/queue` (`npm i @vercel/queue`) and call its `send()` from any server function, for example `const { messageId } = await send('orders', order)`. For long-running, multi-step processes that Cloudflare Workflows handled, consider Vercel Workflows, which run durable steps on Vercel Functions and Vercel Queues. ### 7\. Deploy to Vercel You have two deployment options. Both build your app with Nitro's Vercel preset and run it on Vercel Functions. **Deploy with Git (recommended):** 1. Push your project to GitHub, GitLab, or Bitbucket.     2. In the [Vercel dashboard](https://vercel.com), select **Add New** \> **Project**, then import your repository.
   
3. Vercel detects TanStack Start and sets the build command and output directory. Confirm the framework preset, add your environment variables, and select **Deploy**.
   

If the framework preset or build settings aren't detected correctly, you can fix them from the Vercel CLI (requires v54.21.1+):

`vercel project update <project-name> --framework tanstack-start`

Use `--build-command` and `--output-directory` if those settings also need correcting.

After the first import, every push to your main branch creates a production deployment, and every pull request gets its own preview URL.

**Deploy with the CLI:**

`vercel`

Run `vercel` from your project root to create a preview deployment, or `vercel --prod` to deploy to production.

Once deployed, your app runs on Vercel Functions with Fluid compute, with preview deployments, observability, and the Vercel Firewall available.

## Troubleshooting

### "Cannot find module 'cloudflare:workers'"

Server code still imports the Cloudflare bindings module. Search your project for `cloudflare:workers` and replace each binding call with its Vercel equivalent. The `cloudflare:workers` import only resolves inside the Workers runtime.

### Environment variables are undefined at runtime

Confirm each variable exists in the correct environment under your [project's environment variables](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fsettings%2Fenvironment-variables), then redeploy. Variables added to production aren't available in preview or development unless you add them there too. For local runs, re-run `vercel env pull` after changing variables.

### API routes return a 404

Nitro's `/api` directory convention isn't compatible with Vercel. Move standalone API handlers to `routes/api/` so Nitro generates the correct Vercel Functions.

## Best practices

- **Tune function resources per route.** If specific routes need more memory or a longer timeout than the default, set `vercel.functionRules` in `nitro.config.ts` to override `maxDuration`, `memory`, or `regions` for matching route patterns.
  
- **Place functions near your data.** Set `regions` in `functionRules` or your project settings close to your database region to reduce latency.
  

## Related resources

- [TanStack Start on Vercel](https://vercel.com/docs/frameworks/full-stack/tanstack-start)
  
- [Vercel Functions](https://vercel.com/docs/functions) and [Fluid Compute](https://vercel.com/docs/fluid-compute)
  
- [Storage on the Vercel Marketplace](https://vercel.com/docs/marketplace-storage) and [Vercel Blob](https://vercel.com/docs/vercel-blob)
  
- [Managing environment variables](https://vercel.com/docs/environment-variables)
  
- [Deploy Nitro to Vercel](https://v3.nitro.build/deploy/providers/vercel)

---

[View full KB sitemap](/kb/sitemap.md)
