Keel

Docs / Getting Started / Environment variables

Environment variables

Everything Keel talks to is configured through .env.local, copied from .env.example. Every variable is feature-gated: a missing key disables one feature and fails clearly at the point you use it, it never breaks the build. Here's what each group unlocks and what happens when you leave it blank.

Database

  • DATABASE_URL, your Postgres connection string. This is the one variable you actually need. Without it the migrations, seed, and every page that touches the database fail. Works with Neon, Supabase, Railway, or local Postgres, Keel uses drizzle-orm/postgres-js, not a provider-specific driver.

Auth (Better Auth)

  • BETTER_AUTH_SECRET, generate one with npx @better-auth/cli secret. Sessions can't be signed without it.
  • BETTER_AUTH_URL, leave it as http://localhost:3000 in dev. It's the origin for the CSRF check, so running dev on a different port breaks every auth call.

Tip, start with the database only

As the Setup page notes, auth and AI features stay disabled until their keys are set. You can see the whole app render with just DATABASE_URL and the two auth vars filled in.

OAuth providers

  • GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET and GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET. Both vars in a pair are required to enable that provider, either pair can stay blank to leave it disabled. The sign-in button for an unconfigured provider simply doesn't appear. Register the redirect URI {BETTER_AUTH_URL}/api/auth/callback/google (or /github) with the provider.

Email (Resend)

  • RESEND_API_KEY, without it every transactional email (verify, reset, magic link, team invite, welcome) logs to the console instead of sending. Get a key at resend.com, it works immediately with no domain setup.
  • RESEND_FROM_EMAIL, the from address. This is one of the vars that must fall back with ||, not ??: a blank RESEND_FROM_EMAIL= line is a defined empty string, and ?? would pass it straight through to Resend, which rejects an empty from address with a confusing "domain is invalid" error. Leave it unset to use Resend's shared test domain (delivers only to your own address), set it once you've verified your own domain.
  • CONTACT_INBOX_EMAIL, where the marketing contact form (app/api/contact/route.ts) delivers. Same convention: without it, submissions log instead of sending.

Billing (Stripe / Polar / LemonSqueezy)

Keel ships three swappable providers behind one typed interface in lib/billing/. Pick exactly one with BILLING_PROVIDER (stripe, lemonsqueezy, or polar, defaults to stripe). Only the selected provider's checkout and portal calls run, though every provider's webhook route stays live at its own URL.

  • Stripe, STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET. Needs zero dashboard setup, prices are built inline at checkout (see lib/billing/plans.ts). Register the webhook at {BETTER_AUTH_URL}/api/webhooks/stripe, or run stripe listen in dev, to get the signing secret.
  • LemonSqueezy, LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID, LEMONSQUEEZY_WEBHOOK_SECRET, plus a LEMONSQUEEZY_VARIANT_* id for every plan, interval, and credit pack. It has no inline pricing, so each variant must exist in your store first.
  • Polar, POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_MODE (sandbox by default), plus a POLAR_PRODUCT_* id per plan, interval, and credit pack, created in the Polar dashboard first.

Leave billing unconfigured and checkout fails with a clear error, the rest of the app is unaffected.

AI providers

  • ANTHROPIC_API_KEY / OPENAI_API_KEY, set either one to enable real replies in the AI chat demo (/app/ai). Without a key the chat shows a clear "isn't configured yet" error and charges no credits, rather than faking a reply.
  • AI_PROVIDER (anthropic or openai) only matters when both keys are set, otherwise whichever key exists wins. ANTHROPIC_MODEL and OPENAI_MODEL pick the model per provider.

Rate limiting (Upstash)

  • UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN, optional. They protect paid-per-request routes like the AI chat from being burst-hit. Without them rate limiting silently no-ops. Use the REST database from upstash.com, not the ioredis one.

File uploads (uploadthing)

  • UPLOADTHING_TOKEN, gates avatar and team-logo uploads. Without it the upload buttons still render but each click fails with a clear error. This is the v7 single-token format, copied from your uploadthing app's dashboard.

Site URL

  • NEXT_PUBLIC_SITE_URL, used for the sitemap, OG images, and canonical links. In dev it falls back to http://localhost:3000 automatically (see lib/seo.ts, another || fallback). Set it to your real domain in production.
Edit this page on GitHub →Last updated Jul 22, 2026