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 usesdrizzle-orm/postgres-js, not a provider-specific driver.
Auth (Better Auth)
BETTER_AUTH_SECRET, generate one withnpx @better-auth/cli secret. Sessions can't be signed without it.BETTER_AUTH_URL, leave it ashttp://localhost:3000in 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_SECRETandGITHUB_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 blankRESEND_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_KEYandSTRIPE_WEBHOOK_SECRET. Needs zero dashboard setup, prices are built inline at checkout (seelib/billing/plans.ts). Register the webhook at{BETTER_AUTH_URL}/api/webhooks/stripe, or runstripe listenin dev, to get the signing secret. - LemonSqueezy,
LEMONSQUEEZY_API_KEY,LEMONSQUEEZY_STORE_ID,LEMONSQUEEZY_WEBHOOK_SECRET, plus aLEMONSQUEEZY_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(sandboxby default), plus aPOLAR_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(anthropicoropenai) only matters when both keys are set, otherwise whichever key exists wins.ANTHROPIC_MODELandOPENAI_MODELpick 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 tohttp://localhost:3000automatically (seelib/seo.ts, another||fallback). Set it to your real domain in production.