Guide · not normative
The Focus AI Standards
Guide: GDE-010
Status: Current
Relates to: STD-008
W. Schenk
The Focus AI
2026-07-25
Verified 2026-07-25

Vercel deployment

Status of this guide

This is a guide: explanation, walkthrough and reference implementation. It contains no clauses and binds nothing (STD-001 §4). The rules in this area are STD-008; where this document and a standard disagree, the standard is the authority.

When a guide turns out to contain a rule, the rule moves to a standard where it can be cited and checked, and the guide keeps the explanation.

Rules extracted to STD-008; this is the platform walkthrough.

Canonical pattern for Next.js apps on Vercel with GitHub previews, fnox-backed secrets, Neon, Vercel Blob, and Clerk. Reference implementation: the habitats repo (mise.toml vercel:sync, fnox.toml, Neon + Blob + Clerk).

For remote-server / pi / tmux setup see best-practices/GDE-006-local-environment.md. For Clerk domain and OAuth rules see best-practices/GDE-002-clerk.md. For the full fnox playbook see best-practices/GDE-003-fnox-secrets.md.

Defaults

ConcernStandard
HostVercel (GitHub integration)
Production branchmain → Production deployment
Other branches / PRsPreview deployments (*.vercel.app)
DatabaseNeon Postgres (DATABASE_URL pooled + DATABASE_URL_UNPOOLED when needed)
Object storageVercel Blob (BLOB_READ_WRITE_TOKEN)
AuthClerk — dev instance for local + Preview; production instance for Production only
Secrets1Password vault → fnox → mise run vercel:sync → Vercel env store
Runtime on VercelNative Vercel env vars only — do not run fnox in the Vercel build or serverless runtime

One-time project setup

1. GitHub + Vercel

  1. Create a Vercel project linked to the GitHub repo.
  2. Settings → Git → Production Branch = main.
  3. Leave Preview Deployments enabled for non-main branches and pull requests.
  4. If the Next app is not at the repo root (e.g. app/ or web/):
  5. Settings → General → Root Directory = that folder.
  6. Enable including source files outside the root when using a pnpm workspace.
  7. Add "npm:vercel" = "latest" (or a pin) to mise.toml tools; run mise install.
  8. From the app directory: vercel link once. Keep .vercel/ gitignored.

2. vercel.json

Commit a minimal config next to the Next app (adjust path for monorepos):

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "framework": "nextjs"
}

Add crons only when the app needs them. Cron handlers must verify Authorization: Bearer $CRON_SECRET and stay on the public middleware allowlist.

3. Required mise tools and tasks

Pin Vercel CLI via mise. Copy the habitats-style tasks (see best-practices/GDE-003-fnox-secrets.md for the full sync script):

TaskPurpose
mise run setupWrite .fnox/env with the project’s 1Password service-account token
mise run secrets:check / secrets:listVerify fnox resolution
mise run secrets:seed-from-vercelOne-time: pull Production env into the 1P vault
mise run vercel:syncPush every fnox-resolved secret to a Vercel env scope
mise run vercel:deployvercel deploy --prod (or rely on Git push to main)
mise run vercel:previewvercel deploy (ad-hoc preview)

vercel:sync must use fnox export -f json and vercel env add "$KEY" "$ENV" --value "$VALUE" --yes. Never pipe values on stdin — empty strings get stored silently.

mise run vercel:sync                      # → Vercel Production (default)
mise run vercel:sync -- --env preview     # → Vercel Preview
mise run vercel:sync -- --env development # → Vercel Development / `vercel dev`

4. fnox.toml

Declare every runtime env var the app needs. Item title in 1Password = env var name. Non-secrets may use default = "...".

Minimum set for a typical app:

Env varRole
DATABASE_URLNeon pooled connection string
DATABASE_URL_UNPOOLEDNeon direct URL (migrations, LISTEN/NOTIFY, session features)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYClerk publishable key
CLERK_SECRET_KEYClerk secret key
CLERK_WEBHOOK_SECRETSvix signing secret (if webhooks are used)
BLOB_READ_WRITE_TOKENVercel Blob read/write token
CRON_SECRETBearer for Vercel Cron (if crons exist)
NEXT_PUBLIC_APP_URLCanonical public URL for the environment

Optional but common: AI_GATEWAY_API_KEY, app-specific secrets, JWT template name as a fnox default.

Keep a committed .env.example that lists the same keys with comments for local / Preview / Production — no real values.

Environment matrix

Vercel has three scopes. Map them deliberately:

