Skip to content

Search the guides, deployment pages, and API reference.

Vercel

Deploy a Remix 3 app to Vercel by composing remix() with nitro/vite. Nitro packages the ssr environment's fetch handler for the deploy target. Vercel injects VERCEL=1 into builds, so Nitro auto-selects its vercel preset and emits Vercel's Build Output API format that the platform reads directly. There is no Framework Preset, build command, or output directory to configure by hand.

Configuration

The examples import from pitlane/* paths, which come from the pitlane package. The vercel template depends on scoped packages such as @pitlane/vite-plugin-remix, @pitlane/assets, and @pitlane/theme, so add pitlane before switching its imports. Put it in dependencies when app code imports runtime subpaths such as pitlane/assets:

npm
npm add pitlane
yarn
yarn add pitlane
pnpm
pnpm add pitlane
bun
bun add pitlane
deno
deno add npm:pitlane
vp
vp add pitlane
vlt
vlt add pitlane
nub
nub add pitlane

The Pitlane package guide lists the remix and vite versions it requires and the standalone scoped packages. Then install Nitro:

npm
npm add -D nitro
yarn
yarn add -D nitro
pnpm
pnpm add -D nitro
bun
bun add -D nitro
deno
deno add -D npm:nitro
vp
vp add -D nitro
vlt
vlt add -D nitro
nub
nub add -D nitro
vite.config.ts
import { remix } from "pitlane/vite-plugin-remix";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [remix({ serverHandler: false }), nitro()],
});

serverHandler: false hands request handling to Nitro: its dev server in vp dev, and its preview of the packaged output in vp preview. Both run the same fetch handler Nitro packages for production.

Server routes run as Vercel Functions on Fluid compute, with standard Web APIs and scale-to-zero. Client assets are served from Vercel's CDN.

Local development and preview

Terminal window
vp dev # dev server through Nitro's runtime
vp build # locally: portable .output build
vp preview # preview the build

Locally, Nitro builds its portable Node output; the Vercel-specific .vercel/output structure is produced when the build runs under vercel build (which sets VERCEL=1, locally or in CI).

Deploy with the CLI

Link the project once, then always deploy prebuilt. The artifact built on your machine (or CI) is the artifact that reaches production, and Vercel's build system never runs:

Terminal window
vpx vercel login
vpx vercel link
vpx vercel build && vpx vercel deploy --prebuilt # preview deployment
vpx vercel build --prod && vpx vercel deploy --prebuilt --prod # production

vercel build runs the Vite build locally with Vercel's environment applied (so Nitro emits .vercel/output), and --prebuilt uploads that directory as-is.

Deploy with GitHub Actions

Store three repository secrets: VERCEL_TOKEN (an access token), plus VERCEL_ORG_ID and VERCEL_PROJECT_ID (both written to .vercel/project.json by vercel link).

.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
permissions:
contents: read
deployments: write
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: voidzero-dev/setup-vp@v1.21.1
with:
cache: true
- run: vp install --frozen-lockfile
- run: vpx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- run: vpx vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- run: vpx vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

vercel build runs the Vite build with Vercel's environment applied (so Nitro emits .vercel/output), and --prebuilt uploads that directory as-is. The build you tested is the build that reaches production.

Environment variables

Define them per environment in the Vercel dashboard (Settings → Environment Variables) or with vercel env add. They reach the server handler as process.env.*. vercel env pull writes a local .env.local for development.

Client-only apps

A client-only Remix 3 app runs remix() in SPA mode and drops nitro/vite: server: false builds no server environment, so there is nothing for Nitro to package. The plugin's remaining job is component HMR, so it is worth keeping in a static build.

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Remix App</title>
<script type="module" src="/app/main.tsx"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
app/main.tsx
import { createRoot, on, type Handle } from "remix/component";
function App(handle: Handle) {
let count = 0;
return () => (
<button
mix={[
on("click", () => {
count++;
handle.update();
}),
]}
>
Count: {count}
</button>
);
}
createRoot(document.getElementById("app")!).render(<App />);
tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "remix/component",
},
}
vite.config.ts
import { remix } from "pitlane/vite-plugin-remix";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [remix({ server: false })],
});

vite build emits the site into dist/, which Vercel's Vite preset picks up. Add the SPA fallback so deep links serve index.html:

vercel.json
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}

Deploys are unchanged. Build prebuilt, then upload the artifact: vpx vercel build --prod && vpx vercel deploy --prebuilt --prod, or use the same GitHub Actions workflow above.