Skip to content
Back to skills

Vercel Deploy

ASecurity

Ship a Next.js 16 App Router project to production on Vercel end-to-end: preview → smoke test → a *staged* production deployment (`vercel --prod --skip-domain`) that serves no traffic yet → `vercel promote` → domains and DNS, plus a rollback runbook. Requires `stack.framework` in {`next`, `monorepo`} and `phase >= feature_complete`; sets `phase = "deployed"` on success. Idempotent. Triggers on: "deploy", "ship it", "go live", "push to production", "manda in produzione", "attacca il dominio", ...

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
developmentpythongobashnextjsnodetestinggitapidatabasesecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned October 19, 2026

npx -y skills add lukedj78/dev-flow --skill vercel-deploy --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Vercel Deploy?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Vercel Deploy
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lukedj78-vercel-deploy/badge)](https://www.skillsdirectory.com/skills/lukedj78-vercel-deploy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: vercel-deploy
description: 'Ship a Next.js 16 App Router project to production on Vercel end-to-end: preview → smoke test → a *staged* production deployment (`vercel --prod --skip-domain`) that serves no traffic yet → `vercel promote` → domains and DNS, plus a rollback runbook. Requires `stack.framework` in {`next`, `monorepo`} and `phase >= feature_complete`; sets `phase = "deployed"` on success. Idempotent. Triggers on: "deploy", "ship it", "go live", "push to production", "manda in produzione", "attacca il dominio", "rollback the deploy". Not for: the Vercel project config — `vercel.json`, region, env-var matrix, monorepo root directory are `module-add deploy` (run it first); the pre-deploy gates (`vercel-doctor`, `compliance-audit`, `shadscan`, `launch-audit`, `app-security`); CI and git hooks (`module-add ci`); an Expo/RN app (`rn-eas-deploy`); an eve agent (`eve deploy`).'
---

# vercel-deploy — ship the web app to production on Vercel

The web counterpart of `rn-eas-deploy`. It is the **only** skill that moves a web project to `phase = "deployed"`.

> **Versions checked 2026-08-04**: Vercel CLI `58.5.1`. Docs: <https://vercel.com/docs/cli/deploy>, <https://vercel.com/docs/cli/deploying-from-cli>, <https://vercel.com/docs/cli/promote>, <https://vercel.com/docs/cli/rollback>, <https://vercel.com/docs/deployments/promoting-a-deployment>, <https://vercel.com/docs/instant-rollback>, <https://vercel.com/docs/git>, <https://vercel.com/docs/domains/working-with-domains/add-a-domain>.

## Scope — read this first

This skill **ships**. It does not configure.

| Concern | Owner |
|---|---|
| `vercel.json`, function region, env-var matrix per environment, monorepo Root Directory, project linking | **`module-add deploy`** — run it first |
| husky, lint-staged, GitHub Actions | **`module-add ci`** |
| cost/perf gate before shipping | **`vercel-doctor`** |
| legal/privacy gate before shipping | **`compliance-audit`** |
| UI-quality / accessibility gate before shipping | **`shadscan`** |
| launch-readiness gate before shipping | **`launch-audit`** |
| code-security gate before shipping (dependencies, secrets, authorization) | **`app-security`** |
| preview → smoke → staged production → promote, domains + DNS, rollback, `phase = "deployed"` | **this skill** |

If `vercel.json` or `.vercel/project.json` is missing, do not improvise them here — say so and route to `module-add deploy`. Two skills writing the same config is how the region silently changes between runs.

## Contract

See `references/contracts.md`. Key facts:
- Reads `<project-root>/.workflow/meta.json#stack.framework` — must be `"next"` or `"monorepo"`.
- Requires `meta.json#phase >= "feature_complete"`.
- Sets `meta.json#stack.deploy = "vercel"` and `phase = "deployed"` **only after** the production domain actually serves the deployment.
- Records `stack_config.production_url` + `stack_config.vercel_project` and appends `history`.
- Idempotent: re-running on an already-deployed project becomes a *release* run (deploy → promote), and skips domain setup that is already verified.

## When this skill applies

- Phase is `feature_complete` (or `deployed`, for subsequent releases).
- The user says: "deploy", "ship it", "go live", "manda in produzione".
- The orchestrator routes here from `dev-flow`.

## Monorepo awareness

If `meta.json#stack.framework === "monorepo"`, the web app lives at `apps/web/` and the Vercel project's **Root Directory** is already set to `apps/web` by `module-add deploy`. Consequence: **run the CLI from the repository root**, not from `apps/web/` — with Root Directory configured the CLI is already scoped. `vercel.json` stays in `apps/web/`.

The mobile side ships separately via `rn-eas-deploy`, the agent side via `eve deploy`. All three can be at different points; `phase = "deployed"` for the project means every side that exists has shipped — say which sides remain.

---

## Workflow

### Step 1 — Preconditions

Read `.workflow/meta.json`. Abort with a specific message if:

- `stack.framework === "expo-rn"` → this is `rn-eas-deploy`'s job.
- `stack.framework` is neither `next` nor `monorepo` → refuse; alternative deploy targets are a stack decision, not a flag (see `module-add/references/module-deploy.md` § *Alternative deploy targets*).
- `phase < "feature_complete"` → the build isn't done; say which phase it is and what closes the gap.
- `stack.deploy !== "vercel"` or `<project-root>/.vercel/project.json` is missing → run `module-add deploy` first.

Then confirm the CLI can see the project:

```bash
pnpm dlx vercel@latest whoami            # who the CLI is logged in as
pnpm dlx vercel@latest project inspect   # details of the *linked* project
```

Never `vercel link` from this skill. Re-linking silently repoints the directory at a different project — that is how staging code reaches production.

### Step 2 — Confirm the five gates ran (propose, never block)

Read `meta.json#compliance` (legal), `meta.json#vercel_doctor` (cost/perf), `meta.json#shadscan` (UI quality + a11y), `meta.json#launch_audit` (commercial readiness: metadata, legal and identity pages, a reachable CTA) and `meta.json#security` (can the code be attacked: dependencies, secrets, authorization).

- Missing → tell the user which gate has never run and offer to run it now.
- Present but older than the last material change (compare against `history`) → offer a re-run.
- Present and current → state the findings summary in one line and move on.

No gate blocks a deploy. Report and let the user decide — that is the existing policy for all five, and this skill does not tighten it.

#### Also check observability (propose, never block)

Read `meta.json#stack.observability`. Empty: say that nobody will know when production breaks, and offer `module-add observability`, errors first. Present: run `python3 <skills>/module-add/scripts/observability_check.py <root>` and state its findings in one line (a `found` finding is named, not hidden). Either way the deploy goes on: the user decides.

#### Also check the Next.js security floor

The five gates read the *code*. `app-security`'s dependency scan sees `next` among the packages, but it reports advisories, not which support line you are on or the floor below — that check is here, and `app-security` points to it. Since 2026-07 that is its own risk: Next.js ships **pre-announced security releases roughly monthly**, with two maintained lines whose labels **move**: as of 2026-10-03 they are still **16.3.x Active LTS** and **15.5.x Maintenance LTS**, and both took the 2026-09-30 patches.

```bash
pnpm ls next --depth 0        # or: node -p "require('next/package.json').version"
```

Compare against the floor in `references/contracts.md` § `nextjs_version` (**`16.3.8`** Active LTS or **`15.5.27`** Maintenance LTS, as of 2026-10-03). Below it, say so in one line naming the CVEs that apply to *this* project. The sets, newest first: **2026-09-30** is eight CVEs, of which the one to read first is the **high SSRF in Image Optimization** (conditional on `images.remotePatterns` existing at all) and the one most likely to be ours is the medium **`'use cache'` root-param cache leak** — with `cacheComponents: true` it can serve a page cached for one `[locale]` under another, has **no workaround**, and sits exactly on golden rule ② plus `next/root-params`. **2026-09-22** is a **critical RCE in `next/og` ImageResponse** (`>= 16.2.0 < 16.3.6`). The August set is two critical unauthenticated RCEs (attacker-supplied **AVIF**, which the patch disables outright; **Windows** hosts mixing Pages and App Router), and the July set includes a DoS through **any** Server Action and an SSRF through request-derived `rewrites()`, so most projects we scaffold are in scope of something.

Two things not to get wrong:

- **Move to the current patch of a line that is still patched — and check which line that is.** In July, `16.2.x` was Active LTS and telling someone to jump to 16.3 was wrong. In August the label moved: 16.2 stopped at `16.2.12`, the criticals were fixed in `16.3.3`, and a project still on 16.2 is now unpatched. Read the line labels in the latest release post; never carry them from memory.
- **Hosting on Vercel is not a patch.** Vercel has deployed global WAF rules for past Next.js CVEs, and said in the same breath that they "should not be considered a complete substitute for upgrading". Report the floor for Vercel-hosted and self-hosted projects alike.

Then re-check the floor itself before quoting a number — it moves monthly. **Checked 2026-10-03: the
floor moved.** `16.3.8` is both the newest `16.3.x` and the newest 16.x stable (it is `latest`), and
`15.5.27` is the newest `15.5.x`. The previous record, `16.3.3`/`15.5.24`, had stood since 2026-08-25
through five releases and nine CVEs — **which is the argument for reading the advisory database and not
the version number**: `16.3.4` → `16.3.8` looks like ordinary patch drift and is not. The command for
that is `gh api repos/vercel/next.js/security-advisories`, and it answers from this session where
`nextjs.org` does not.

⚠️ **Several of the September advisories give their patched version as a literal `16.3.?`.** Do not
parse it and do not treat an unparseable range as "not affected" — take the floor from the newest
patch on the line instead.

**`npm view next versions --json` settles this in one command**, and settles it better than the blog:
the blog tells you what was announced, the registry tells you what you can actually install today.
Read the blog for *which line is still patched* — that label moves and the registry cannot tell you —
then take the patch number from npm. `[VERIFY]` against <https://nextjs.org/blog>. Same rule as every
gate here — report, don't block.

### Step 3 — Reproduce the platform build locally

```bash
pnpm build                                   # the same build Vercel will run
pnpm dlx vercel@latest env ls                # every expected var, in every expected environment
```

Two things to actually check in the `env ls` output, not just print:

- Every var the app reads exists in **Preview**, not only Production. A missing preview var is the single most common cause of "works locally and in prod, PR preview 500s".
- Vars that must differ per environment actually differ — `DATABASE_URL` above all. See `references/deploy-checklist.md`.

If `pnpm build` fails, stop. Do not deploy a build you could not reproduce.

### Step 4 — First deploy or subsequent release?

```bash
pnpm dlx vercel@latest ls --prod
```

This decides the path, and the difference is documented behaviour, not a preference:

> *"The first deployment of a new project is always a production deployment, even when you run `vercel` without `--prod`."*

- **No production deployment yet → path A (first deploy).** There is no way to make the first deployment a preview. Tell the user plainly: the first deploy goes to production, so smoke-testing happens *after* it exists and *before* the custom domain points at it. Attach domains in Step 9, after the deployment is verified — a `*.vercel.app` URL nobody knows is a safe place to be wrong.
- **A production deployment exists → path B (release).** Steps 5–8 in full.

### Step 5 — Preview deploy + smoke test (path B only)

```bash
pnpm dlx vercel@latest deploy
```

`stdout` is always the deployment URL — capture it. Then smoke-test the actual flows the release touches (auth, the changed route, one write path), not just "the homepage 200s".

If the project has **Deployment Protection** enabled — Standard Protection is the Hobby default and protects preview URLs — the URL asks for a Vercel login before it renders. That is expected; log in rather than "fixing" it. Note that with Standard Protection the generated production URL is restricted too, so anything reading `VERCEL_URL` / `NEXT_PUBLIC_VERCEL_URL` for a fetch must target the requested domain instead.

Do not proceed until the user confirms the preview is good.

### Step 6 — Staged production deployment

```bash
pnpm dlx vercel@latest deploy --prod --skip-domain
```

This creates a **production** deployment — real production env vars, real production build — that is **not** assigned to the custom domains. Production traffic keeps going to the current deployment.

`--skip-domain` must be used with `--prod`, and it overrides the project's **Auto-assign Custom Production Domains** setting for this deployment.

This is the safe shape and the one the docs point at: `vercel alias` explicitly is *not* the recommended way to put a deployment on a production domain — `--prod --skip-domain` → `promote` → `rollback` is.

### Step 7 — Smoke-test the staged deployment

The staged URL runs with production environment variables against production data. Re-check the same flows as Step 5, plus anything that behaves differently in production: payment webhooks against live keys, transactional email, anything branching on `VERCEL_ENV === "production"`.

This is the last cheap moment. After Step 8 the fix is a rollback.

### Step 8 — Promote to Current

```bash
pnpm dlx vercel@latest promote <staged-deployment-url>
```

The promotion assigns the production domains **without a rebuild**. Default timeout is `3m`; `--timeout=0` returns immediately and lets it finish server-side.

Verify it took:

```bash
pnpm dlx vercel@latest promote status
pnpm dlx vercel@latest ls --prod
```

A deployment that has been promoted once **cannot be promoted again** — to go back to it later you roll back, not promote.

### Step 9 — Custom domains (idempotent)

Skip entirely if `vercel domains verify` already reports the project's domains as configured.

Otherwise follow `references/domains-dns.md`. The short version:

```bash
pnpm dlx vercel@latest domains add <domain> <project>
pnpm dlx vercel@latest domains verify <domain> --project <project> --format=json
```

The DNS records themselves are added at the registrar, by the user — this skill reads the required records out of `domains verify` / the dashboard and hands them over. It never invents record values: the apex is an **A** record and each subdomain has a **project-specific CNAME target**, so a hardcoded `*.vercel-dns-*.com` value is always wrong for someone.

Vercel recommends `www` as the primary domain with a redirect from the apex, because the DNS spec forbids CNAME on an apex and a CNAME gives the CDN more control over incoming traffic. Say so, then do what the user decides — an apex-primary setup with an A record is explicitly supported.

Once a domain is configured it is **automatically applied to the latest production deployment**.

### Step 10 — Post-deploy verification + rollback runbook

```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://<production-domain>/
pnpm dlx vercel@latest inspect <deployment-url>
```

Then hand the user the rollback runbook from `references/rollback-runbook.md` — in the message, not just as a file path. The single fact that must be said out loud:

> After a rollback, Vercel turns **off** auto-assignment of production domains. Pushes to the production branch stop going live until you `vercel promote` a deployment.

Someone who rolls back at 2am without knowing that spends the next hour wondering why their fix isn't deploying.

### Step 11 — Update meta.json + commit

```json
{
  "stack": { "deploy": "vercel" },
  "stack_config": {
    "vercel_project": "<project-name>",
    "production_url": "https://<production-domain>",
    "production_region": "<fra1|iad1|…>"
  },
  "phase": "deployed",
  "history": [
    { "skill": "vercel-deploy", "ran_at": "<iso>", "deployment": "<url>", "domains": ["…"] }
  ]
}
```

Set `phase = "deployed"` **only after Step 10 confirms the production domain serves the new deployment**. A promote that timed out client-side is not a deploy.

Commit: `release: deploy <version> to production on Vercel`.

### Step 12 — Print the next-steps summary

- Production URL, and the deployment id now serving it.
- Which gates ran, and any finding the user chose to ship with.
- The rollback command, verbatim.
- For a monorepo: which sides have shipped and which have not (`rn-eas-deploy` for mobile, `eve deploy` for the agent).
- The maintenance loop: re-run `compliance-audit` + `vercel-doctor` + `shadscan` + `launch-audit` + `app-security` after material changes.

---

## Common anti-patterns (NEVER do)

- ❌ `vercel --prod` straight at a project that already serves traffic. Stage it, smoke it, promote it.
- ❌ `vercel alias` to put a deployment on the production domain. The docs name `promote`/`rollback` as the preferred production commands.
- ❌ `--prebuilt` for a Next.js app whose build reads System Environment Variables — they are missing at build time with that flag.
- ❌ `vercel link` from this skill. Linking belongs to `module-add deploy`.
- ❌ `phase = "deployed"` before the production domain actually serves the deployment.
- ❌ Assuming a rollback restores environment variables. It does not rebuild: env vars and cron jobs revert to the state of the rolled-back deployment.
- ❌ Leaving a project in a rolled-back state without telling the user that pushes to the production branch no longer go live.
- ❌ Hardcoding a `*.vercel-dns-*.com` CNAME target. It is per project.
- ❌ Deploying an app whose only route is the scaffold's default page. A green checkmark on an empty app proves nothing.

## References

- `references/deploy-checklist.md` — the pre-flight list: gates, build, env-var matrix, first-deploy vs release.
- `references/domains-dns.md` — apex vs `www`, A/CNAME records, verification, redirects, when `alias` is still right.
- `references/rollback-runbook.md` — instant rollback, plan limits, the auto-assign trap, undoing a rollback.
- `module-add/references/module-deploy.md` — the configuration this skill assumes exists.

## Updating meta.json (recommended pattern)

When this skill modifies state (artifact written, phase advanced, history appended), use the canonical script when available:

```bash
# Wherever dev-flow is installed (e.g. ~/.claude/skills/dev-flow/), invoke:
python3 .../dev-flow/scripts/update_meta.py <project-root> set-phase deployed
python3 .../dev-flow/scripts/update_meta.py <project-root> append-history \
    --skill 'vercel-deploy' --inputs '{...}' --outputs '{...}' --phase-after deployed
```

The script enforces phase monotonicity, normalizes legacy kebab-case aliases, and writes the canonical sha256 + timestamp into `meta.json#artifacts`. **Fall back to direct JSON editing only if the script is not on PATH** (and warn the user).

## Sources

- Vercel CLI: <https://vercel.com/docs/cli/deploy>, <https://vercel.com/docs/cli/deploying-from-cli>, <https://vercel.com/docs/cli/promote>, <https://vercel.com/docs/cli/rollback>, <https://vercel.com/docs/cli/alias>, <https://vercel.com/docs/cli/domains>, <https://vercel.com/docs/cli/list>, <https://vercel.com/docs/cli/inspect>
- Platform: <https://vercel.com/docs/deployments/promoting-a-deployment>, <https://vercel.com/docs/instant-rollback>, <https://vercel.com/docs/git>, <https://vercel.com/docs/deployment-protection>
- Domains: <https://vercel.com/docs/domains/working-with-domains/add-a-domain>, <https://vercel.com/docs/domains/working-with-domains/deploying-and-redirecting>

Files in this skill

  • SKILL.md19 KB
  • references/contracts.md80.5 KB
  • references/deploy-checklist.md8.3 KB
  • references/domains-dns.md5.4 KB
  • references/rollback-runbook.md5.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.