Deployment
Everything about getting code into production. Verified against main on 2026-08-18.
Deploy is git push to main. Never run vercel deploy, vercel --prod, or vercel build by hand.
A manual deploy produces a per-deployment *.vercel.app origin. Inngest Cloud can memorize that origin, keep accepting events, and then silently invoke nothing — background jobs die with no error anywhere. This is the single most expensive failure mode in the project.
Project identity
| Thing | Value |
|---|---|
| Repo | ScaledByDesign/launchpad-v2 (internal package name creative-ingest) |
| Host | Vercel, Git-integrated — push to main auto-deploys |
| Production URL | https://launch.lynkr.cc |
| Framework | Next.js 16.3.1 (App Router), React 18.3.1 |
| Package manager | npm (package-lock.json); there is no pnpm lockfile in the repo |
The custom-domain requirement
https://launch.lynkr.cc is not cosmetic branding — it is the only working URL for anything machine-to-machine.
Vercel deployment protection on this project is set to ssoProtection.deploymentType = all_except_custom_domains. Only a custom domain is exempt. The project's assigned .vercel.app domain is not exempt — it is gated like any hash URL.
Measured 2026-08-14 (from Vercel Protection Bypass):
| URL | Result |
|---|---|
GET https://launch.lynkr.cc/api/health | 200 |
PUT https://launch.lynkr.cc/api/inngest | 200 {"message":"Successfully registered"} |
GET https://launcpadv2-scaled.vercel.app/api/inngest | 302 → Vercel SSO gate |
APP_BASE_URL is set to https://launch.lynkr.cc, and src/app/api/inngest/route.ts pins Inngest's serveHost to it. A correctly registered app therefore advertises the custom domain no matter which host served the registration request.
/api/inngest is additionally on the PUBLIC_PREFIXES allow-list in src/lib/public-paths.ts — it is authenticated by Inngest's own request signature, not by a session cookie.
For day-to-day operation the Vercel protection-bypass secret is not required, provided Inngest Cloud syncs against launch.lynkr.cc. Provision it only if you additionally want ordinary browser access to preview deployments. Generation is a dashboard-only step — see Vercel Protection Bypass for why the automation token gets a 403 on that endpoint.
Migrations apply on deploy
Both migration ledgers run before next build, via the build script:
"build": "tsx scripts/migrate-on-deploy.ts && next build"
scripts/migrate-on-deploy.ts runs two targets in order, each gated on an env var:
| Target | Gate env var | Runner |
|---|---|---|
| Supabase | SUPABASE_DB_URL | scripts/migrate-supabase.ts |
| ClickHouse | CLICKHOUSE_URL | scripts/migrate-clickhouse.ts |
A configured database must migrate successfully or the build fails — a deploy cannot ship code whose schema was never applied. This exists because migrations 029 and 030 once shipped as code while their columns never existed live: the ClickUp auto-binder and public share links were dead on arrival and nothing surfaced it.
Production only. scripts/migrate-gate.ts decides first: migrations run only when VERCEL_ENV=production. Vercel Preview builds carry the same database credentials as Production, so they skip (logged) — a PR's new migrations apply only when it merges to main, and a preview whose code needs them may error until then. A Vercel build with a missing/unknown VERCEL_ENV fails. Outside Vercel (local, CI) the build skips unless MIGRATE_ON_BUILD=1.
The skip path is the risk. On a production build, if the env var is missing the migration is skipped and only logged. That means pending schema changes silently do not apply — so if you see the skip message in a production build log, treat it as a broken deploy, not a warning.
Source-reviewed migration heads on 2026-10-20: Supabase 20261016140000_count_no_account_orders (numbered files end at 062), ClickHouse 086. Re-list both migration directories before relying on these numbers; applied production heads require ledger verification.
You can also run either ledger by hand:
npm run migrate:supabase # scripts/migrate-supabase.ts
npm run ch:migrate # alias: npm run migrate:clickhouse
Environment variables
src/lib/env.ts is the primary validated application schema, but selected runtime modules, framework config, and deployment scripts also read environment variables directly. Use the environment scopes and a focused source search before changing deployment configuration.
Real values live in the Vercel env store, scoped per environment (Production / Preview / Development). Add a variable to the applicable source/config contract—Zod when application validation owns it, or the focused runtime/tooling contract for direct reads—and promote it to every environment that needs it.
Two gotchas:
- Setting
PLATFORM_TOKENsilently backfillsCLICKUP_WEBHOOK_SECRETandMARKETING_API_TOKENunless those were set individually. This is a real source of "why is this secret already working?" confusion. - Per-client platform credentials are not environment variables. They live encrypted in the vaults — see Credentials.
Variable inventory: Environment Variables and the grep-derived docs/docs/specs/env.md.
Inngest sync after deploy
Inngest only invokes functions belonging to an app it has synced.
The normal path is automatic. This project is linked to the Vercel marketplace integration (verified behaviourally 2026-08-07: a function dispatched right after a fresh deploy executed within 25s with no manual sync). You do not need to sync by hand on a routine deploy.
The fallback is npm run inngest:sync (scripts/sync-inngest.ts), for when an auto-sync fails (Inngest surfaces these as "unattached syncs"), when syncing from CI, or when pointing Inngest at a specific stable URL. It syncs APP_BASE_URL, never a per-deployment URL, for the SSO reason above.
Keep this script. The failure mode it remedies is invisible: inngest.send() keeps returning ok with a real event id even when nothing will ever execute, because an event key authorizes ingestion only — acceptance proves nothing about execution.
Reading a sync result correctly: modified: true on a manual PUT is not drift on SDK v3.54+. Check latestSync.status instead — "duplicate" is healthy — and the cron_liveness block in /api/admin/sync-health.
Changing a cron expression forces Inngest Cloud to recreate the stored schedule. That is the documented fix when a schedule silently stops firing (observed on sync-hyros-orders, 2026-08-17).
Plan concurrency cap is 5. A function config declaring concurrency above 5 gets a 400 at sync time and blocks registration of every function in the app — one bad function takes down all background work.
Post-deploy verification
Vercel reporting "Ready" means the build compiled and the migration gate passed. It does not mean your change is live, correct, or reachable.
Do all three, in order:
- Health check.
curl https://launch.lynkr.cc/api/health→ expect200. - Probe the changed behavior live. Hit the actual surface you changed on
launch.lynkr.ccand confirm the new behavior, not just a 200. - Confirm the artifact contains the change. For a background job, confirm the function appears in Inngest with its new shape after sync. For a schema change, query the live database for the new column — do not infer it from the build log.
For anything touching scheduled jobs, also check /admin/sync-health — specifically the cron_liveness block — before calling the deploy done.
Rollback
- Application code: promote the previous production deployment from the Vercel Deployments tab.
- Migrations do not roll back with the deployment. Promoting an older deployment does not un-apply a migration the newer build ran. Roll schema forward with a corrective migration; never try to reverse by redeploying.
See also
- Vercel Protection Bypass — protection-bypass generation, the
launch.lynkr.cc-only URL rule, and the measurement table. - The Inngest folder guide (
src/inngest/AGENTS.md) — the current sync model and per-function non-negotiables. - Sync failures — when scheduled jobs fail after a deploy.