A PORT=8O80 typo (that's a letter O), a DATABASE_URL you forgot to set, a NODE_ENV=prodd — none of these fail when your app starts. They fail later: a cryptic stack trace three layers into startup, a service that boots but talks to the wrong database, a feature flag that's silently off in production. The error is never "your env is wrong"; it's whatever broke downstream.
dotenv loads your .env. It doesn't check it. So I built envward: validate the whole environment against a small typed schema up front, and fail loudly — with the actual problem — before a single line of app code runs. Zero dependencies, no network.
$ envward
.env — checked against env.schema.json
✗ API_KEY missing — required string
✗ NODE_ENV "prodd" is not one of: development, production, test
✗ PORT 70000 is above max 65535
3 problem(s), 3 key(s) valid
It exits non-zero on any problem, so it drops straight into a prestart hook or a CI step.
Get a schema in one command
No hand-writing JSON from scratch:
envward --init > env.schema.json
--init reads your existing .env and guesses a type for each key (8080 → int, https://… → url, true → bool, …), marking them required. Then you tighten it:
{
"PORT": { "type": "int", "required": true, "min": 1, "max": 65535 },
"DATABASE_URL": { "type": "url", "required": true },
"NODE_ENV": { "type": "enum", "values": ["development", "production", "test"] },
"API_KEY": { "type": "string", "required": true, "minLength": 16 }
}
Types: string (with minLength/maxLength/pattern), int / number (with min/max), bool, url, email, enum. An empty value (KEY=) counts as missing.
How it's different from a drift checker
A .env drift tool tells you which keys are missing versus .env.example. envward validates the values: is PORT actually an integer in range, is DATABASE_URL actually a URL, is NODE_ENV one of the allowed set. Different failure mode, caught at a different time.
Install
npx envward # Node
pip install envward # Python — same behavior
Two builds (Node + Python) that validate identically, so it fits whatever your stack already runs.
Use it as a gate
# package.json: "prestart": "envward" — refuse to boot with a broken .env
# CI: envward --env .env.ci --strict
--strict also flags keys present in .env but missing from the schema.
A couple of honest notes
- Zero dependencies, both builds — stdlib only.
-
A malformed schema is an error, not a guess. A non-numeric
min, a bad regexpattern, an unknown type — envward exits2with a clear message in both builds, rather than crashing or silently passing. (Getting Node and Python to agree on every edge here took a real adversarial pass.) -
patternis matched in ASCII mode and as an unanchored search — wrap it in^…$; keep to a portable regex subset for identical behavior across both builds.
Links
- npm: https://www.npmjs.com/package/envward
- PyPI: https://pypi.org/project/envward/
- Source: https://github.com/jjdoor/envward
How do you guard environment config today — a hand-rolled startup check, a framework feature, or just hope? And would you gate CI on it?
Top comments (1)
Use varlock.dev - it’s a mature solution that solves this and many other features around config. Open source with an active user base.