Skip to main content
Vercel is the path of least resistance. The repo’s preset matches Vercel’s defaults (Next.js 15, pnpm, Node runtime), so a fresh project import “just works” once env vars are in place. Plan on 5 minutes from git push to live URL — the bulk of the time is pasting secrets and creating the database.

Prerequisites

  • A Postgres database with a pooled connection string. Neon, Supabase, Railway, Crunchy — any of them. Serverless functions don’t keep connections warm, so the pooler URL (port 6543 on Supabase, the -pooler host on Neon) is required.
  • A BETTER_AUTH_SECRET of 32+ random characters: openssl rand -base64 32.
  • Your active payment provider’s keys. Whichever provider you set in siteConfig.payment.provider needs its *_SECRET_KEY, *_WEBHOOK_SECRET and the relevant *_PRICE_* ids populated.
  • Optional but recommended: a RESEND_API_KEY for transactional email. Without one, the mail facade no-ops gracefully and signups skip the verification step.
  • A GitHub repo Vercel can read. SSO, monorepo, fork — all fine.
Run pnpm typecheck && pnpm lint && pnpm build locally before pushing. CI runs the same three; Vercel will refuse a broken build at the same gate.

Step-by-step

1. Push to GitHub

git push origin main
If you keep a private repo, grant Vercel access in the GitHub app settings.

2. Import on Vercel

In the Vercel dashboard: Add New → Project → pick the repo. The Framework Preset auto-resolves to Next.js. Leave the build command blank (Vercel reads pnpm build from package.json). Output directory is .next (default).

3. Set env vars

Settings → Environment Variables. The required baseline is:
DATABASE_URL=postgres://user:pass@host:6543/db?sslmode=require
BETTER_AUTH_SECRET=<32+ random chars>
BETTER_AUTH_URL=https://your-domain.com
NEXT_PUBLIC_APP_URL=https://your-domain.com
[email protected]
Add your active payment provider’s keys (Stripe shown):
STRIPE_SECRET_KEY=sk_live_…
STRIPE_WEBHOOK_SECRET=whsec_…
STRIPE_PRICE_VIBESTRAP_PROMO=price_…
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_…
Optional but common: RESEND_API_KEY, GOOGLE_CLIENT_ID/SECRET, TURNSTILE_SECRET_KEY + NEXT_PUBLIC_TURNSTILE_SITE_KEY. See Env reference for the full list.

4. Migrate the production database

Before the first deploy, sync your schema. You have two options:
# Option A — fastest, dev-only style. Diffs schema against DB and applies.
DATABASE_URL=<prod-url> pnpm db:push
# Option B — committed migrations. Use this once you have real data.
pnpm db:generate            # writes a SQL file under drizzle/
git add drizzle/ && git commit
DATABASE_URL=<prod-url> pnpm db:migrate
Once you have paying users, never run db:push against production again — it can drop columns silently. Switch to Option B and run db:migrate from a deploy hook or a one-shot script, not from your running app.

5. Configure payment webhooks

In your payment provider’s dashboard, add a webhook endpoint pointing at:
https://your-domain.com/api/webhooks/stripe
https://your-domain.com/api/webhooks/paddle
https://your-domain.com/api/webhooks/lemonsqueezy
https://your-domain.com/api/webhooks/creem
Pick the route matching your active provider. Subscribe to: checkout.session.completed, invoice.paid, customer.subscription.updated, customer.subscription.deleted (Stripe naming — adapt for others). Copy the signing secret into STRIPE_WEBHOOK_SECRET (or equivalent).

6. Deploy

Push or click Deploy. The first build takes 2-3 minutes. Vercel’s logs surface env-var errors loudly — src/env.ts throws at startup if anything required is missing.

Verify it works

Hit each URL once. Anything red means something’s misconfigured.
  • https://your-domain.com/ — home renders, hero copy is your locale
  • https://your-domain.com/zh/ — Chinese hero
  • https://your-domain.com/api/ping — returns { ok: true }
  • https://your-domain.com/sitemap.xml — every public route listed
  • https://your-domain.com/robots.txt — disallows /admin, /api/, /settings
  • https://your-domain.com/register — signup works, you receive the verification email
Then run a real $1 test charge through your live keys (refund yourself afterwards): the payment and credit_transaction rows should both appear within ~5 seconds of completing checkout. If they don’t, check the webhook logs in your payment dashboard for 4xx responses.

Common pitfalls

  • BETTER_AUTH_URL mismatch. It must be the full prod URL with scheme, no trailing slash. Auth callbacks silently fail when this drifts from the actual domain.
  • Missing env vars in prod. Vercel separates Production / Preview / Development scopes. Set vars to “All Environments” unless you know you need per-env values.
  • Webhook signing-secret mismatch. A stale STRIPE_WEBHOOK_SECRET returns 400 on every event. Rotate the secret in the dashboard, paste it into Vercel, redeploy.
  • Postgres connection limits. Serverless functions burn connections fast. Use the pooler URL (port 6543 on Supabase, -pooler.region host on Neon). Hit “max connections” errors? You’re using the direct URL.
  • Forgotten NEXT_PUBLIC_APP_URL. Used by sitemap, OG images, OAuth redirects, payment success URLs. Must match BETTER_AUTH_URL in production.
  • Skipping db:push before first deploy. App boots, then 500s on the first query because the tables don’t exist.

Rollback

Vercel keeps every deployment forever. Promote any previous build from the Deployments list — instant revert. If a migration is the culprit, restore from your DB provider’s point-in-time snapshot (Neon and Supabase both ship this on every plan).

Official docs