---
title: "Deploy on Vercel"
description: "Ship a MainSDK app on Vercel with secrets as references, a production overlay, mainsdk check on every pull request and a second network wall."
section: "guides"
canonical: "https://mainsdk.dev/docs/guides/deploy-vercel"
updated: "2026-10-28"
---

# Deploy on Vercel

> Ship a MainSDK app on Vercel with secrets as references, a production overlay, mainsdk check on every pull request and a second network wall.

Canonical: https://mainsdk.dev/docs/guides/deploy-vercel · Updated: 2026-10-28

Deploy Pinecart, a Next.js app on MainSDK, to Vercel with no key in your code and a check that blocks unsafe changes before they ship. MainSDK is a library, not a host, so Vercel still deploys it; [Ship with deploy config](https://mainsdk.dev/docs/guides/ship-with-deploy-config) writes `vercel.json` from `main.yaml`. This guide adds secrets as references, a production overlay, `mainsdk check` in CI and a second network wall.

**Before you start:**

- A Next.js App Router app with a `main.yaml` (see [Adopt an existing app](https://mainsdk.dev/docs/getting-started/existing-app) if you don't have one yet).
- A Vercel project linked to the repo, and the Vercel CLI.
- `npx mainsdk check` passing locally (see [Your first check](https://mainsdk.dev/docs/getting-started/first-check)).

## 1. Keep keys as references

`main.yaml` never holds a key. It names one with `secret:NAME`, and the `secrets` driver resolves it on the server at startup. On Vercel, the `env` driver reads Vercel environment variables.

```yaml title="main.yaml"
schema: 1
project: prj_01JC8Q4W7ZK3M5N8P2R6T9V0XY
baseline: main27
targets: { web: { framework: nextjs } }
secrets:
  driver: env                       # reads Vercel environment variables
auth:
  driver: github
db:
  driver: postgres
  regions: { us: secret:DATABASE_URL_US, eu: secret:DATABASE_URL_EU }
ai:
  driver: bedrock                   # IAM role, no key in code
config:
  driver: vercel-global-config      # settings change without a deploy
  schema: ./config.schema.json
jobs:
  driver: vercel-queues
overlays: { order: [market, segment, env] }
```

Add the values in Vercel, per environment:

```bash
vercel env add DATABASE_URL_US production
vercel env add DATABASE_URL_EU production
```

> **Warning:** A key in source, a committed `.env` file or a secret value in the browser bundle fails the check (`MAIN-SECRET-0002`, `MAIN-SECRET-0003`, `MAIN-SECRET-0004`). `.env.example` is allowed.

If your keys live in Vault, AWS, Google Cloud, Azure, Doppler or Infisical, change `secrets.driver` and keep the same `secret:` references. See [Secrets](https://mainsdk.dev/docs/interfaces/secrets).

## 2. Add a production overlay

Put production-only values in `main.env.prod.yaml`. Environment resolves at build time, so the production build carries only that layer. See [main.yaml and overlays](https://mainsdk.dev/docs/concepts/main-yaml).

```yaml title="main.env.prod.yaml"
network:
  default: deny
  allow: ["*.pinecart.dev"]
```

## 3. Check the production config

`mainsdk check --env prod` refuses any interface that still resolves to a local driver. If an earlier setup left `secrets.driver: local`, you see:

```bash
npx mainsdk check --env prod
```

```text
mainsdk check · pinecart · env prod · Main 27
✗ config  1 finding
  MAIN-CHECK-0026  critical  main.yaml
  fix: local driver in production: secrets. Set secrets.driver to env or a key service.
✓ secrets
✓ policy
✓ network
✓ data
1 gate failed · exit 1
```

Set `secrets.driver: env` as in step 1 and run it again until it ends with `all gates passed · exit 0`.

## 4. Check every pull request

Run the check in CI on each pull request, before Vercel promotes anything. The check is free and runs on your own runner.

```yaml title=".github/workflows/mainsdk.yml"
name: mainsdk
on: [pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npx mainsdk check --env prod --sarif mainsdk.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: mainsdk.sarif }
```

SARIF puts each finding inline on the pull request. Make the job a required status check, so a failed gate blocks the merge and the deploy that follows it. Exit codes are explained in [The check](https://mainsdk.dev/docs/concepts/the-check).

## 5. Add a second network wall

`network.allow` is enforced in the SDK for every outbound call. Export the same list as a firewall rule for an independent wall. The CLI writes Kubernetes rules with `--to k8s` and AWS security-group rules with `--to aws-sg`. Vercel and Cloudflare exports are planned, so on Vercel the SDK check is the wall for now:

```bash
npx mainsdk network export --to k8s
```

See [Network permissions](https://mainsdk.dev/docs/interfaces/network).

## 6. Verify the deploy

Open a pull request. The `mainsdk` job runs, and a red check blocks the merge. Once it is green and merged, Vercel deploys as usual. To confirm nothing secret reached the browser, run the check against the production build output:

```bash
npx mainsdk check --env prod
```

A secret value in a client chunk fails with `MAIN-SECRET-0004` and names the chunk.

## Optional: OIDC instead of a stored key

> **Note:** Sending telemetry to MainSDK Cloud is phase 2 (milestone 0.6). Nothing leaves your app before `mainsdk login`.

When you send telemetry to MainSDK Cloud, each environment normally needs an ingest key in `MAINSDK_KEY` (`msk_live_…` for prod, `msk_test_…` for dev and stage). On Vercel and in GitHub Actions the platform's OIDC token for the linked project or repo is accepted in place of the key.

```yaml title="main.yaml"
telemetry:
  to: [terminal, mainsdk]
  sample: 0.2
```

The Vercel-side setup for that OIDC trust (which audience and issuer to register) is not decided yet. Until it is, store `MAINSDK_KEY` as a sensitive Vercel variable.

> **Tip:** The app never waits on MainSDK Cloud. If it is down, telemetry is dropped and requests are served as usual. See [Fail static](https://mainsdk.dev/docs/concepts/fail-static).

## Next

- [Environment variables](https://mainsdk.dev/docs/reference/environment)
- [Secrets](https://mainsdk.dev/docs/interfaces/secrets)
- [Security model](https://mainsdk.dev/docs/concepts/security-model)
