zazzy
Deployment

Deployment

Vercel, compose services, and the env matrix.

The generated app is a standard Next.js server — nothing in it is Vercel-specific. This guide is Vercel-first because that's the fastest path for Next.js, but the same four moves (build, set environment variables, point at a managed Postgres, run migrations) apply to any Node host: Fly, Render, Railway, or a container platform.

This mirrors the shipped guide

Every generated app carries its own docs/DEPLOY.md with these steps, pruned to the features you kept. This page is the always-complete version. The docs site itself deploys separately — see Deploying this docs site.

The four moves

  1. A production PostgreSQL database.
  2. The environment variables from .env.example, set in your host's project settings (never committed).
  3. prisma migrate deploy against that database, before the new build serves traffic.
  4. A Git remote connected to your host.

1 — Provision Postgres

Create a managed Postgres instance with any provider — Neon, Supabase, Vercel Postgres, Railway, or your own. Put its connection string in DATABASE_URL. Prefer the provider's pooled string for the running app and keep a direct (non-pooled) string for migrations.

2 — Apply the schema

Migrations are checked into prisma/migrations. Run them once per deploy:

prisma migrate deploy

Never run prisma migrate dev or db push against production — migrate deploy applies only committed migrations and never generates or resets anything. On Vercel, fold it into the build so every deploy is migrated:

// package.json
"build": "prisma migrate deploy && next build"

If you keep a separate direct string for migrations, point prisma migrate deploy at it via an env var such as DIRECT_DATABASE_URL.

3 — Deploy to Vercel

  1. Import the repository at vercel.com/new. The Next.js preset is detected automatically — no vercel.json needed.
  2. Add every variable from .env.example under Project → Settings → Environment Variables. At minimum:
    • DATABASE_URL — the pooled Postgres string.
    • BETTER_AUTH_SECRET — 32+ random chars (openssl rand -base64 32).
    • BETTER_AUTH_URL — the app's public URL (e.g. https://app.example.com).
    • NEXT_PUBLIC_APP_URL — same public URL, for canonical/OG metadata.
  3. Deploy. Vercel builds, runs prisma migrate deploy, and serves the app.

From local compose to production services

Local dev runs on docker-compose.yml; each service maps to a production counterpart. The compose file only contains services for the features you kept — deselect a feature and its service is rewritten out.

Compose serviceFeatureProduction counterpart
postgrescoreManaged Postgres (DATABASE_URL)
inngest (dev server)jobsInngest cloud app + signing keys
stripe (listen forwarder)billingA real Stripe webhook endpoint at /api/webhooks/stripe
miniofilesAny S3-compatible bucket (AWS S3, R2, B2)

The dev-only forwarders (inngest dev server, stripe listen) have no production equivalent to run — in production the real services call your app's endpoints directly, which is what the per-feature setup below configures.

Environment matrix

Every variable is validated in src/env.ts. Blank optional keys keep their feature dormant (the credential gate) — the app builds and runs without them. Set them to activate the feature in production.

Variable(s)FeatureRequired?Notes
DATABASE_URLcoreRequiredPooled Postgres connection string.
BETTER_AUTH_SECRET, BETTER_AUTH_URLauth (core)RequiredSecret is 32+ random chars; URL is the public origin.
NEXT_PUBLIC_APP_URLcoreRecommendedCanonical/OG URLs. Defaults to http://localhost:3000 in src/env.ts — set it to your public URL so production links and social cards are correct.
GITHUB_*, GOOGLE_* client id/secretauthOptionalA provider's button renders only when both of its vars are set.
INNGEST_EVENT_KEY, INNGEST_SIGNING_KEYjobsSet in prodLeave INNGEST_DEV unset in production.
RESEND_API_KEY, EMAIL_FROMemailSet to sendBlank ⇒ console transport. EMAIL_FROM must be on a verified domain.
STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRETbillingSet to billBlank ⇒ billing dormant. Webhook secret verifies inbound signatures.
S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEYfilesSet to uploadBlank ⇒ uploads dormant.
SENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN, SENTRY_ENVIRONMENT, NEXT_PUBLIC_SENTRY_ENVIRONMENT, LOG_LEVELobservabilityOptionalSentry is off when the DSN is empty; LOG_LEVEL defaults to info.

Per-feature production setup

Only the features you kept appear here (in a generated app, the pruned sections are gone).

Auth — OAuth providers (optional)

Register each production callback URL (https://your-domain/api/auth/callback/<provider>) in the provider's console and set its client-id/secret pair. Email/password sign-in needs none of this.

Billing (Stripe)

  1. Create your products/prices in Stripe and set STRIPE_SECRET_KEY + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.
  2. Sync the plan catalog from src/config/plans.ts: pnpm stripe:sync.
  3. Add a webhook endpoint at https://your-domain/api/webhooks/stripe, subscribe it to the subscription/invoice events, and copy its signing secret into STRIPE_WEBHOOK_SECRET. A missing or wrong secret means subscription state never updates.

Background jobs (Inngest)

  1. Create an Inngest app and set INNGEST_EVENT_KEY + INNGEST_SIGNING_KEY (leave INNGEST_DEV unset).
  2. Register the serve endpoint https://your-domain/api/inngest so Inngest can invoke your functions.

Transactional email (Resend)

Create a Resend API key (RESEND_API_KEY), verify your sending domain, and set EMAIL_FROM to an address on it. Unverified domains are rejected in production.

File uploads (S3)

Point the S3_* vars at any S3-compatible bucket, and allow the app's origin in the bucket's CORS policy so browser presigned uploads succeed.

Observability (Sentry)

Set SENTRY_DSN + NEXT_PUBLIC_SENTRY_DSN (plus the *_ENVIRONMENT vars) and LOG_LEVEL. Sentry stays fully disabled while the DSN is empty.

Post-deploy checklist

  • prisma migrate deploy ran against the production database.
  • BETTER_AUTH_SECRET is a fresh random value; BETTER_AUTH_URL / NEXT_PUBLIC_APP_URL are the public URL.
  • Sign-up, sign-in, and sign-out work against production.
  • Every third-party webhook (Stripe, Inngest) points at the production domain.

Deploying this docs site

The docs site (apps/docs) is a separate Next.js app — deploy it as its own Vercel project rooted at apps/docs, independent of any apps/web deployment. It needs zero external services and zero required env vars: search runs on a local Orama index built in-process from the content tree, and there is no database or auth. The only optional variable is NEXT_PUBLIC_DOCS_URL for canonical URLs and OG images; it defaults to the Vercel deployment URL.

On this page