Keel

Docs / Deployment / Env validation

Env validation

Keel has no centralized env schema, no env.ts that parses process.env through Zod and halts the build if something's missing. That's deliberate. Validation here is pragmatic: one reference file, one fallback convention, and each integration failing loudly at the point it's actually used.

.env.example is the source of truth

Every variable the app reads has a line in .env.example, with a comment explaining what it unlocks and where to get it. When you add a new process.env.X read anywhere in the code, add it to .env.example too. That file, not a schema, is the canonical list of what Keel consumes.

Fallbacks use ||, never ??

Anywhere a string env var has a default, Keel uses ||, not ??:

ts
const FROM_EMAIL = process.env.RESEND_FROM_EMAIL || "Keel <onboarding@resend.dev>";

This isn't stylistic. A blank RESEND_FROM_EMAIL= line in .env.local is a defined, empty string, which is non-nullish, so ?? treats it as a real value and never falls back. The empty string then propagates downstream. This already broke email once: an empty from address, which Resend rejected with a confusing "domain is invalid" error. || catches the empty string and uses the default. See the "Env var string fallbacks" note in AGENTS.md.

Empty is not the same as unset

A missing var and a var set to "" behave differently under ??. Since a stray X= line is easy to leave in a .env file, || is the safe default for every string fallback in this codebase.

Each integration fails at its point of use

There's no build-time gate that blocks deploy when a key is missing, because most keys are optional. Instead every feature is env-gated on its own and fails obviously when its key isn't set:

  • Missing DATABASE_URL, the app can't connect and errors on the first query, this is the one var that's genuinely required.
  • Missing AI keys, the /app/ai demo returns a clear "isn't configured yet" error, no credits charged, no fake reply.
  • Missing Stripe/billing keys, checkout and portal calls error at the call site rather than silently no-op.
  • Missing RESEND_API_KEY, transactional email logs to the console instead of sending.
  • Missing Upstash keys, rate limiting silently no-ops (the one intentional exception, it's not required to run Keel).

The tradeoff: you won't get a single startup failure listing everything that's missing. You get a clear, specific error the moment you exercise a feature whose key you forgot, which for a template buyers extend feature by feature is the more useful signal.

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