Why I Built nevr-env — And Why process.env Deserves Better
Source: Dev.to
The Problem with Environment Variables
I got tired of crashing apps, leaked secrets, and copy‑pasting .env files on Slack.
Every developer has that moment:
- Deploy on Friday. CI passes. You go home feeling productive.
- A ping arrives: “App is crashing in production.”
The culprit? DATABASE_URL was never set. The app accessed process.env.DATABASE_URL, got undefined, and silently passed it as a connection string. Postgres didn’t appreciate that.
I’ve hit this exact bug more times than I’d like to admit. Each time the fix was the same: add another line to .env.example, hope teammates read the README, and move on.
Why the Current Tooling Fails
- No validation at startup –
process.env.PORTis typed asstring | undefined. If you forgetPORT, the server silently listens onundefined. - No type safety –
process.env.ENABLE_CACHEis"true"(a string), nottrue(a boolean). Every developer writes their own parsing logic. - Secret sprawl – Teams share secrets via Slack DMs, Google Docs, or worse.
.env.exampleis always outdated. - Boilerplate everywhere – New projects require copying Zod schemas and writing the same
DATABASE_URL: z.string().url(),PORT: z.coerce.number(), etc.
The t3‑env Gap
t3-env was a step forward: type‑safe env validation with Zod. I used it and liked it.
But as my projects grew, the gaps showed:
// Every. Single. Project.
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
OPENAI_API_KEY: z.string().startsWith("sk-"),
RESEND_API_KEY: z.string().startsWith("re_"),
// ... 20 more lines of the same patterns
},
});
- I was writing the same schemas across eight projects. When Stripe changed their key format, I had to update all of them.
- New teammates would clone the repo, run
npm run dev, see a wall of validation errors, and spend 30 minutes figuring out what goes where.
Introducing nevr‑env
nevr-env is an environment lifecycle framework—not just validation, but the entire lifecycle from setup to production monitoring.
Simplified Configuration
import { createEnv } from "nevr-env";
import { postgres } from "nevr-env/plugins/postgres";
import { stripe } from "nevr-env/plugins/stripe";
import { openai } from "nevr-env/plugins/openai";
import { z } from "zod";
export const env = createEnv({
server: {
NODE_ENV: z.enum(["development", "production", "test"]),
API_SECRET: z.string().min(10),
},
plugins: [
postgres(),
stripe(),
openai(),
],
});
Three plugins replace 15+ lines of manual schemas. Each plugin knows the correct format, provides proper validation, and even includes auto‑discovery (e.g., a running Postgres container is detected automatically).
Interactive Onboarding
When a new developer runs the app with missing variables:
$ npx nevr-env fix
They get an interactive wizard instead of a wall of errors:
? DATABASE_URL is missing
This is: PostgreSQL connection URL
Format: postgresql://user:pass@host:port/db
> Paste your value: █
Onboarding time goes from “ask someone on Slack” to “run one command.”
Encrypted Secret Management (Vault)
# Generate a key (once per team)
npx nevr-env vault keygen
# Encrypt your .env into a vault file
npx nevr-env vault push # creates .nevr-env.vault (safe to commit)
# New teammate pulls the repo and decrypts
npx nevr-env vault pull # creates .env locally
- The vault uses AES‑256‑GCM encryption with PBKDF2 (600 K iterations).
- The encryption key never touches the repository.
- No more Slack DMs or paid secret‑management SaaS for small teams.
Secret Scanning
$ npx nevr-env scan
Found 2 secrets in codebase:
CRITICAL src/config.ts:14 AWS Access Key (AKIA...)
HIGH lib/api.ts:8 Stripe Secret Key (sk_live_...)
Runs in CI to catch secrets before they enter git history—no extra tools required.
Plugins
| Category | Plugins |
|---|---|
| Database | postgres(), redis(), supabase() |
| Auth | clerk(), auth0(), better-auth(), nextauth() |
| Payment | stripe() |
| AI | openai() |
resend() | |
| Cloud | aws() |
| Presets | vercel(), railway(), netlify() |
Creating a Custom Plugin
import { createPlugin } from "nevr-env";
import { z } from "zod";
export const myService = createPlugin({
name: "my-service",
schema: {
MY_API_KEY: z.string().min(1),
MY_API_URL: z.string().url(),
},
});
CLI Commands
| Command | Description |
|---|---|
init | Set up nevr-env in your project |
check | Validate all env vars (CI‑friendly) |
fix | Interactive wizard for missing vars |
generate | Auto‑generate .env.example from schema |
types | Generate env.d.ts type definitions |
scan | Find leaked secrets in code |
diff | Compare schemas between versions |
rotate | Track secret rotation status |
ci | Generate CI config (GitHub Actions, Vercel, Railway) |
dev | Validate + run your dev server |
watch | Live‑reload validation on .env changes |
vault | Encrypted secret management (keygen/push/pull/status) |
Installation
pnpm add nevr-env zod
npx nevr-env init
The init wizard detects your framework, finds running services, and generates a complete configuration.
Links
- GitHub: https://github.com/nevr-ts/nevr-env
- npm: https://www.npmjs.com/package/nevr-env
- Docs: https://nevr-ts.github.io/nevr-env/
If you’ve ever lost production time to a missing env var, I’d love to hear your story. And if nevr-env saves you from that, a star on GitHub would mean the world.
Built by Yalelet Dessalegn as part of the nevr‑ts ecosystem.