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, Nitro auto-selects its vercel preset, and the output lands in Vercel's Build Output API format that the platform reads directly — no framework preset, build command, or output directory to configure by hand.
Start from the template
npx giget github:pitlane-tools/templates/vercel my-app scaffolds a working guest book app wired for this guide — see pitlane-tools/templates.
Configuration
npm add -D nitroyarn add -D nitropnpm add -D nitrobun add -D nitrodeno add -D npm:nitrovp add -D nitrovlt add -D nitronub add -D nitro// vite.config.ts
import { remix } from "@pitlane/dev";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [remix({ serverHandler: false }), nitro()],
});serverHandler: false hands dev-time request handling to Nitro's dev server, which runs the same fetch handler Nitro packages for production.
Server routes run as Vercel Functions on Fluid compute — standard Web APIs, 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 ships, 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.
Don't use plain vercel
A plain vercel / vercel --prod uploads source and builds on Vercel's infrastructure — skip it. The Vite build point stays in your CI.
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
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 ships.
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 skips @pitlane/dev and nitro/vite entirely — with no SSR and no clientEntry() boundaries there is nothing to transform or package, so no Remix- or Pitlane-specific Vite settings are needed. Plain Vite builds a static site Vercel serves from its CDN.
<!-- 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/ui";
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/ui",
},
}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, upload the artifact: vpx vercel build --prod && vpx vercel deploy --prebuilt --prod, or the same GitHub Actions workflow above.