envs

One store for every .env in the project.

dotenv reads a file into process.env, which is all most projects need. This is for the ones with four files, three machines, and a credential that gets rotated.

npx @modootoday/envs init
+ catalog created .envs/catalog.sqlite
+ added to .gitignore .envs/ Recovery codes — 5, shown once, not stored
  1575H-MPBMP-K41AR-PXAVB-NN920-G
  G8T2M-4KQZR-7VXWD-3NBHE-J05YA-P
  … envs load .env
+ .env 12 keys
+ release 8f21ac04 is current 12 values envs run -- node server.js
- running with 12 values  node

The values live in a SQLite catalog instead of a file. That is what lets envs doctor name the file a value came from, and makes envs rollback a pointer move rather than a re-import.

Get started How it compares

What the store buys you

Which file set this key

When .env and .env.local both declare DATABASE_URL, envs doctor names the one that won. It prints the key and the source. It never prints the value.

One secret under two names

DB_URL and DATABASE_URL holding the same value is invisible to anything that compares names, and it is not a duplicate — it is a second copy free to drift. Rotate one and the other still holds what you retired. envs doctor names both keys and, as always, neither value.

Losing the key is survivable

init prints five recovery codes once. Each wraps the same data key, so any of them opens the catalog on its own, and the catalog stores only the wraps.

A file that isn't env format gets refused

dotenv parses a two-line YAML document into environment variables. envs checks the file first, and if it fails, nothing loads at all.

Rollback is a pointer move

Releases are immutable. Changing a value writes a new release; envs rollback moves the current pointer back and deletes nothing.

Point dotenv at the wrong file

It does not fail. It hands back keys and values, and the process starts with the wrong configuration. Measured against dotenv 17.4.2:

Input dotenv envs
name: envs over two lines parses into keys rejected
KEY: value {KEY: "value"} rejected
A="oops, rest valid keeps going rejected
{"a": 1} {}, silently rejected

Every input we measured, and the rule envs uses

Migrating from dotenv

config() takes the same options: path, encoding, override, processEnv. It is synchronous, so the side-effect import still works.

- import "dotenv/config";
+ import "@modootoday/envs/config";

The command names follow dotenvx: run -- cmd, get, set, ls, rotate. If you have used it, most of this is already familiar.

Three reasons you might not want this

The licence is not OSI open source

Elastic License 2.0: use it inside your company at any scale, ship it in a product you sell, fork it. What you cannot do is offer it to third parties as a hosted service. If your policy requires an OSI licence, dotenvx is the better choice.

Startup costs about 6.6 ms

That is for a hundred keys. dotenv resolves the same set in 0.2 ms, because it decrypts and authenticates nothing. If that gap matters in your process, stay on dotenv.

One file does not need a store

A single .env and one developer is what dotenv is for. No key to manage, nothing to back up.

No account. The catalog is a file in your project. envs makes no network call unless you run backup or serve. When you want that file on a second machine or shared with a team, a hosted catalog keeps it in sync — still sealed, because the server has no key to open it with.