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 ??:
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/aidemo 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.