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
- A production PostgreSQL database.
- The environment variables from
.env.example, set in your host's project settings (never committed). prisma migrate deployagainst that database, before the new build serves traffic.- 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 deployNever 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
- Import the repository at vercel.com/new. The
Next.js preset is detected automatically — no
vercel.jsonneeded. - Add every variable from
.env.exampleunder 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.
- 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 service | Feature | Production counterpart |
|---|---|---|
postgres | core | Managed Postgres (DATABASE_URL) |
inngest (dev server) | jobs | Inngest cloud app + signing keys |
stripe (listen forwarder) | billing | A real Stripe webhook endpoint at /api/webhooks/stripe |
minio | files | Any 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) | Feature | Required? | Notes |
|---|---|---|---|
DATABASE_URL | core | Required | Pooled Postgres connection string. |
BETTER_AUTH_SECRET, BETTER_AUTH_URL | auth (core) | Required | Secret is 32+ random chars; URL is the public origin. |
NEXT_PUBLIC_APP_URL | core | Recommended | Canonical/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/secret | auth | Optional | A provider's button renders only when both of its vars are set. |
INNGEST_EVENT_KEY, INNGEST_SIGNING_KEY | jobs | Set in prod | Leave INNGEST_DEV unset in production. |
RESEND_API_KEY, EMAIL_FROM | Set to send | Blank ⇒ console transport. EMAIL_FROM must be on a verified domain. | |
STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET | billing | Set to bill | Blank ⇒ billing dormant. Webhook secret verifies inbound signatures. |
S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | files | Set to upload | Blank ⇒ uploads dormant. |
SENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN, SENTRY_ENVIRONMENT, NEXT_PUBLIC_SENTRY_ENVIRONMENT, LOG_LEVEL | observability | Optional | Sentry 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)
- Create your products/prices in Stripe and set
STRIPE_SECRET_KEY+NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY. - Sync the plan catalog from
src/config/plans.ts:pnpm stripe:sync. - Add a webhook endpoint at
https://your-domain/api/webhooks/stripe, subscribe it to the subscription/invoice events, and copy its signing secret intoSTRIPE_WEBHOOK_SECRET. A missing or wrong secret means subscription state never updates.
Background jobs (Inngest)
- Create an Inngest app and set
INNGEST_EVENT_KEY+INNGEST_SIGNING_KEY(leaveINNGEST_DEVunset). - Register the serve endpoint
https://your-domain/api/inngestso 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 deployran against the production database. -
BETTER_AUTH_SECRETis a fresh random value;BETTER_AUTH_URL/NEXT_PUBLIC_APP_URLare 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.