Going to production
The pre-launch checklist for a real deploy - required env vars, SEO flags, trusted origins, payment webhooks, and schema migrations.
Every required env var, @repo/config flag, and external sync to settle before your first real deploy. Each step links the page that owns the detail.
Never deploy a --demo build. Its schema step resets the database and force-pushes the schema on
every container start (node) or every build (vercel), wiping production data each time.
The checklist
Provision a database and cache. Production runs against services you own - self-hosted
Postgres/Redis or managed (Neon/Supabase, Upstash). pnpm infra is local development only and is
never part of a deploy. See Database and Caching.
Set the required env vars on the platform. @repo/runtime validates them at boot and throws on anything missing or malformed.
DATABASE_URL=... # Postgres connection
BETTER_AUTH_SECRET=... # min 32 chars
API_URL=https://... # the backend's public URL
REDIS_URL=... # or UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN on Upstash
INNGEST_APP_ID=...
INNGEST_EVENT_KEY=...
INNGEST_BASE_URL=https://...BASE_URL and TRUSTED_ORIGINS are schema-optional but required in practice. Full list in Environment variables.
Set TRUSTED_ORIGINS to your real domains, comma-separated, no trailing slash. It becomes
config.origins; without it CORS and auth reject every browser call. It defaults to localhost
origins only.
Fill the SEO config so crawlers and metadata resolve.
| Key | What it sets |
|---|---|
config.indexable | true allows crawling; false blocks robots (use for staging) |
config.siteName | Brand name in titles and metadata |
config.domain | Bare production domain |
config.baseUrl | Canonical origin (override with BASE_URL) |
config.seo.description | Default meta description |
See Marketing & SEO.
Check the client IP. A CDN in front of a directly exposed server is the one topology the
backend cannot detect - set TRUSTED_PROXY=cf-connecting-ip for Cloudflare. Platform edges,
containers, and same-host proxies are detected automatically, and one curl confirms it either
way. See Client IP.
Configure payment webhooks when config.payment.enabled, and confirm price IDs match packages/config/src/pricing.ts.
| Provider | Env vars |
|---|---|
Stripe (config.payment.provider: "stripe") | STRIPE_WEBHOOK_SECRET, STRIPE_SECRET_KEY |
Polar (config.payment.provider: "polar") | POLAR_WEBHOOK_SECRET, POLAR_ACCESS_TOKEN |
See Payments.
Add captcha and analytics keys. When config.captcha.enabled, set TURNSTILE_SECRET_KEY and
the site key it pairs with - NEXT_PUBLIC_TURNSTILE_SITE_KEY overrides the committed
config.captcha.siteKey, and a mismatched pair rejects every solved widget. Add any analytics
sub-keys you use. See CAPTCHA, Cookie consent and
Analytics.
Register the production Inngest app. Point the Inngest dashboard at your deploy's
/api/inngest endpoint so automated and event-driven jobs run. See Background
jobs.
Confirm migrations run. init already wires the schema step into the deploy - prepended to the owner app's start script on node, to vercel.json's buildCommand on Vercel. It applies pending migrations and then verifies the live schema matches the schema files, failing the boot if it does not. Run it by hand only for an out-of-band apply:
pnpm --filter @repo/database migrateCommit the generated migration with the schema change that produced it - a deploy applies only what is in packages/database/drizzle/. See Database.
Gate deploys on green, not on push. Prefer triggering the deploy from a CI job that runs after
the test jobs succeed (the generated Checks workflow) over the host's push webhook - a push
webhook deploys red commits. If you keep the webhook, never trigger a manual deployment while a
push deploy may be queued: two racing deploys can fail with 409 service already exists and leave
the service half-recreated. Trigger manually only when the deploy queue has been empty for several
minutes.
A daily Inngest cron (license-heartbeat, at a stable per-install time of day) POSTs your license
token and detected domain to generatesaas.com. It is by design - opt out with
eject rather than deleting the code.
Client IP
How the backend works out which proxy is in front of it on its own, and TRUSTED_PROXY - the override for the one topology it cannot see.
Customization
Rebrand the boilerplate by editing the identity, logo, social, and SEO fields in @repo/config plus your English i18n copy - one source of truth per surface.