Deploy to Vercel
Deploy Next Starter to Vercel: import the repo, set environment variables, run build-time migrations, and wire up the domain, Stripe webhook, and Turnstile.
Vercel runs Next.js with no adapter or config file and gives you a preview deploy for every pull request. This page is the production checklist.
For the full variable reference see Environment Variables, and for the bot check see Bot Protection.
The build command is prisma generate && next build && prisma migrate deploy (in package.json). Migrations run at the end of the build, never at runtime, so DATABASE_URL has to be set before your first deploy. An unreachable database fails the build.
Deploy checklist
Import the repo. Go to vercel.com/new and import your Git repository. Vercel detects Next.js and picks up the build script from package.json. Keep the defaults.
Add environment variables. In Settings → Environment Variables, paste the block below and scope it to Production. Do this before you deploy, so the build-time migration can reach the database. Repeat for Preview with test Stripe keys and a separate database.
Deploy. Once next build succeeds, prisma migrate deploy applies any pending migrations in prisma/migrations/ against DATABASE_URL.
Set the production URL. Point your domain at Vercel in Settings → Domains (SSL is automatic). Set BETTER_AUTH_URL and NEXT_PUBLIC_BETTER_AUTH_URL to your live https:// origin, then redeploy.
Also update APP_CONFIG.production.baseUrl in lib/config.ts. next.config.ts derives the Server Actions allowedOrigins list from it and adds both the bare host and its www. variant. Next.js only consults that list when a request's Origin header doesn't match its Host or X-Forwarded-Host, so plain same-origin submissions never touch it. A stale value therefore looks harmless right up until you put a proxy in front of the app or serve both the apex and www hostnames. Then actions start failing CSRF checks.
Register the Stripe webhook. In the Stripe Dashboard → Webhooks, add an endpoint at https://yourdomain.com/api/auth/stripe/webhook and subscribe it to six events: checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, and invoice.payment_failed. Copy the signing secret into STRIPE_WEBHOOK_SECRET, then redeploy. Billing covers what each one does.
Allow your domain in Turnstile. In the Cloudflare dashboard, add your production hostname and any preview domains to the widget's Hostnames list. Without this the bot check fails. The keys are NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY.
Environment variables
lib/validations/env.ts validates on import, and next build pulls that module in while prerendering, so a missing or malformed required value fails the deploy rather than surfacing later under traffic. Only NEXT_PUBLIC_BETTER_AUTH_URL, NODE_ENV, and NEXT_PUBLIC_APP_NAME pass when absent.
# Core
NEXT_PUBLIC_APP_NAME="Your App"
# Database (Postgres)
DATABASE_URL="postgresql://user:password@host:5432/dbname"
# Better Auth
BETTER_AUTH_SECRET="<32+ chars, generate with: pnpm dlx auth secret>"
BETTER_AUTH_URL="https://yourdomain.com"
NEXT_PUBLIC_BETTER_AUTH_URL="https://yourdomain.com"
# Google OAuth
GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."
# Stripe (live keys in Production)
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
STRIPE_PRICE_PLUS_MONTHLY="price_..."
STRIPE_PRICE_PLUS_ANNUAL="price_..."
STRIPE_PRICE_PRO_MONTHLY="price_..."
STRIPE_PRICE_PRO_ANNUAL="price_..."
# Email (SMTP2Go)
SMTP2GO_API_KEY="..."
SENDER_EMAIL="noreply@yourdomain.com"
# Storage (Cloudflare R2)
STORAGE_S3_KEY="..."
STORAGE_S3_SECRET="..."
STORAGE_S3_REGION="auto"
STORAGE_S3_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"
STORAGE_S3_BUCKET="your-bucket"
NEXT_PUBLIC_STORAGE_S3_CDN_URL="https://your-r2-cdn-domain.com"
# Bot protection (Cloudflare Turnstile)
NEXT_PUBLIC_TURNSTILE_SITE_KEY="..."
TURNSTILE_SECRET_KEY="..."What to watch for
- Connection pooling. Serverless functions open many short-lived connections, which can exhaust a database, so give the app a pooled
DATABASE_URL. One catch specific to this template:prisma.config.tsreads onlyDATABASE_URL, so the build-time migration runs through the pooler too. If yours can't run schema changes, see Database Setup for the way around it. - Build-time migrations. Because
prisma migrate deployis the last step, a broken migration or an unreachable database fails the deploy afternext buildhas already succeeded. A green compile tells you nothing. Test migrations against a staging database first. output: "standalone". This setting innext.config.tsis for Docker. Vercel deploys its own functions and never runs the standalone server, so the build does a little extra work and nothing breaks.- Long-running routes. Route handlers run as functions with a time limit. Set it per route with
export const maxDuration = 60;(seconds). Check what your Vercel plan and compute settings already default to before assuming that number is an increase. - Previews. Scope Preview env vars to
sk_test_...Stripe keys and a separate database, so preview builds never touch live data.
Database Setup
How Next Starter connects PostgreSQL to Prisma 7: local Docker, migrations, managed providers, and connection pooling for serverless deployments.
Docker Deployment
Self-host Next Starter with Docker: a multi-stage Dockerfile, Next.js standalone output, and the build args versus runtime env split explained.