---
title: "Deploy to Vercel"
description: "Run a Remix 3 app on Vercel with pitlane/vite-plugin-remix and the Nitro Vite plugin, which produces Vercel Functions through the Build Output API."
url: "https://pitlane.tools/deploy/vercel"
---

# Deploy to Vercel

Deploy a Remix 3 app to [Vercel](https://vercel.com) by composing [`remix()`](https://pitlane.tools/package/vite-plugin-remix/function/remix.md) with [`nitro/vite`](https://nitro.build). 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](https://vercel.com/docs/build-output-api) format that the platform reads directly. There is no **Framework Preset**, build command, or output directory to configure by hand.

> [!TIP]
>
> **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](https://github.com/pitlane-tools/templates).

## 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`:

```sh
npm add pitlane
```

With yarn, pnpm, bun, vp, vlt, or nub, run the same command with the manager's name in place of `npm`. With deno, put its name in place of `npm` and write each package as `npm:<package>`.

The [Pitlane package guide](https://pitlane.tools/guides/umbrella.md) lists the `remix` and `vite` versions it requires and the standalone scoped packages. Then install Nitro:

```sh
npm add -D nitro
```

With yarn, pnpm, bun, vp, vlt, or nub, run the same command with the manager's name in place of `npm`. With deno, put its name in place of `npm` and write each package as `npm:<package>`.

```ts title="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`](https://pitlane.tools/package/vite-plugin-remix/interface/RemixPluginOptions.md#serverhandler) 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](https://vercel.com/docs/functions) on Fluid compute, with standard Web APIs and scale-to-zero. Client assets are served from Vercel's CDN.

## Local development and preview

```sh
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:

```sh
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.

> [!WARNING]
>
> **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](https://vercel.com/account/settings/tokens)), plus `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` (both written to `.vercel/project.json` by `vercel link`).

```yaml title=".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](https://pitlane.tools/guides/spa.md) 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](https://pitlane.tools/guides/hmr.md#component-hmr), so it is worth keeping in a static build.

```html title="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>
```

```tsx title="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 />);
```

```jsonc title="tsconfig.json"
{
    "compilerOptions": {
        "jsx": "react-jsx",
        "jsxImportSource": "remix/component",
    },
}
```

```ts title="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`:

```json title="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](#deploy-with-github-actions) above.
