Next Starter Logo
Deployment

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_URL for the app (the Neon or Supabase pooler, or PgBouncer). Keep DIRECT_DATABASE_URL as the non-pooled string for migrations. One catch: prisma.config.ts reads only DATABASE_URL, so migrations run against whatever you set there. If your pooler can't run schema changes (DDL), either point DATABASE_URL at the direct connection for the build, or edit prisma.config.ts to use DIRECT_DATABASE_URL.
  • Build-time migrations. Every deploy applies pending migrations. A broken migration or an unreachable database fails the build before next build even runs. Test migrations on a staging database first.
  • output: "standalone". This setting in next.config.ts is 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.

On this page