saasprokit
Deployment

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.
  • Dockerdocker 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 setup

Re-clone the kit if you later switch hosts — setup is one-way by design.

Choosing a platform

VercelCloudflare Workers
Code changes requiredNoneNone — driver adapter and storage config are pre-wired, gated on env vars
Next.js 16 supportNativeVia @opennextjs/cloudflare
Database accessPostgres via PrismaPg (Neon pooled URL is fine at runtime; DDL needs unpooled DIRECT_URL)Neon serverless (WS pool; fetch for one-shots)
File uploadsS3 provider (serverless FS is ephemeral)S3 provider (R2 or S3)
Stripe webhooksWorks as-isWorks 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:

  1. Neon (or any hosted Postgres) — copy the pooled connection string as DATABASE_URL. For prisma db push, also set unpooled DIRECT_URL in packages/database/.env (prisma.config.ts uses DIRECT_URL || DATABASE_URL; the pooler cannot run DDL).
  2. Stripe — create the Pro and Enterprise products with monthly/annual prices; note the four price_... IDs
  3. Resend — verify your sending domain; note the API key
  4. OAuth apps (optional) — Google and GitHub apps; callback URLs are added post-deploy

Shared post-deploy configuration

After either guide's deploy step:

  1. Push the schema — from your machine, with unpooled DIRECT_URL (or a non-pooler DATABASE_URL) in packages/database/.env:

    cd packages/database && npx prisma db push
  2. Stripe webhook — in the Stripe Dashboard, add an endpoint:

    https://yourdomain.com/api/auth/stripe/webhook

    Subscribe to checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted. Copy the signing secret into STRIPE_WEBHOOK_SECRET and redeploy.

  3. OAuth callback URLs — add to each provider:

    https://yourdomain.com/api/auth/callback/google
    https://yourdomain.com/api/auth/callback/github
  4. Production URLsNEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, and BETTER_AUTH_TRUSTED_ORIGINS must all match the final domain. NEXT_PUBLIC_* values are baked in at build time — changing them requires a rebuild, not just a restart.

  5. First admin — set ADMIN_EMAILS to your email before anyone signs up, then sign up normally with that address and verify it. You land with role: "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/health returns 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. Default en has no prefix (rewriteDefault).
  • Middleware gating: signed-out visit to /{orgSlug}/billing redirects to sign-in with callbackUrl
  • Stripe test-mode checkout completes; the webhook deliveries show 200 in the Stripe Dashboard; the subscription appears on the billing page
  • Organization logo upload at /{orgSlug}/settings succeeds and the file is served back
  • Response headers include Content-Security-Policy with a nonce

On this page