npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@romaintaillandier1978/dotenv-never-lies

v1.5.0

Published

Typed, validated, and explicit environment variables — powered by Zod.

Downloads

786

Readme

dotenv-never-lies

[!CAUTION] Experimental — not yet ready to use.

Because environment variables lie all the time.

dotenv-never-lies validates, types, and documents your environment variables from a TypeScript / Zod schema.
It fails fast, loud, and before production.

Why?

ecause all of this happens all the time:

  • ❌ a missing env variable → runtime crash
  • ❌ a malformed URL → subtle production bug
  • ❌ CI was not updated after a new variable → confusing red deployment
  • ❌ an optimistic process.env.FOO! → lying to yourself
  • ❌ dotenv silently overrides duplicate keys

And because .env files are:

  • untyped
  • undocumented
  • shared manually
  • rarely up to date

👉 dotenv-never-lies turns this fragile configuration into an explicit contract.


What the library does

  • ✅ validates environment variables at startup
    powered by Zod, enabling complex transformations (arrays, parsing, coercion…)
  • ✅ prevents duplicate environment variables
  • ✅ infers a first real-world DNL schema from an existing .env
  • ✅ provides reliable TypeScript typings
  • ✅ documents each variable
  • ✅ exposes a CLI for CI and humans (dnl export)

What dotenv-never-lies is not

This package has a deliberately limited scope.

  • It is not a frontend tool
    It is not meant to be used in a browser.
    No bundler, no import.meta.env, no variables exposed to the client.

  • It is not a secrets manager
    It does not encrypt anything, does not store anything, and does not replace Vault, AWS Secrets Manager,
    nor your CI/CD provider’s secure variables.

  • It is not a cross-runtime solution
    Targeted support: Node.js.
    Deno, Bun, Cloudflare Workers, edge runtimes: out of scope (for now).

  • It is not a global configuration framework
    It does not manage YAML/JSON files, dynamic profiles,
    nor magical per-environment overrides.

  • It is not permissive
    If a variable is missing or a value is invalid, it crashes.
    That’s the point.

In short:
dotenv-never-lies is for Node.js APIs and backend services
that prefer to fail cleanly at startup rather than silently break in production.


Installation

npm install @romaintaillandier1978/dotenv-never-lies
# or
yarn add @romaintaillandier1978/dotenv-never-lies

Dependencies and compatibility

zod — dotenv-never-lies exposes Zod schemas in its public API.

⚠️ Important: Zod v4.2.1 minimum is required.
Using Zod v3 will cause typing or inference errors.

dotenv allows dotenv-never-lies to automatically handle parsing of env files.

dotenv-expand allows dotenv-never-lies to automatically handle environment variable expansion. This lets you define variables composed from other variables without duplication or fragile copy-paste.

Example

FRONT_A=https://a.site.com
FRONT_B=https://b.site.com
FRONT_C=https://c.site.com

NODE_CORS_ORIGIN="${FRONT_A};${FRONT_B};${FRONT_C}"

DNL schema

The DNL schema is your new source of truth.

(dnl infer will help you scaffold the first skeleton)

schema location

Recommended: env.dnl.ts

Supported in this order for all CLI commands:

  1. --schema path/to/my-dnl.ts
  2. declared in package.json:
{
    ...
    "dotenv-never-lies": {
        "schema": "path/to/my-dnl.ts"
    }
    ...
}
  1. one of env.dnl.ts, env.dnl.js, dnl.config.ts, dnl.config.js

define a schema

import { z } from "zod";
import { define } from "@romaintaillandier1978/dotenv-never-lies";

export default define({
    NODE_ENV: {
        description: "Runtime environment",
        schema: z.enum(["test", "development", "staging", "production"]),
    },

    NODE_PORT: {
        description: "API port",
        schema: portSchema("NODE_PORT").default(3000),
    },

    FRONT_URL: {
        description: "My website",
        schema: z.url(),
    },

    JWT_SECRET: {
        description: "JWT Secret",
        schema: z.string(),
        secret: true,
    },
});

Secrets handling

Reminder: dotenv-never-lies is not a secrets manager.

declaration in the DNL schema

A variable is considered secret if and only if it is explicitly marked in the schema with secret: true.
(secret: undefined is equivalent to secret: false)
This rule is intentionally strict.

JWT_SECRET: {
    description: "JWT signing key",
    schema: z.string(),
    secret: true,
}

More about secrets, and usage in cli commands : → Read secret documentation

Variable lifecycle

dotenv-never-lies works with three distinct representations of environment variables:

  1. Raw value
    The original value coming from the source (.env file or process.env).
    Always a string (or undefined).

  2. Runtime value (validated)
    The value after validation — and possible transformation — by the Zod schema.
    This value may be strongly typed (number, boolean, array, object, etc.).

  3. Exported value
    The value written by dnl export.

    • For env-like formats (env, docker-*, github-*, k8s-*), this is the raw value, after validation.
    • For js, ts and json, the runtime value can be exported using --serialize-typed.

