Site
  • Company info — fill LEGAL_COMPANY_NAME, LEGAL_ADDRESS, and LEGAL_REGISTRATION_NUMBER in .env.local (and later in your production env). They render on legal pages and in invoices.
  • Legal pages — log in as admin, open /admin-dashboard/cms, and finalize /terms, /privacy, /legal. Also review the built-in /privacy-choices page so the public rights/opt-out copy matches your actual processors and advertising setup.
  • Cookie consent — the banner is wired by category (necessary / analytics / marketing). Verify the categories match what you actually load, and test that Global Privacy Control keeps marketing/targeted-advertising consent disabled.
  • Account deletion — review the supported account types, ownership/dependency checks and terminal-erasure limitations in Data Privacy & GDPR. Make sure process-account-deletions and its scheduler/worker are operating; an enabled job alone is not proof of complete erasure.

2. Production env vars

Two categories — they live differently:

TypeExamplesWhere to setTo change later
Build-timeNEXT_PUBLIC_*, including the selected provider's public browser token when requiredSet before pnpm run build — they are baked into the JS bundleRequires a new build/deploy
RuntimePAYMENTS_PROVIDER, the selected provider's API/webhook secrets, SUPABASE_SECRET_KEY, AI_LLM_TRANSPORT, selected AI credentialSet on the server / container envRestart the container; no rebuild needed

3. Deploy

Before publishing, run pnpm run qa:release and resolve its blockers. It checks Database/Auth certification separately from application startup and Docker builds. Follow Deployment for the full release gate.

Pick a deployment target:

4. Production payment provider and webhooks

Keep the provider selected during initialization. Do not switch an existing installation in place: persisted subscriptions, transactions, adjustments, customer bindings, and reconciliation retain their original provider identity. Stripe, Lemon Squeezy, and Paddle implement member checkout and customer portal locally. Production remains fail-closed until the selected installation has coherent mode-specific credentials, a complete catalogue, the exact webhook, all required local capabilities, and the required operator attestations. Paddle guest checkout remains intentionally unavailable.

  1. Confirm that production keeps the same PAYMENTS_PROVIDER selected during initialization. Copy only that provider's production credentials and catalogue bindings; never expose the selection or external catalogue identifiers to the browser.
  2. Create the provider's webhook endpoint at the exact query-free route listed below and configure the required event set documented in Payments & Billing.
  3. When Stripe is selected, explicitly pin API version 2026-07-29.dahlia. When Paddle is selected, configure its approved Default Payment Link. Lemon Squeezy and Paddle require their exact paid-customer portal attestations collected in sandbox.
  4. Copy the endpoint signing secret into the matching server-only variable from .env.example, restart, then run pnpm run check:payment-catalog. Run pnpm run test:staging only against the explicitly approved non-indexable test/sandbox origin; this workflow never sends Lemon Squeezy or Paddle readiness traffic with live credentials.
Selected providerProduction webhook route
Stripe/api/billing/webhooks/stripe
Lemon Squeezy/api/billing/webhooks/lemon-squeezy
Paddle/api/billing/webhooks/paddle

Webhook-secret rotation uses the selected provider's *_WEBHOOK_SECRET_PREVIOUS key only during the bounded cutover. The readiness probe rejects query strings and fragments. After signed deliveries succeed on the new endpoint, disable the old endpoint, remove the previous secret, and restart.

5. Observability

  • Error logs — set LOGS_ENABLED=true, generate a strong LOGS_SALT, pick a LOGS_RETENTION_DAYS. Logs surface in /admin-dashboard/logs.
  • Rate limiting and AI resilience — create an Upstash Redis database and paste UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN. Production uses it for distributed limits and shared LLM closed/open/half-open circuit state; the app refuses to boot without the shared rate-limit store unless the explicit single-instance override is enabled.
  • Scheduler runtime — for supabase_pg_cron, set app_settings.jobs_api_url to the public production endpoint. For postgres_pg_cron, no URL or HTTP secret is used; supervise pnpm jobs:worker beside the application. For external_runner, configure the runner to call the public endpoint with its Bearer secret.
  • Indexing — set NEXT_PUBLIC_INDEXABLE=true only after legal pages are real and placeholder text is gone.

6. Final smoke tests

Walk through these on the live URL before announcing the launch:

  1. Landing page renders, no console errors, branding correct
  2. GET /api/health returns 200
  3. Magic link arrives within 30 seconds in a real inbox
  4. Admin can reach /admin-dashboard
  5. The selected provider's test/sandbox checkout, signed webhook convergence, and customer portal were validated on the approved non-indexable environment before production promotion; Lemon Squeezy/Paddle require no live purchase in this workflow
  6. A chat message streams from the LLM and credits decrement
  7. Cookie banner appears for new visitors and respects choices
  8. Footer links expose /privacy, /privacy-choices, /terms, /legal, and Cookie settings
  9. Legal pages have your real company info, not placeholders
You are live when…
  • All smoke tests above pass for their documented production or approved sandbox target
  • The selected provider's signed webhooks deliver successfully (check its dashboard log)
  • Scheduled jobs ran at least once (check job_runs)
  • You sent a magic link to a teammate and they got in

Where to next?

The onboarding is done. From here, dive into specific features as you need them: