I got tired of crashing apps, leaked secrets, and copy-pasting .env files on Slack. So I built an environment lifecycle framework.
Every developer has that moment.
You deploy on Friday. CI passes. You go home feeling productive.
Then the ping comes: "App is crashing in production."
The culprit? DATABASE_URL was never set. Your 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 want to admit. And every time, the fix was the same: add another line to .env.example, hope your teammates read the README, and move on.
I got tired of hoping. So I built nevr-env.
What's Actually Wrong With .env Files?
Nothing — as a concept. Environment variables are the right way to configure apps. The problem is the tooling around them:
No validation at startup —
process.env.PORTreturnsstring | undefined. If you forgetPORT, your server silently listens onundefined.No type safety —
process.env.ENABLE_CACHEis"true"(a string), nottrue(a boolean). Every developer writes their own parsing.Secret sprawl — Your team shares secrets via Slack DMs, Google Docs, or worse.
.env.exampleis always outdated.Boilerplate everywhere — Every new project: copy the Zod schemas, write the same
DATABASE_URL: z.string().url(), samePORT: z.coerce.number().
The t3-env Gap
t3-env was a step forward. Type-safe env validation with Zod. I used it. I 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 8 projects. When Stripe changed their key format, I had to update all of them.
And when a new teammate joined? They'd clone the repo, run npm run dev, see a wall of validation errors, and spend 30 minutes figuring out what goes where.
So I Built nevr-env
nevr-env is an environment lifecycle framework. Not just validation — the entire lifecycle from setup to production monitoring.
Here's what the same code looks like:
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(),
],
});
3 plugins replace 15+ lines of manual schemas. Each plugin knows the correct format, provides proper validation, and even includes auto-discovery — if you have a Postgres container running on Docker, the plugin detects it.
The Three Features That Changed Everything
1. Interactive Fix Wizard
When a new developer runs your app with missing variables:
$ npx nevr-env fix
Instead of a wall of errors, they get an interactive wizard:
? DATABASE_URL is missing
This is: PostgreSQL connection URL
Format: postgresql://user:pass@host:port/db
> Paste your value: █
Onboarding time went from "ask someone on Slack" to "run one command."
2. Encrypted Vault
This is the feature I'm most proud of.
# 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 to git!)
# New teammate clones repo and pulls
npx nevr-env vault pull
# Decrypts vault → creates .env
The vault file uses AES-256-GCM encryption with PBKDF2 600K iteration key derivation. It's safe to commit to git. The encryption key never touches your repo.
No more Slack DMs. No more "hey can you send me the .env?" No more paid secret management SaaS for small teams.
3. 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_...)
This runs in CI and catches secrets before they hit your git history. Built-in, no extra tools needed.
13 Plugins and Counting
Every plugin encapsulates the knowledge of how a service works:
| Category | Plugins |
|---|---|
| Database |
postgres(), redis(), supabase()
|
| Auth |
clerk(), auth0(), better-auth(), nextauth()
|
| Payment | stripe() |
| AI | openai() |
resend() |
|
| Cloud | aws() |
| Presets |
vercel(), railway(), netlify()
|
And you can create your own:
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(),
},
});
The Full CLI
nevr-env ships with 12 CLI commands:
| Command | What it does |
|---|---|
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) |
Try It
pnpm add nevr-env zod
npx nevr-env init
The init wizard detects your framework, finds running services, and generates a complete configuration.
GitHub: github.com/nevr-ts/nevr-env
npm: 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.
Top comments (7)
This is a great breakdown of why we need better tools for environment variables. I like that you focused on making the developer experience better and more "type-safe." Thanks for sharing your work with the community!
Thank you! Developer experience was the #1 priority if the tool isn't easier than the problem, nobody uses it. The fix wizard and plugin system came directly from watching teammates struggle with onboarding. Appreciate the kind words!
You are welcome!
You might like varlock.dev - does some things differently, but generally trying to solve many of the same problems, and is a a fairly mature solution. A few things we could likely integrate into our toolkit as well. Would love to hear what you think!
Interesting just checked out varlock! Cool project, congrats on the traction.
There's definitely overlap in the problem space (type-safe env validation, security), but the approaches are quite different:
varlock uses a schema-as-comments approach — decorators in
.env.schemafiles (@type,@sensitive,@required). It's language-agnostic (works with Python,Ruby, Go, etc.) and integrates with external secret managers like 1Password via
exec()functions. The log redaction and leak prevention is a nice touch.nevr-env takes a code-first approach schemas are TypeScript with Standard Schema (Zod/Valibot/ArkType), and the plugin system encapsulates service knowledge
(
postgres(),stripe(),openai()etc. 13 built-in). Instead of external secret managers, we ship a built-in encrypted vault (AES-256-GCM) so teams can sharesecrets via git with zero SaaS dependency.
I'd say the key differences are:
Different philosophies, both solving real pain. Would be happy to chat more about potential areas where our approaches could learn from each other.
The Friday deploy story is painfully relatable. I've lost count of how many times process.env.SOMETHING returned undefined and the app just silently did the wrong thing instead of crashing loudly.
The gap between t3-env and a full lifecycle tool makes sense. Validation at startup is table stakes, but the secret sharing problem is where most teams actually bleed time. Curious how you handle rotation - does nevr-env have any hooks for when a secret changes in the vault?
Thanks! Yeah, that silent undefined behavior is exactly what pushed me to build this.
For rotation yes, nevr-env has built-in rotation tracking. The CLI command:
npx nevr-env rotate
It checks all sensitive variables (detected from your schema anything that looks like a secret key, token, or password) and reports how long since each was last
rotated.
You can record a rotation:
npx nevr-env rotate --record STRIPE_SECRET_KEY --max-age 90
And programmatically, there's a
createRotationCheckerAPI you can hook into your app:import { createRotationChecker } from "nevr-env";
const checker = createRotationChecker({
trackedKeys: ["STRIPE_SECRET_KEY", "DATABASE_URL"],
defaultMaxAgeDays: 90,
onStaleSecret: (record, ageDays) => {
// Send Slack alert, log to monitoring, etc.
},
});
As for vault-specific hooks when you
vault push, the metadata tracksupdatedAtandcreatedBy, so you can see who last changed the vault and when. If asecret changes, you'd
vault pushagain (re-encrypts everything), and the rotation tracker independently tracks age per-key.The two systems complement each other: vault handles secure sharing, rotation handles lifecycle monitoring.