This separation ensures that validation, runtime usage and configuration export remain explicit and predictable.

Runtime usage

import envDef from "./env.dnl";

export const ENV = envDef.load();

// if (process.env.NODE_ENV && process.env.NODE_ENV === "test") {
if (ENV.NODE_ENV === "test") {
    doAdditionalTest();
}

const server = http.createServer(app);
//server.listen(process.env.NODE_PORT||3000, () => {
server.listen(ENV.NODE_PORT, () => {
    console.log(`Server started on ${ENV.NODE_PORT}`);
});

Result:

  • ENV.NODE_ENV is an enum
  • ENV.NODE_PORT is a number
  • FRONT_URL is a valid URL
  • ENV.JWT_SECRET is a string

If a variable is missing or invalid → the process exits immediately.
This is intentional.

Avoid process.env in application code

Once the schema is loaded, environment variables
must be accessed exclusively via the ENV object.

This guarantees:

  • strict typing
  • validated values
  • a single entry point for configuration

To identify residual process.env usages in your codebase, a simple search tool is enough:

grep -R "process\.env" src

Choosing to refactor (or not) those usages depends on context and is intentionally left to the developer.

CLI

The CLI lets you validate, load, generate, export, and document environment variables from a dotenv-never-lies schema.

It is designed to be used:

  • locally (by humans)
  • in CI (without surprises)
  • before the application starts (not after)

Exit codes

dotenv-never-lies uses explicit exit codes, designed for CI:

| Code | Meaning | | ---: | ----------------------------- | | 0 | Success | | 1 | Usage error or internal error | | 2 | DNL schema not found | | 3 | Environment validation failed | | 4 | Error during export |

assert: Validate a .env file (CI-friendly)

Validates variables without injecting them into process.env.

dnl assert --source .env --schema env.dnl.ts

Without --source, dnl assert validates process.env.
This is the recommended mode when variables are injected by the runtime or CI.

→ fails if:

  • a variable is missing
  • a value is invalid
  • the schema is not respected

init: Generate a .env file from the schema

Initialize a documented .env from the schema. Do not read any environment variables

dnl init --schema env.dnl.ts --out .env

Useful for:

  • bootstrapping a project
  • sharing a template
  • avoiding obsolete .env.example files

infer: discover a .envfile, and generate a schema from an existing .env

Generate a DNL schema from an existing .env file.

dnl infer --source .env

Useful for migrating an existing project

Read infer documentation

explain: Display variables documentation

Displays the list of known variables and their description.

dnl explain

Sample output:

NODE_ENV: Runtime environment
NODE_PORT: API port
FRONT_URL: My website
JWT_SECRET: JWT Secret

export: Export variables to other formats

The export command transforms variables validated by the schema
into formats directly consumable by other tools (Docker, CI, Kubernetes, scripts…).

The schema remains the source of truth.
Values are validated before export.

dnl export <format>

By default, values are read from process.env.
A .env file can be provided via --source.

Examples:
Export environment variables as JSON from a .env file

dnl export json --source .env

Clean a .env file (remove comments and extraneous lines)

dnl export env --source .env --out .env.clean
dnl export env --source .env --out .env --force

Export variables as docker-args

dnl export docker-args --source .env

Result:

-e "NODE_ENV=production" -e "NODE_PORT=3000"

Export for GitHub Actions (variables)

dnl export github-env

Result:

printf '%s\n' "NODE_ENV=production" >> $GITHUB_ENV
printf '%s\n' "NODE_PORT=3000" >> $GITHUB_ENV

There are a few more formats and options (see CLI docs dnl export --help).

export types : Exporting TypeScript types

dnl export types generates a .d.ts file describing the static contract of your environment variables.

This file is intentionally conservative.

Transformed variables

If a variable uses a Zod transform, the exported type always reflects the input type, not the runtime output.

In that case:

  • a warning is emitted in the CLI
  • the generated type is annotated with @dnl-transform
  • the runtime value returned by assert() may differ

This is a deliberate design choice to avoid lying to TypeScript.

Real-life usage

GitIgnore

dotenv-never-lies creates temporary files in your project directory.
Add .dnl/ to your .gitignore.

Git

Recommended Git hooks

Using dotenv-never-lies via Git hooks is strongly recommended:

  • pre-commit: prevents committing if the local configuration is not compliant with the schema
  • post-merge: immediately detects schema changes impacting the local environment

The goal is simple:
if the local configuration is not compliant with the schema, code must not be committed.

The schema is the source of truth, not .env files.

