Vercel
Every web surface we ship. 18 projects.
Authoritative: the vercel MCP server (in 5 of our repos) — use it to read build
logs, runtime errors and deployment status rather than asking the user to paste them.
Plus the bundled vercel-cli and deployments-cicd skills.
CLI
vercel link # connect the local folder to a project
vercel env pull .env.local # pull dev env vars down — do this first, always
vercel # preview deploy
vercel --prod # production
vercel logs <url> # runtime logs
vercel inspect <url> # build output + timings
vercel env pull is the correct way to get a working .env.local. Copying from a
teammate's laptop is how the three different Supabase key names got into the portfolio.
Environments
Three scopes: Development, Preview, Production. A variable set only for Production is absent in preview — which is why preview builds fail with "supabaseUrl is required" while production is fine.
Set per environment deliberately:
- Test/sandbox keys (Stripe
sk_test_, Base Sepolia RPC) → Development + Preview - Live keys → Production only
NEXT_PUBLIC_* changes need a redeploy, not just a save — they are baked in at
build time.
Vercel provides for free: VERCEL_ENV, VERCEL_URL, VERCEL_GIT_COMMIT_SHA,
VERCEL_PROJECT_PRODUCTION_URL. Use VERCEL_ENV to branch behaviour rather than
inventing your own flag.
const isProd = process.env.VERCEL_ENV === "production";
Preview deploys
Every push gets a URL. The trap: third-party allowlists do not know about it.
Add the wildcard preview domain to:
- Privy dashboard → allowed domains (
*.vercel.appor the project's preview pattern) - Alchemy → allowed referrers
- Supabase → Auth → redirect URLs
- Stripe → webhook endpoint (or use the CLI locally instead)
A login that works in production and silently fails in preview is almost always this.
Cron
// vercel.json
{ "crons": [{ "path": "/api/cron/scan", "schedule": "*/15 * * * *" }] }
Cron routes are publicly reachable URLs. Protect them:
if (req.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("no", { status: 401 });
}
Vercel sends that header automatically when CRON_SECRET is set. Limits: Hobby is
one cron/day; Pro allows minute-level. A 15-minute scanner needs Pro.
Functions have a max duration (10s Hobby, 60s+ Pro, configurable). A long scan must either stream, chunk across invocations, or move to Railway — which is exactly why our long-running P2P agent runs on Railway, not Vercel.
Build failures — the usual four
- Type errors. Vercel type-checks;
next devdoes not. Runnpx tsc --noEmitbefore pushing. - Missing env var in that environment.
supabaseUrl is required= the var is not set for Preview. - Case-sensitive imports. macOS is case-insensitive, the build container is not.
./Buttonvs./buttonbuilds locally and fails on Vercel. - Lockfile drift. Commit the lockfile; the build uses it exactly.
Gotchas
- Env var set for the wrong environment — the #1 preview failure.
NEXT_PUBLIC_without redeploy stays stale.- Preview URLs missing from third-party allowlists.
- Unprotected cron routes are public endpoints.
- Function timeout on long jobs.
- Case-sensitive filesystem in the build container.
.vercel/is gitignored — it holds the project link, and is per-developer.