Local / DevelopmentPreview (PR / branch)Production
Triggerpnpm dev / vercel devPush to non-main or open PRPush to main (or vercel:deploy)
Sync target--env development--env preview--env production (default)
1PasswordProject vault (or … - Dev)Prefer … - Preview / Staging vault… - Prod vault
NeonDev branch or localStaging / preview branchProd branch
ClerkDev instance pk_test_ / sk_test_Same dev instanceProduction instance pk_live_ / sk_live_
BlobDev store tokenPreview store token (or shared non-prod)Prod store token
NEXT_PUBLIC_APP_URLhttp://localhost:3000Preview URL or branch aliasCustom domain
CLERK_AUTHORIZED_PARTIESOften unsetBlank or known aliasLocked to the prod origin

Blast radius: Prefer separate 1Password vaults (and Neon branches) for Preview vs Production so a leaked preview token cannot read prod. Use fnox profiles (FNOX_PROFILE=preview / prod) locally when syncing; see best-practices/GDE-003-fnox-secrets.md § Staging vs production isolation. Habitats currently runs a single vault with profiles commented out — fine for dogfood, not the long-term default for client work.

GitHub branch previews

  1. Connect the repo in the Vercel dashboard (Git integration).
  2. Every push to a non-production branch and every PR gets a Preview deployment built with the Preview env scope.
  3. After changing Preview secrets: mise run vercel:sync -- --env preview, then push or redeploy so the build picks up new values.
  4. NEXT_PUBLIC_* vars are baked at build time. Changing them requires a new deployment (Git push or vercel --prod --force / preview redeploy).
  5. Allow preview origins where the framework requires it (e.g. Next server actions allowedOrigins including *.vercel.app).

Neon

# Example: migrate prod from laptop (fnox or explicit URL)
DATABASE_URL="$(fnox get DATABASE_URL_UNPOOLED)" pnpm db:migrate

Vercel Blob

  1. Create a Blob store in the Vercel project (or reuse per-environment stores).
  2. Put BLOB_READ_WRITE_TOKEN in the matching 1Password vault.
  3. Sync with vercel:sync for each env scope that needs storage.
  4. Prefer private blobs; serve through authenticated app routes (signed URL or stream), not public world-readable URLs for client data.
  5. Health checks should report whether the blob token is present (boolean), not the token value.

Clerk: production vs staging / preview

Full rules: best-practices/GDE-002-clerk.md. On Vercel specifically:

Preview + localProduction
Clerk instanceDevelopment (*.clerk.accounts.dev)Production instance on the client custom domain
Keyspk_test_… / sk_test_… in Development + Preview scopespk_live_… / sk_live_… in Production scope only
Social OAuthClerk shared dev credentialsYour Google/Microsoft apps (see best-practices/GDE-002-clerk.md)
DomainsAdd needed *.vercel.app / localhost in the dev Clerk instanceCustom domain + Clerk CNAMEs (DNS-only / grey cloud on Cloudflare)
authorizedPartiesUsually omit (preview URLs vary) or list a stable branch aliasLock to https://<prod-host>
WebhooksOptional; point at a stable preview URL or disableProd URL + CLERK_WEBHOOK_SECRET in Production scope
MiddlewarePublic: sign-in/up, health, cron, webhooksSame; never put DEV_AUTH_BYPASS in Production

Never put pk_test_ / sk_test_ in Vercel Production. The #1 prod outage pattern is shipping the dev instance to the live domain.

Sanity-check a publishable key:

echo "${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY#pk_live_}" | base64 -d
# → clerk.<client>.thefocus.ai for live; *.clerk.accounts.dev means DEV

Practical sync model:

  1. Preview vault holds dev Clerk keys + staging Neon + preview Blob token.
  2. Prod vault holds live Clerk keys + prod Neon + prod Blob token.
  3. FNOX_PROFILE=preview mise run vercel:sync -- --env preview
  4. FNOX_PROFILE=prod mise run vercel:sync (production)

If the project still uses one vault (habitats today), keep Clerk live keys only on items that you sync to Production, and never run a blind sync of test keys into --env production.

Daily operations

# 1. Rotate or edit in 1Password
op item edit DATABASE_URL --vault "<Project - Prod>" password='…'

# 2. Push to the right Vercel scope
mise run vercel:sync
# or: FNOX_PROFILE=preview mise run vercel:sync -- --env preview

# 3. Ship
git push origin main          # preferred: Git → Production
# or: mise run vercel:deploy

# 4. Verify
vercel env ls
curl -sS "https://<host>/api/health"

Health and cron

Anti-patterns

New project checklist