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 is the recommended host. It runs Next.js natively, manages the build, and gives you a preview deploy for every pull request. This page is your production checklist.
For the full variable reference, see Environment Variables. For the Turnstile bot check, see Bot Protection.
The build command is prisma generate && next build && prisma migrate deploy (in package.json). Database migrations run during the build, not at runtime. So DATABASE_URL must be set before your first deploy. If the database is missing or unreachable, the build fails.
Deploy checklist
Import the repo. Go to vercel.com/new and import your Git repository. Vercel detects Next.js and fills in the build settings for you. 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. The build applies any pending migrations from prisma/migrations/ against DATABASE_URL, then builds the app.
Set the production URL. Point your domain at Vercel in Settings → Domains (SSL is automatic). Set PUBLIC_URL, 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 to your domain. next.config.ts reads it to build the Server Actions allowedOrigins list. A stale value blocks form and action submissions in production.
Register the Stripe webhook. In the Stripe Dashboard → Webhooks, add an endpoint at https://yourdomain.com/api/auth/stripe/webhook. The Better Auth catch-all route (app/api/auth/[...all]/) handles it. Subscribe to the customer.subscription.* and invoice.* events. Copy the signing secret into STRIPE_WEBHOOK_SECRET, then redeploy.
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 checks every variable when the app starts. A missing or malformed value fails the build. There's no STRIPE_PUBLISHABLE_KEY. Checkout runs server-side through the Better Auth Stripe plugin.
# Core
PUBLIC_URL="https://yourdomain.com"
NEXT_PUBLIC_APP_NAME="Your App"
# Database (Postgres)
DATABASE_URL="postgresql://user:password@host:5432/dbname"
# If your provider pools connections (Neon, Supabase), also set the direct, non-pooled URL.
DIRECT_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. Use a pooled
DATABASE_URLfor the app (the Neon or Supabase pooler, or PgBouncer). KeepDIRECT_DATABASE_URLas the non-pooled string for migrations. One catch:prisma.config.tsreads onlyDATABASE_URL, so migrations run against whatever you set there. If your pooler can't run schema changes (DDL), either pointDATABASE_URLat the direct connection for the build, or editprisma.config.tsto useDIRECT_DATABASE_URL. - Build-time migrations. Every deploy applies pending migrations. A broken migration or an unreachable database fails the build before
next buildeven runs. Test migrations on a staging database first. output: "standalone". This setting innext.config.tsis for Docker. Vercel ignores it and uses its own build, so it does no harm.- Long-running routes. API routes run as serverless functions with a time limit. For heavy work, raise it per route with
export const maxDuration = 60;. - Previews. Scope Preview env vars to
sk_test_...Stripe keys and a separate database. That way 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.