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 pitlaneyarn
yarn add pitlanepnpm
pnpm add pitlanebun
bun add pitlanedeno
deno add npm:pitlanevp
vp add pitlanevlt
vlt add pitlanenub
nub add pitlaneThe Pitlane package guide lists the remix and vite versions it requires and the standalone scoped packages. Then install Nitro:
npm
npm add -D nitroyarn
yarn add -D nitropnpm
pnpm add -D nitrobun
bun add -D nitrodeno
deno add -D npm:nitrovp
vp add -D nitrovlt
vlt add -D nitronub
nub add -D nitroimport { 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
vp dev # dev server through Nitro's runtime
vp build # locally: portable .output build
vp preview # preview the buildLocally, 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:
vpx vercel login
vpx vercel link
vpx vercel build && vpx vercel deploy --prebuilt # preview deployment
vpx vercel build --prod && vpx vercel deploy --prebuilt --prod # productionvercel 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).
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.
<!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>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 />);{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "remix/component",
},
}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:
{
"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.