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

Published: (February 12, 2026 at 12:19 AM EST)
5 min read
Source: Dev.to

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:

  1. Deploy on Friday. CI passes. You go home feeling productive.
  2. 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 startupprocess.env.PORT is typed as string | undefined. If you forget PORT, the server silently listens on undefined.
  • No type safetyprocess.env.ENABLE_CACHE is "true" (a string), not true (a boolean). Every developer writes their own parsing logic.
  • Secret sprawl – Teams share secrets via Slack DMs, Google Docs, or worse. .env.example is 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

CategoryPlugins
Databasepostgres(), redis(), supabase()
Authclerk(), auth0(), better-auth(), nextauth()
Paymentstripe()
AIopenai()
Emailresend()
Cloudaws()
Presetsvercel(), 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

CommandDescription
initSet up nevr-env in your project
checkValidate all env vars (CI‑friendly)
fixInteractive wizard for missing vars
generateAuto‑generate .env.example from schema
typesGenerate env.d.ts type definitions
scanFind leaked secrets in code
diffCompare schemas between versions
rotateTrack secret rotation status
ciGenerate CI config (GitHub Actions, Vercel, Railway)
devValidate + run your dev server
watchLive‑reload validation on .env changes
vaultEncrypted 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.

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.

0 views
Back to Blog

Related posts

Read more »