DEV Community

Cover image for Why I Built nevr-env — And Why process.env Deserves Better
yalelet dessalegn
yalelet dessalegn

Posted on

Why I Built nevr-env — And Why process.env Deserves Better

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:

  1. No validation at startupprocess.env.PORT returns string | undefined. If you forget PORT, your server silently listens on undefined.

  2. No type safetyprocess.env.ENABLE_CACHE is "true" (a string), not true (a boolean). Every developer writes their own parsing.

  3. Secret sprawl — Your team shares secrets via Slack DMs, Google Docs, or worse. .env.example is always outdated.

  4. Boilerplate everywhere — Every new project: copy the Zod schemas, write the same DATABASE_URL: z.string().url(), same PORT: 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
  },
});
Enter fullscreen mode Exit fullscreen mode

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(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: █
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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_...)
Enter fullscreen mode Exit fullscreen mode

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()
Email 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(),
  },
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
maame-codes profile image
Maame Afua A. P. Fordjour

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!

Collapse
 
yalelet_dessalegn_b87ed18 profile image
yalelet dessalegn

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!

Collapse
 
maame-codes profile image
Maame Afua A. P. Fordjour

You are welcome!

Collapse
 
theoephraim profile image
Theo Ephraim

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!

Collapse
 
yalelet_dessalegn_b87ed18 profile image
yalelet dessalegn

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.schema files (@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 share
secrets via git with zero SaaS dependency.

I'd say the key differences are:

  • varlock is broader (multi-language, external secret sourcing) — great for polyglot teams
  • nevr-env goes deeper in TypeScript (full type inference from schema to Proxy, plugin ecosystem, 12 CLI commands for the entire lifecycle)

Different philosophies, both solving real pain. Would be happy to chat more about potential areas where our approaches could learn from each other.

Collapse
 
nedcodes profile image
Ned C

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?

Collapse
 
yalelet_dessalegn_b87ed18 profile image
yalelet dessalegn

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 createRotationChecker API 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 tracks updatedAt and createdBy, so you can see who last changed the vault and when. If a

secret changes, you'd vault push again (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.