Deployment Overview
Deploy the web app to Vercel, Cloudflare Workers, or Docker — comparison, shared setup, and the verification checklist
The web app (apps/web) deploys to three hosts that pnpm run setup offers (scripts/hosts.ts). Vercel and Cloudflare have full guides; Docker has a short build guide:
- Vercel — the recommended path. Zero code changes, native Next.js support.
- Cloudflare Workers — cheaper at scale. Runs through the OpenNext adapter; the Prisma driver adapter and S3-compatible storage config it needs already ship in the repo, so it's accounts and env vars, not code.
- Docker —
docker build -f apps/web/Dockerfile .from the repo root (AWS / VPS).
One-time setup
After cloning, run the interactive setup — it asks for your project name and deploy target (Cloudflare Workers, Vercel, or Docker), writes your dev env files, and removes the other hosts' configuration:
pnpm install
pnpm run setupRe-clone the kit if you later switch hosts — setup is one-way by design.
Choosing a platform
| Vercel | Cloudflare Workers | |
|---|---|---|
| Code changes required | None | None — driver adapter and storage config are pre-wired, gated on env vars |
| Next.js 16 support | Native | Via @opennextjs/cloudflare |
| Database access | Postgres via PrismaPg (Neon pooled URL is fine at runtime; DDL needs unpooled DIRECT_URL) | Neon serverless (WS pool; fetch for one-shots) |
| File uploads | S3 provider (serverless FS is ephemeral) | S3 provider (R2 or S3) |
| Stripe webhooks | Works as-is | Works as-is — @better-auth/stripe already prefers async crypto verification |
| Effort | ~30 minutes | ~30–45 minutes |
Shared prerequisites
Both platforms need these accounts provisioned first:
- Neon (or any hosted Postgres) — copy the pooled connection string as
DATABASE_URL. Forprisma db push, also set unpooledDIRECT_URLinpackages/database/.env(prisma.config.tsusesDIRECT_URL || DATABASE_URL; the pooler cannot run DDL). - Stripe — create the Pro and Enterprise products with monthly/annual prices; note the four
price_...IDs - Resend — verify your sending domain; note the API key
- OAuth apps (optional) — Google and GitHub apps; callback URLs are added post-deploy
Shared post-deploy configuration
After either guide's deploy step:
-
Push the schema — from your machine, with unpooled
DIRECT_URL(or a non-poolerDATABASE_URL) inpackages/database/.env:cd packages/database && npx prisma db push -
Stripe webhook — in the Stripe Dashboard, add an endpoint:
https://yourdomain.com/api/auth/stripe/webhookSubscribe to
checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted. Copy the signing secret intoSTRIPE_WEBHOOK_SECRETand redeploy. -
OAuth callback URLs — add to each provider:
https://yourdomain.com/api/auth/callback/google https://yourdomain.com/api/auth/callback/github -
Production URLs —
NEXT_PUBLIC_APP_URL,BETTER_AUTH_URL, andBETTER_AUTH_TRUSTED_ORIGINSmust all match the final domain.NEXT_PUBLIC_*values are baked in at build time — changing them requires a rebuild, not just a restart. -
First admin — set
ADMIN_EMAILSto your email before anyone signs up, then sign up normally with that address and verify it. You land withrole: "admin"and can open/admin. The var only applies at signup — see Authentication → Configuration. Do not run the dev seed against production; it wipes every table.
Verification checklist
Run this after every deploy:
-
GET /api/healthreturns OK (database + auth checks pass) - Sign up with email/password; verification email arrives via Resend
- Magic link sign-in works
- 2FA enrolment works (QR code renders, TOTP accepted)
- Social sign-in works (if OAuth configured)
- Create an organization; invite a member; the invite email arrives
- Locale routing:
/es/...(or another shipped locale:es,de,zh,fr,pt) renders. Defaultenhas no prefix (rewriteDefault). - Middleware gating: signed-out visit to
/{orgSlug}/billingredirects to sign-in withcallbackUrl - Stripe test-mode checkout completes; the webhook deliveries show
200in the Stripe Dashboard; the subscription appears on the billing page - Organization logo upload at
/{orgSlug}/settingssucceeds and the file is served back - Response headers include
Content-Security-Policywith a nonce