GenerateSaaS

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.

KeyWhat it sets
config.indexabletrue allows crawling; false blocks robots (use for staging)
config.siteNameBrand name in titles and metadata
config.domainBare production domain
config.baseUrlCanonical origin (override with BASE_URL)
config.seo.descriptionDefault 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.

ProviderEnv 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 migrate

Commit 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.

On this page