Vercel
Run Frontguard against every Vercel preview deployment — including custom-domain previews — with a native integration.
Vercel integration
Frontguard's Vercel integration runs visual regression checks on every
preview deployment — *.vercel.app, custom domains, and branch aliases —
without any GitHub Actions workflow or npx step in your build. Install it
once on a team, and every PR gets a Frontguard / Preview check populated by
us.
This page is the user-facing guide for the integration. Engineers working on
the integration itself should also read
integrations/vercel/README.md
in the repo.
What you get
- A pass/fail check on every preview deployment, posted back to the linked PR by the Frontguard Cloud API.
- Support for custom-domain previews (e.g.
https://preview.acme.com,https://feature-x.staging.shop.io), not just*.vercel.app. - Routes and viewports configured once per project — no CI YAML.
- Idempotent ingestion: Vercel sometimes retries deliveries; we dedupe by delivery id.
How it compares to the CLI
| Concern | CLI / GitHub Actions | Vercel integration |
|---|---|---|
| Install effort | Add frontguard to npx/yaml | One-click on Vercel Marketplace |
| Per-project config | frontguard.config.ts | Project env vars (FRONTGUARD_*) |
| Triggers | CI job runs on push | Vercel webhook fires on success |
| Custom-domain previews | Resolves env var (VERCEL_URL) | Native — accepts any host |
| PR comments | Via GitHub token in CI | Via cloud-api github-pr reporter |
| Works without GitHub | No | Yes (Cloud-only) |
Both approaches use the same Cloud API and produce the same baseline. You can install the integration and also keep the CLI workflow if you want belt-and-braces — runs are deduped server-side by deployment URL + commit SHA.
Install
The Vercel Marketplace listing and the post-install callback
(/api/install) are both in review — the one-click flow below works
once the listing is approved. To run Frontguard against Vercel previews
today, use the GitHub Actions or CLI path from Where to find
Frontguard instead; both are available now.
Once the listing is live, installation is one-click:
-
Open the Frontguard integration page on the Vercel Marketplace.
-
Click Add Integration.
-
Choose the team or personal scope you want the integration installed on.
-
Pick the projects (or "all current and future projects") you want Frontguard to monitor. This is the consent boundary — only projects you select here will be allowed to have their deployment URLs (custom domains included) accepted by Frontguard.
-
Vercel sends you to the Frontguard post-install callback, which exchanges the code for an access token, creates your Frontguard team if it doesn't exist, and sends you on to the post-install screen.
-
Add three env vars to each project (Vercel → Project → Settings → Environment Variables):
Var Value FRONTGUARD_API_URLhttps://api.frontguard.devFRONTGUARD_API_KEYYour team's API key (from frontguard.dev settings) FRONTGUARD_ROUTESComma-separated routes, e.g. /,/about,/pricing
That's it — push a PR and you should see a Frontguard / Preview check
appear once Vercel's preview deployment goes green.
Trust model
The integration's job is to take a deployment URL Vercel hands us — possibly a custom domain we've never seen before — and decide whether it's safe to screenshot. We use a multi-layer check.
Layer 1: HMAC signature
Every webhook from Vercel carries an x-vercel-signature header that's an
HMAC-SHA1 over the raw request body, keyed by our integration's client
secret. We verify it before doing anything else. If the secret isn't
configured server-side, we fail closed — every webhook returns 500. If
the signature doesn't match, we return 401.
A valid signature proves the event was emitted by Vercel for an integration whose secret we hold — i.e. it's an installation that someone consented to create. That's the foundation everything else rests on.
Layer 2: install record lookup
Layer 1 alone isn't quite enough — a valid signature tells us some
installation exists, but not which projects the team meant to share with
us. So on install, we persist a per-configurationId record (and a
per-teamId index) into a KV store. When a webhook fires, the handler
looks up the project id and team id from the payload against KV:
project:<id>→ exact-match consent for that specific Vercel project.team:<id>→ consent at the team level (every project on the team).- First-time arrival → if a team install record exists, we lazy-register the project so subsequent lookups are O(1).
If neither key resolves and the URL isn't on *.vercel.app, the handler
rejects the webhook with HTTP 400. This is how revoking an installation
takes effect: deleting the KV record causes custom-domain webhooks for that
team to bounce immediately.
Layer 3: SSRF defense (always on)
A consenting user could still — accidentally or maliciously — point a custom domain at a private IP and use Frontguard as an SSRF gadget. So even with consent, the URL guard rejects:
- Anything that isn't
https:(http://,file://,javascript:, etc.) - Loopback hosts:
127.0.0.0/8,::1,localhost,localhost.localdomain - Link-local:
169.254.0.0/16— note this is the AWS / GCP cloud metadata endpoint at169.254.169.254. Allowing this would let any install exfiltrate cloud credentials. - RFC1918 private space:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - IPv6 unique-local:
fc00::/7 - IPv6 link-local:
fe80::/10 - CGNAT:
100.64.0.0/10 - Multicast / reserved:
224.0.0.0/4and up, plus0.0.0.0/8 metadata.google.internal,metadata
These checks run on the literal host in the URL. The Cloud-side fetcher is expected to do DNS-time SSRF validation as well (re-resolving the host inside the screenshot worker) — never trust a single layer.
Layer 4: idempotency
Vercel will sometimes redeliver the same event. When the payload carries a
delivery id, we record it in KV with a 24h TTL and short-circuit duplicate
deliveries with triggered:false, reason:"Duplicate delivery".
Event filtering
The handler only triggers a Frontguard run when all of these hold:
payload.typeisdeployment.succeededordeployment.ready(not.created, not.error, not.canceled)payload.payload.deployment.targetisnullorundefined— i.e. it's a preview deployment, never production orstagingpayload.payload.deployment.urlis non-empty
Production deployments are explicitly skipped. You almost never want a visual diff against the version that's live for your users — that's not a regression, that's the new baseline.
Configuring routes
Frontguard takes screenshots of routes you list explicitly. The integration
reads them from the FRONTGUARD_ROUTES project env var:
FRONTGUARD_ROUTES=/,/about,/pricing,/docsWhitespace is trimmed. Empty entries are dropped. If you don't set the var
at all, the Cloud API falls back to a single screenshot of /.
For more advanced shapes (per-route viewports, masked regions, network
overrides), drop a frontguard.config.ts at your project root and check it
into git. The integration ignores this file; it's read by the Cloud-side
crawler. See Configuration reference for the
full schema.
Git metadata + PR comments
The webhook payload carries Vercel's git metadata for the deployment under
payload.payload.deployment.meta. We extract:
githubCommitSha→ the deployed commit SHAgithubPrId/githubCommitRef→ PR number or branch refgithubOrg/githubCommitOrg→ repo ownergithubRepo/githubCommitRepo→ repo sluggithubCommitRef/gitCommitRef→ branch name
We forward these to the Cloud API under a github object on the POST /v1/run body. The API's github-pr reporter uses them to post the result as
a PR comment via the GitHub App you already authorized when connecting your
GitHub workspace. If the metadata isn't present (e.g. you deployed directly
from CLI), we skip the comment — the run still happens; results are
viewable in the Frontguard dashboard.
Custom git providers (GitLab, Bitbucket) aren't mapped yet. Vercel doesn't
populate githubCommitSha for non-GitHub repos, so we fall back to
gitCommitSha / gitCommitRef and post results only in the dashboard.
Follow #143 for
multi-provider PR comments.
Endpoints reference
| Method | Path | Purpose |
|---|---|---|
GET | /health | Returns {status:"ok", integration:"vercel"}. Use for uptime probes. |
GET | /api/install | OAuth landing (302 to Vercel) + callback handler (?code=…) |
POST | /api/webhook | Vercel deployment events. HMAC-required, fails closed. |
/api/webhook response shapes
A few examples of what callers see:
// 200 — preview triggered a run
{ "triggered": true, "runId": "run_abc", "previewUrl": "https://preview.acme.com" }
// 200 — preview ignored (production target)
{ "triggered": false, "reason": "Skipping production deployment" }
// 200 — duplicate delivery
{ "triggered": false, "reason": "Duplicate delivery (already processed)" }
// 200 — fully ignored (e.g. event type we don't act on)
{ "triggered": false, "reason": "Ignored event type: deployment.created" }
// 200 — signature valid, but Cloud API not yet configured
{ "triggered": false, "reason": "Frontguard API not configured (FRONTGUARD_API_URL / FRONTGUARD_API_KEY)" }
// 400 — preview URL failed the SSRF / authorization check
{ "triggered": false, "error": "Preview URL not allowed" }
// 401 — signature didn't match
{ "error": "Invalid signature" }
// 500 — VERCEL_CLIENT_SECRET not configured (fail-closed)
{ "error": "Webhook secret not configured" }Troubleshooting
"Preview URL not allowed" on every webhook
You're using a custom domain and the integration has no install record for the project. Three things to check:
- Re-open the Marketplace listing and confirm the project is in the "Authorized projects" list (or that the team install is set to "All projects"). If you toggled it off and back on, Vercel sometimes drops the project from the access list silently.
- Confirm KV is bound in your deployment. Without KV, custom-domain
webhooks are always rejected —
*.vercel.apppreviews still work. - Confirm the webhook payload's
project.idandteam.idaren't being stripped by a proxy. Hit/api/webhookfromcurl --verbosewith a sample signed payload to confirm what reaches the handler.
Webhooks return 401
x-vercel-signature didn't match the HMAC of the raw body keyed by
VERCEL_CLIENT_SECRET. The most common causes:
- The body was JSON-parsed by an upstream middleware before reaching the handler. The signature is over the raw bytes — re-serializing changes whitespace and breaks the MAC.
- You're using the wrong secret. Vercel rotates secrets on integration config changes; double-check the dashboard.
Webhooks return 500 with "secret not configured"
The handler is configured to fail closed when VERCEL_CLIENT_SECRET
isn't present. Set it on the deployment and redeploy.
Run was triggered but no PR comment appeared
The Frontguard Cloud API posts PR comments through the GitHub App you authorized when you connected your workspace. In the Frontguard dashboard, open Settings → Integrations → GitHub to confirm the App is installed on the repo. If the deployment came from a non-GitHub provider, the run will appear in the dashboard but no comment will be posted yet.
Production deployment was skipped — was that on purpose?
Yes. The handler explicitly skips target=production deployments. Visual
regression against the version that's currently live is rarely what you
want. To monitor production look + feel, use a production
monitor instead.
FAQ
Does the integration read my source code? No. It receives only deployment metadata (URL, commit SHA, branch, PR id) from Vercel. The Cloud-side worker fetches the deployed pages from your preview URL.
Does it cost anything beyond my Frontguard plan? The integration itself is free — runs it triggers count against your plan's monthly run budget like any other.
What happens if my Cloud API key rotates? Set the new key as
FRONTGUARD_API_KEY on the project. Runs queued with the old key during the
swap will return 502 from the integration with the underlying Cloud API
error; Vercel will retry the delivery automatically.
Can I install on multiple teams? Yes. Each install gets its own
configurationId and KV record. Removing one team's install doesn't affect
the others.
Can I run this without the Frontguard Cloud? Not yet — the integration
forwards to the Cloud API by design. For self-hosted Frontguard, point
FRONTGUARD_API_URL at your installation; the wire format is the same.
Is this open source? Yes — see
integrations/vercel/
in the monorepo.