Skip to content

Migrate a TanStack Start app from Netlify to Vercel

Move your TanStack Start app from Netlify to Vercel, replacing the Netlify Vite plugin with Nitro and deploying on Vercel Functions.

8 min read
Last updated June 3, 2026

Moving a TanStack Start app from Netlify to Vercel mostly means swapping the deployment layer. On Vercel, TanStack Start runs on Vercel Functions with Fluid compute enabled by default, so your app automatically scales up and down with traffic.

This guide walks you through the full migration. You'll swap @netlify/vite-plugin-tanstack-start for the Nitro Vite plugin, delete your netlify.toml, and move Netlify storage to its Vercel equivalents (e.g., Netlify Blobs to Vercel Blob). It also covers recreating environment variables, mapping Scheduled Functions and background work to Vercel Cron Jobs and Vercel Queues, and deploying with Git or the vercel CLI.

Before you begin, make sure you have:

  • A working TanStack Start app
  • A Vercel account
  • Vercel CLI installed (npm i -g vercel)
  • Node.js 20 or later

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 to provide your agent with Vercel-specific context, then add the companion skill for this guide.

Install the Vercel Plugin:

Terminal
npx plugins add vercel/vercel-plugin

Add the TanStack Start migration skill:

Terminal
npx skills add vercel-labs/vercel-kb-skills --skill tanstack-start-netlify-to-vercel

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

On Netlify, TanStack Start deploys to Netlify Functions, and your server code uses Netlify platform primitives such as Blobs and Scheduled Functions. 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 via native integrations.

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

NetlifyVercel
Netlify Functions (serverless)Vercel Functions (Fluid compute)
Netlify Edge FunctionsVercel Functions (Fluid compute), or Routing Middleware for request-time logic
@netlify/vite-plugin-tanstack-startNitro Vite plugin (nitro/vite)
netlify.tomlvercel.json (optional) and nitro.config.ts
Netlify CLI (netlify deploy)Git push or the vercel CLI
Netlify environment variablesVercel environment variables (process.env)
Netlify Blobs (file storage)Vercel Blob
Netlify Blobs (key/value data)Redis from the Vercel Marketplace, or Edge Config for read-heavy config
Netlify DB (Postgres via Neon)Postgres from the Vercel Marketplace
Scheduled FunctionsVercel Cron Jobs
Background Functions and Async WorkloadsVercel Queues
Netlify Image CDNVercel Image Optimization
Fine-grained caching (Cache API)Vercel CDN Cache and Nitro route rules
Netlify AI GatewayVercel AI Gateway with AI SDK
Netlify FormsNo direct equivalent. Use a form backend (e.g., Formspree) or a function that writes to storage

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

Terminal
npm i nitro

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

vite.config.ts
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 netlify() plugin and its import from @netlify/vite-plugin-tanstack-start. Nitro detects Vercel during a Vercel build and applies the vercel preset without any extra configuration.

If you're on an older TanStack Start version, remove its Netlify target as well. Versions 1.121.0 to 1.131.x set tanstackStart({ target: 'netlify' }) in vite.config.ts, and versions before 1.121.0 set preset: 'netlify' in app.config.ts. Delete either setting so Nitro can apply the vercel preset instead.

Delete the Netlify-specific files and dependencies that no longer apply on Vercel:

  • Remove netlify.toml, including its publish = "dist/client" setting. Vercel auto-detects the TanStack Start output through Nitro.
  • Uninstall the Netlify packages: npm uninstall @netlify/vite-plugin-tanstack-start. Also remove any @netlify/functions, @netlify/blobs, or @netlify/edge-functions packages once you've migrated your code.
  • If netlify.toml contained redirects, rewrites, or custom headers, recreate them as Nitro route rules or in vercel.json.

If you used a netlify/functions or netlify/edge-functions directory for standalone functions, or relied on Background Functions and Scheduled Functions, see step six and the mapping table for the Vercel approach to those features.

Replace any Netlify-specific scripts in package.json with the standard Vite commands. Vercel runs the build for you, so you no longer need a script that calls netlify deploy:

package.json
{
"scripts": {
"dev": "vite dev",
"build": "vite build"
}
}

Vercel auto-detects TanStack Start during import and sets the build command and output directory, so the remaining scripts mainly support local development.

This is the core code change. On Netlify, you import getStore from @netlify/blobs and call methods such as store.set() and store.get(). On Vercel, you read connection details from process.env and talk to each store through its SDK. Remove every import { getStore } from "@netlify/blobs" statement and replace the blob calls.

For example, a file upload with Netlify Blobs looks like this:

app/routes/index.tsx
// Before (Netlify Blobs)
import { createServerFn } from '@tanstack/react-start';
import { getStore } from '@netlify/blobs';
const uploadFile = createServerFn({ method: 'POST' })
.validator((data: { key: string; content: string }) => data)
.handler(async ({ data }) => {
const store = getStore('uploads');
await store.set(data.key, data.content);
return { success: true };
});

On Vercel, the same upload uses Vercel Blob:

app/routes/index.tsx
// 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 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 is recommended over the long-lived BLOB_READ_WRITE_TOKEN, which you'd use only for code that runs outside Vercel.

Netlify Blobs serves two roles, so map your usage based on how you used it:

  • Blobs used as a key/value store for app data becomes a Redis integration (e.g., Upstash Redis) from the Vercel Marketplace, or Edge Config for small, read-heavy configuration.
  • Netlify Database becomes a Postgres database (e.g., Neon) from the Vercel Marketplace.

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

Recreate your Netlify environment variables as Vercel environment variables. Netlify stores these in the dashboard, CLI, or API, while Vercel stores them per environment (production, preview, and development) in project settings. Variables you set only in netlify.toml were never available to Netlify Functions, so check the dashboard or run netlify env:list to find the full set before you move them.

Add each variable to your project's environment variables, or from the CLI:

Terminal
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:

Terminal
vercel link
vercel env pull

vercel env pull writes a .env file with your development environment variables. Vercel supports up to 64 KB of environment variables per deployment across all variables combined.

If your Netlify site used Scheduled Functions, Background Functions, or Async Workloads, Nitro maps the scheduled side to Vercel Cron Jobs at build time, and you use Vercel Queues for background message processing. Skip this step if your app doesn't use them.

On Netlify, you set a schedule by exporting a config object with a schedule cron expression from a function, or by declaring it in netlify.toml. On Vercel, 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:

nitro.config.ts
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 Netlify Background Functions and Async Workloads with Vercel Queues. Define your topics under the vercel.queues key in nitro.config.ts:

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/:

server/plugins/queues.ts
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 Netlify background work handled, consider Vercel Workflows, which run durable steps on Vercel Functions and Vercel Queues.

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, 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.

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.

Server code still calls Netlify-only APIs such as Netlify Blobs, the Netlify Functions Context object, or edge function imports. Search your project for @netlify/ and replace each call with its Vercel equivalent from the mapping table. These packages depend on Netlify's runtime and won't connect when your app runs on Vercel.

Confirm each variable exists in the correct environment under your project's environment 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.

Run vercel env pull to download a short-lived VERCEL_OIDC_TOKEN and BLOB_STORE_ID into your local .env, then keep Blob access inside server functions. With Vite, only variables prefixed with VITE_ reach client code, so reading blob credentials from the client won't work.

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

  • Recreate Netlify redirects, rewrites, and headers. If you relied on netlify.toml or redirects and headers files, move that logic into Nitro route rules or vercel.json so requests resolve the same way on Vercel.
  • 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 Marketplace database to reduce latency.

Was this helpful?

supported.