Keel

Docs / Deployment / Vercel deploy

Vercel deploy

Keel deploys to Vercel with no special configuration, it's a stock Next.js app. What takes a minute is getting the env vars, the production database, and each provider's webhook pointed at the right place. These are the steps.

Connect the repo

Push your fork to GitHub, then in the Vercel dashboard pick Add New, Project and import it. Vercel detects Next.js and sets the build command (next build) and output on its own, leave those defaults alone.

Set the env vars

Add your variables under Settings, Environment Variables before the first deploy. The full list and what each one unlocks is in Environment variables, don't copy your .env.local blindly, a few need production values:

  • DATABASE_URL, point at your production Postgres, not the local one. Include ?sslmode=require for hosted providers like Neon or Supabase.
  • BETTER_AUTH_URL, set to your real deployed origin (e.g. https://yourdomain.com), no trailing slash. This is the CSRF origin auth checks against, a wrong value breaks every sign-in.
  • BETTER_AUTH_SECRET, generate a fresh one for production with npx @better-auth/cli secret, don't reuse the dev secret.
  • NEXT_PUBLIC_SITE_URL, your public domain, used for sitemap, OG images, and canonical links.
  • Each OAuth pair (GOOGLE_*, GITHUB_*), each billing provider's keys, and every webhook signing secret need their production values, the test/sandbox keys won't work against live traffic.

Update your OAuth redirect URIs

Google and GitHub OAuth apps only accept redirects to registered URLs. Add {BETTER_AUTH_URL}/api/auth/callback/google (and /github) to each provider's allowed list, or production sign-in fails with a redirect mismatch.

Run migrations against production

Vercel's build doesn't touch your database. Apply the schema yourself, pointing DATABASE_URL at production:

bash
DATABASE_URL="your-production-url" bun run db:migrate

Use db:migrate, not db:push, for production, see memory/conventions.md for why. Re-run it after any deploy that adds a migration.

Point webhooks at the deployed URL

Only your selected BILLING_PROVIDER sells, but each provider's webhook route is always live at its own path. Register the one you're using in that provider's dashboard, pointed at your deployed domain:

  • Stripe, {BETTER_AUTH_URL}/api/webhooks/stripe
  • LemonSqueezy, {BETTER_AUTH_URL}/api/webhooks/lemonsqueezy
  • Polar, {BETTER_AUTH_URL}/api/webhooks/polar

Copy the signing secret each dashboard gives you back into the matching *_WEBHOOK_SECRET env var, then redeploy so it takes effect. Without the correct secret the route rejects every event as unsigned.

Edit this page on GitHub →Last updated Jul 22, 2026