These hooks help avoid classic mistakes:

  • missing variable after a pull
  • invalid format detected too late
  • “works on my machine” due to an outdated .env

Hooks installation

git config core.hooksPath .githooks
mkdir -p .githooks

cat > .githooks/pre-commit <<'EOF'
#!/bin/sh
yarn dnl assert --source .env
EOF

cat > .githooks/post-merge <<'EOF'
#!/bin/sh
yarn dnl assert --source .env || true
EOF

chmod +x .githooks/pre-commit .githooks/post-merge

GitLab CI

Environment variables validation step.

# .gitlab-ci.yml
check-env:
    stage: test
    image: node:20-alpine
    script:
        - corepack enable
        - yarn install --frozen-lockfile
        - yarn dnl assert --source $DOT_ENV_FILE

GitHub Actions

# .github/workflows/check-env.yml
name: Check environment

on: [push, pull_request]

jobs:
    check-env:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4

            - uses: actions/setup-node@v4
              with:
                  node-version: 20

            - run: corepack enable
            - run: yarn install --frozen-lockfile

            # Example with a .env file
            - run: yarn dnl assert --source .env

The .env file can be generated from a GitHub secret or mounted dynamically.

- run: echo "$ENV_FILE_CONTENT" > .env
  env:
      ENV_FILE_CONTENT: ${{ secrets.ENV_FILE }}

Which commands should I use?

| Situation | Command to use | | --------------------------------------: | ------------------------------ | | New project | init | | Existing project with a .env | infer | | Validate configuration in CI | assert | | Validate config injected by the runtime | assert | | Document variables | explain | | Generate a clean .env | export env | | Prepare a Docker build | export docker-* | | Inject variables in CI | export github-env / gitlab-env | | Kubernetes (ConfigMap / Secret) | export k8s-* |

Simple rule:

The schema is always the source of truth.
Commands only validate, document, or transform.

Tips

Alternative: minimal-intrusion integration

For a minimally invasive integration, you can add an optional Yarn script to validate your .env file before starting the application:

{
    "scripts": {
        "env:check": "dnl assert --source .env",
        "start:withdnl": "yarn env:check && yarn start"
    }
}

This validates the environment before startup, without changing runtime behavior or application code.

This is optional. It does not replace runtime validation.

Intellisens and JSdoc for env vars

After using dnl export types and importing the generated file, you get strong typing, auto-completion, IntelliSense and JSDoc for each environment variable.

IntelliSense and JSDoc for environment variables

IntelliSense and JSDoc generated from the DNL schema after dnl export types.

Common issues / Troubleshooting

TypeScript projects with rootDir: "src"

If your project uses a strict TypeScript setup with rootDir: "src", and your env.dnl.ts file is located at the project root, TypeScript may report an error like:

File 'env.dnl.ts' is not under 'rootDir' (TS6059)

Option A (preferred) :

Keeps the schema outside application code, allowing reuse by the CLI, CI, and tooling.

To fix this, include env.dnl.ts explicitly in your tsconfig.json and widen rootDir:

{
    "compilerOptions": {
        "rootDir": "."
    },
    "include": ["src/**/*.ts", "env.dnl.ts"]
}

This ensures env.dnl.ts is included in the TypeScript compilation graph.

Option B :

Move the schema into ./src (for example ./src/env/env.dnl.ts).

This avoids TypeScript configuration changes, but makes the schema part of the application codebase

You will need to specify schema location, see related section

FAQ / Design choices

Why so strict?

Because configuration errors are bugs, not warnings.

If a variable is missing or invalid:

  • the application must not start
  • the error must be immediate and explicit

Tolerating an invalid config is just moving the bug to production.

Why Node.js only?

Because the target runtime is clear:

  • APIs
  • workers
  • jobs
  • CI

Edge runtimes (Deno, Bun, Cloudflare Workers…) have:

  • different environment models
  • different constraints
  • different expectations

They are deliberately out of scope.

Why Zod?

Because Zod provides:

  • reliable TypeScript typing
  • consistent runtime validation
  • expressive transformations

The schema is both:

  • documentation
  • contract
  • validation
  • typing source

No other tool covers these four points as cleanly today.

Why not use dotenv-safe / env-schema / others?

These tools:

  • partially validate
  • provide little or weak typing
  • do not really document
  • do not offer a coherent CLI

dotenv-never-lies assumes a stricter scope,
but provides a full chain:
schema → validation → typing → CI → export.

Why not manage secrets?

Because it is not the right level.

dotenv-never-lies:

  • identifies secrets
  • can exclude, mask, or export them

But it:

  • encrypts nothing
  • stores nothing

It integrates with existing tools; it does not compete with them.

Conclusion:

dotenv-never-lies does not aim to be flexible. It aims to be reliable, explicit, and predictable.