@modootoday/envs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,350 @@
1
+ ---
2
+ name: env-value-store
3
+ description: Use when environment values are spread across several .env files, layers or machines and something reads the wrong one — a key defined twice with different values, a config file loaded as if it were env, a value that works locally and is missing in CI, or a secret that needs rotating in more than one place. Covers @modootoday/envs, which keeps the values in an encrypted catalog and answers where each one came from.
4
+ ---
5
+
6
+ # Using `@modootoday/envs`
7
+
8
+ ## When to reach for it
9
+
10
+ Use the catalog when multiple env files or machines make provenance, conflicts,
11
+ rotation and rollback hard to track. For one file in one project, dotenv is
12
+ usually sufficient. The parser, catalog, keyring, loader and command dispatch
13
+ exist; that does not establish that this package has been published to a registry.
14
+
15
+ ## Install and wire
16
+
17
+ The package is not published yet. Build it in its owning repository and install
18
+ an approved local directory or tarball; do not assume a registry version exists.
19
+
20
+ ```sh
21
+ npm install /path/to/approved/envs-package
22
+ ```
23
+
24
+ Two entries. `import { config } from "@modootoday/envs"` calls it explicitly,
25
+ and `import "@modootoday/envs/config"` loads on import the way `dotenv/config`
26
+ does; both resolve under `require` as well, so `node -r @modootoday/envs/config`
27
+ works. The CLI executable is `envs`.
28
+
29
+ ## What the consuming repository must supply
30
+
31
+ Supply the project directory, env inputs, a supported SQLite runtime and an
32
+ unlock method. Keep the catalog gitignored and recovery codes outside it. For
33
+ CI, disable the personal global layer and declare required key names rather
34
+ than copying personal secrets. Backup destinations and server credentials are
35
+ explicit configuration, not values this package discovers by guessing.
36
+
37
+ ## API
38
+
39
+ ### Public module and command boundary
40
+
41
+ The root exports parsing (`parseEnv`, `toRecord`), catalog and keyring operations,
42
+ `config`, and command dispatch. `parseEnv` returns `{ ok, entries, findings }`;
43
+ `toRecord` returns a record only for a valid parse and `null` otherwise. CLI help
44
+ comes from command definitions, so consult it rather than guessing option names.
45
+
46
+ ### build is the dangerous one
47
+
48
+ It bakes values into a module for targets that cannot read a file. Default is **nothing
49
+ selected**, and a key not classified `low` is refused even when named — unclassified
50
+ reads as `medium`, so saying nothing gives the safe answer. `--allow-sensitive` exists
51
+ and should be argued for, not reached for: a deployed bundle cannot be recalled.
52
+
53
+ ### serve hands over bytes, not values
54
+
55
+ It serves the sealed catalog. It holds no key and cannot decrypt, so a compromised server
56
+ gives up what a stolen catalog file would and no more. Requiring a token of real length
57
+ is the whole security of the endpoint, so it is required rather than generated quietly,
58
+ and binding to anything but localhost warns that the token crosses the wire.
59
+
60
+ `--once` stops after the catalog is **handed over**, not after any request: an
61
+ unauthorised probe must not end a one-shot pull before the client with the token arrives.
62
+
63
+ ### Backups follow the same provider rule as the sqlite backends
64
+
65
+ Differences are data, eligibility is declared rather than discovered by failing, and a
66
+ pin exercises the non-default path. `s3` covers S3, R2, MinIO and Backblaze — what
67
+ differs between them is a hostname — and `file` writes to a directory. Neither is
68
+ eligible unless configured: a destination nobody set is a missing setting, not a failed
69
+ write.
70
+
71
+ A snapshot seals the catalog under its data key and carries that key's **wraps in the
72
+ header**. Without them a backup could only be opened by the catalog it came from, which
73
+ is the thing that may be gone — so **a recovery code alone restores**, which is the whole
74
+ point. The wraps are already sealed blobs, so the header leaks nothing.
75
+
76
+ SigV4 is implemented here rather than depended on, and verified against the derivation
77
+ vector AWS publishes. **No live endpoint was exercised**; say so rather than implying the
78
+ S3 path has been run against a real bucket.
79
+
80
+ - **Restore must delete the `-wal` and `-shm` beside the catalog.** Replacing only the
81
+ main file leaves the old journal, and sqlite replays it onto the restored database —
82
+ the values the snapshot was taken to undo come straight back. Measured on bun, where
83
+ the write had not been checkpointed; node passed by luck. It pairs with the
84
+ `wal_checkpoint(TRUNCATE)` backup takes before reading the file.
85
+
86
+ ### The surface deliberately matches dotenvx
87
+
88
+ dotenvx, Doppler and Infisical have converged on the same shape, so this package uses it
89
+ rather than inventing names: `run -- <cmd>` to inject, `get`/`set`/`del` for values,
90
+ `ls` to list, `export --format` to take them out, plus `rotate`, `genexample`,
91
+ `gitignore` and `precommit`. `set` accepts both `KEY VALUE` and `KEY=VALUE` because the
92
+ three tools disagree about which one is canonical — but refuses them mixed, since
93
+ `set A=1 B=2` could be read either way.
94
+
95
+ Three deliberate differences, worth stating when a user expects dotenvx exactly:
96
+
97
+ - **No `encrypt`/`decrypt`.** dotenvx encrypts a file in place; this keeps a store, and
98
+ the round trip is `load` then `export`.
99
+ - **`get` with no key does not print everything.** dotenvx does, but that would make the
100
+ `export --yes` gate meaningless. Names come from `ls --keys`, values from `export`.
101
+ - **No `native`, `armor` or `lock`** — those are that service's own features.
102
+
103
+ `rotate` is cheap here because of the key hierarchy: it re-wraps the data key and
104
+ re-encrypts nothing. New wraps are written **before** the old ones are retired, so an
105
+ interruption leaves too many ways in rather than none.
106
+
107
+ ### The shape of a change
108
+
109
+ Releases are immutable. `load` and `set` both write a **new** release carrying every
110
+ value with the change applied, and move the pointer; `rollback` moves the pointer back.
111
+ Nothing is deleted, which is what makes going back cheap and reversible. Two consequences
112
+ worth telling a user: `load` carries other sources forward unless `--replace`, so loading
113
+ one file does not empty the rest; and `set` refuses to guess which source owns a key when
114
+ there is more than one.
115
+
116
+ `doctor` answers where a value came from. Same key with the same value in two sources
117
+ warns, different values error, and neither prints a value. It also names the catalogs it
118
+ actually read — running in the wrong directory is the one trap this layout has.
119
+
120
+ ### Losing a key does not lose the catalog
121
+
122
+ Values are sealed under a random DEK, and the DEK is wrapped once per way back: the KEK,
123
+ and each recovery code. Any one wrap opens it, so a lost KEK costs nothing while a
124
+ recovery code survives, and a new way back can be added without re-encrypting a value.
125
+
126
+ **Recovery codes are printed once at `init` and never stored** — the table holds only the
127
+ wrapped DEK, so it cannot give a code back. Codes are Crockford base32 (no I, L, O or U)
128
+ and are accepted in any case with any separators, because refusing a transcription slip
129
+ the alphabet was designed to absorb would mean a lost catalog.
130
+
131
+ To open a catalog with one:
132
+
133
+ ```
134
+ printf '%s' "$CODE" | npx @modootoday/envs export --yes --recovery-code -
135
+ ```
136
+
137
+ `export` is the only command that prints values, so it refuses without `--yes`, prefers
138
+ stdin over argv for the code, and warns when a code is passed inline where other
139
+ processes can see it. Its CSV quotes every field — env values really do contain commas,
140
+ quotes and newlines — and a value starting with `=`, `+`, `-` or `@` is **reported, not
141
+ rewritten**: altering it would hand back something the catalog does not hold.
142
+
143
+ ## Worked examples
144
+
145
+ ### Parse without changing the environment
146
+
147
+ ```ts
148
+ import { parseEnv, toRecord } from "@modootoday/envs";
149
+
150
+ const parsed = parseEnv("MODE=test\n");
151
+ if (!parsed.ok) throw new Error("Invalid env document");
152
+ const values = toRecord(parsed);
153
+ // values is local data; no process environment or catalog has been modified.
154
+ ```
155
+
156
+ ### Refuse an input that is not env format
157
+
158
+ ```ts
159
+ import { parseEnv, toRecord } from "@modootoday/envs";
160
+
161
+ const parsed = parseEnv("name: service\n");
162
+ if (parsed.ok || toRecord(parsed) !== null) throw new Error("Expected refusal");
163
+ // Report parsed.findings (codes and line numbers), never candidate values.
164
+ ```
165
+
166
+ ## Testing against it
167
+
168
+ ### Runtime and database fixtures
169
+
170
+ The suite must pass under **both** runtimes, and running only one hides real defects.
171
+ `npx vitest run` uses node; `bunx --bun vitest run` uses bun.
172
+
173
+ Three sqlite backends sit behind one interface, and everything they disagree about is
174
+ data in `src/sqlite/provider.ts`. Do not reintroduce these differences in calling code:
175
+
176
+ - **Booleans.** node and better-sqlite3 throw on a bound boolean, bun accepts it. Bind
177
+ through the adapter, which normalises to 0/1.
178
+ - **A missing row.** bun returns `null` from `get()` and node returns `undefined`, so a
179
+ `!== undefined` check passes on one and dereferences null on the other. The adapter
180
+ returns `undefined` on both; do not compare against `null` in calling code.
181
+ - **Named parameters.** bun needs the sigil form (`$a`), better-sqlite3 needs the bare
182
+ name (`a`), node takes either. Getting it wrong is **not always an error**: bun binds
183
+ NULL and says nothing, so the adapter matches keys against the statement's own
184
+ placeholders.
185
+ - **Constructor options.** bun rejects `{}` and `{readonly:false}` and wants
186
+ `create: true`; node spells it `readOnly` and **silently ignores** the lowercase
187
+ spelling, handing back a writable database; better-sqlite3 wants the lowercase name.
188
+ Assert that a write is actually refused, never that a flag was passed.
189
+ - **Eligibility is checked, not attempted.** Loading better-sqlite3 under bun 1.3.14
190
+ panics the process with `NAPI FATAL ERROR` — no try/catch contains it. A provider
191
+ declares where it may run.
192
+ - **Import through a variable specifier.** A literal makes TypeScript try to resolve
193
+ `bun:sqlite` and makes bundlers treat the absent module as a hard failure.
194
+
195
+ `config()` is synchronous. Its entry point resolves the binding through
196
+ `createRequire` and uses `node:crypto` rather than Web Crypto. Adding an `await`
197
+ changes the public contract. The supported call is the named root export;
198
+ there is no exported `/config` side-effect subpath.
199
+
200
+ Commands are values in `src/commands/`: name, description, option specs, `run`. Help is
201
+ generated from the spec, so a flag cannot exist in the parser and not the help, and an
202
+ option the command did not declare is an **error** rather than a silently ignored typo.
203
+ Output goes through `src/cli/ui.ts` — values to stdout so they can be piped, everything
204
+ else to stderr, colour only on a TTY without `NO_COLOR`. No dependency for either.
205
+
206
+ - **Never write a control byte into a source file.** Build it from `String.fromCharCode`
207
+ and reference the constant. This has gone wrong twice here: a NUL in a test fixture and
208
+ raw escape bytes in the colour table.
209
+ - better-sqlite3 is an **optional peer**: not installed by this package, so its rows in
210
+ the backend matrix only run where someone installed it. `ENVS_SQLITE_BACKEND` pins one
211
+ backend when you need to exercise a specific path.
212
+
213
+ ### Values are never syntax
214
+
215
+ Env values are arbitrary text from files this package does not control, so no statement
216
+ is ever built from data — no interpolation, no concatenation, parameters only. A guard
217
+ test scans every source file for both patterns and proves the detector fires on a
218
+ fixture. Two consequences worth keeping in mind:
219
+
220
+ - Key names reach the database as HMACs and values as sealed blobs, so even the
221
+ identifier surface carries no caller text.
222
+ - A string containing a NUL is **refused**. Measured: sqlite TEXT ends at the first NUL,
223
+ so `a\0b` is stored and read back as `a`, losing the rest without an error.
224
+
225
+ Do not tell a user a command works until it appears in the list above.
226
+
227
+ ## Invariants
228
+
229
+ ### Catalog ownership
230
+
231
+ `dotenv` reads a file and puts it in `process.env`. That is enough until there is more
232
+ than one file. Then nobody can answer which file supplied a value, a key defined in two
233
+ places silently resolves to one of them, and rotating a credential means finding every
234
+ copy.
235
+
236
+ This package keeps the values in a catalog — a small encrypted SQLite database — and
237
+ treats files as inputs to it. Because there is a store rather than a file, it can answer
238
+ where a value came from, hold releases and a pointer so a rollback moves the pointer, and
239
+ carry an audit trail.
240
+
241
+ Reach for it when:
242
+
243
+ - The same key exists in more than one file and you need to know which one wins.
244
+ - A value works on one machine and is missing on another.
245
+ - A credential must be rotated and you do not know how many copies exist.
246
+ - A `.env` path actually points at YAML or JSON and is being read as env anyway.
247
+
248
+ Do not reach for it to hold one `.env` in one project. `dotenv` is the right size there.
249
+
250
+ ### The format rule, and why it is strict
251
+
252
+ An env document is a sequence of logical entries, each one of exactly three kinds:
253
+
254
+ 1. a line starting with `#`
255
+ 2. a blank line
256
+ 3. `KEY=VALUE`
257
+
258
+ Entries are logical, not physical: a quoted value may span several lines. Anything that
259
+ is not one of the three means the file is not env format, and then **none** of it loads.
260
+
261
+ This matters because `dotenv` is lenient in ways that produce plausible wrong answers.
262
+ Measured against dotenv 17.4.2:
263
+
264
+ | Input | dotenv | this package |
265
+ | ------------------------------------------ | ------------------------------------- | ------------------- |
266
+ | `name: envs` over two lines (YAML) | parses into keys | rejected |
267
+ | `KEY: value` | `{KEY: "value"}` | rejected |
268
+ | `A="oops` unterminated, rest of file valid | `A` becomes `"oops`, rest still loads | whole file rejected |
269
+ | `{"a": 1}` (JSON) | `{}`, no error | rejected |
270
+
271
+ Pointing a loader at the wrong file should fail, not return something that looks like
272
+ configuration.
273
+
274
+ ### Value syntax
275
+
276
+ - `VALUE` may be bare, or wrapped in `"`, `'`, or a backtick.
277
+ - Inside quotes, `#` and `=` are literal. A bare value ends at the first `#`.
278
+ - Whitespace is trimmed only from bare values.
279
+ - **Double quotes expand `\n` and `\r` and nothing else.** `\t`, `\\` and `\"` stay
280
+ literal, which is what keeps `"C:\path\to"` intact. Never parse a value with
281
+ `JSON.parse` — it throws on Windows paths and expands escapes dotenv does not.
282
+ - Keys are not required to be uppercase. `api_key=secret` is env format. Casing is a
283
+ convention, so `lint` mentions it and the parser does not enforce it.
284
+
285
+ ### Where things live
286
+
287
+ | | Path | Note |
288
+ | ------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
289
+ | Catalog (source of truth) | `<project>/.envs/catalog.sqlite` | `~/.envs/` only when there is no project root; `ENVS_CATALOG_PATH` overrides |
290
+ | Cache (derived) | `node_modules/.cache/envs/` when installed, `~/.envs/cache/` under npx or a global install | Safe to delete; rebuilt from the catalog |
291
+ | Requirements | `envs.requires` at the project root | Committed. Names only, never values |
292
+
293
+ Two rules follow from this and are worth stating to a user before they are surprised:
294
+
295
+ - **The catalog stays with the project even under a global install.** Installing the CLI
296
+ with `-g` while the runtime is a devDependency is the ordinary setup; a home-directory
297
+ catalog would mean a value set inside a project is invisible to that project.
298
+ - **`.envs/` must be gitignored.** It holds envelopes, history and audit rows. `doctor`
299
+ reports an error, not a warning, if it is ever tracked.
300
+
301
+ ### The global layer
302
+
303
+ `~/.envs` is a settings layer, not a second store. It holds what belongs to a person and
304
+ a machine — a personal API key, `ENVS_PROVIDER`, backup targets — and the loader resolves:
305
+
306
+ ```
307
+ process.env > project catalog and files > ~/.envs
308
+ ```
309
+
310
+ The project always wins; the global layer only fills keys the project does not define.
311
+ It is switched off with `config({ global: false })` or `ENVS_NO_GLOBAL=1`, and CI should
312
+ switch it off, because a build that passes locally on a home-directory value and fails in
313
+ CI is the standard failure of this pattern.
314
+
315
+ ### Diagnosing, and repairing a split
316
+
317
+ `doctor` answers where each value came from and never prints a value.
318
+
319
+ When it reports keys that exist only in the global layer, there are two repairs and they
320
+ move different things:
321
+
322
+ - `doctor --require <KEY>` records that the key is needed, in `envs.requires`. It carries
323
+ no value, so it can be committed, which is what lets it reach a teammate or CI at all.
324
+ `config()` then throws when a required key resolves nowhere, instead of the application
325
+ receiving `undefined`.
326
+ - `doctor --adopt <KEY>` moves the value into the project. Correct only when the value was
327
+ project scoped and had been left global by mistake.
328
+
329
+ Prefer `--require`. Copying a value into `<project>/.envs/` makes the project
330
+ self-contained on one machine only — that directory is gitignored, so a teammate and CI
331
+ are exactly where they were. `--adopt --all` is refused by default: personal credentials
332
+ are what the global layer exists to hold once, and copying them per project turns one
333
+ rotation into many edits.
334
+
335
+ ## What it will not do
336
+
337
+ It does not provision credentials, make a registry release available, or prove
338
+ that an S3 destination has been exercised live. Sealed data still needs an
339
+ external recovery path. Report the tested runtime and backend rather than
340
+ claiming every deployment has been validated.
341
+
342
+ ### Reporting rules
343
+
344
+ When surfacing anything from this tool to a user:
345
+
346
+ - **Never print a value.** Report the key, the file, the line, and whether two values are
347
+ the same or different. The existing lint rule `duplicate-key-across-layers` sets the
348
+ convention: same value warns, different value errors, neither prints.
349
+ - Say which catalog was actually used. Running in a directory with no project root writes
350
+ to `~/.envs`, and a user who does not notice will look for the value in the wrong place.
package/LICENSE ADDED
@@ -0,0 +1,93 @@
1
+ Elastic License 2.0
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject to
14
+ the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set of
20
+ the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other notices
27
+ of the licensor in the software. Any use of the licensor’s trademarks is subject
28
+ to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license for
39
+ the software granted under these terms ends immediately. If your company makes
40
+ such a claim, your patent license ends immediately for work on behalf of your
41
+ company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from you
46
+ also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license no
61
+ later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your licenses
64
+ to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ *As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.*
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is the
76
+ software the licensor makes available under these terms, including any portion
77
+ of it.
78
+
79
+ **you** refers to the individual or entity agreeing to these terms.
80
+
81
+ **your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that
84
+ organization. **control** means ownership of substantially all the assets of an
85
+ entity, or the power to direct its management and policies by vote, contract, or
86
+ otherwise. Control can be direct or indirect.
87
+
88
+ **your licenses** are all the licenses granted to you for the software under
89
+ these terms.
90
+
91
+ **use** means anything you do with the software requiring one of your licenses.
92
+
93
+ **trademark** means trademarks, service marks, and similar rights.
package/NOTICE ADDED
@@ -0,0 +1,24 @@
1
+ @modootoday/envs
2
+
3
+ Copyright (c) 2026 modootoday. All rights reserved.
4
+
5
+ Licensor: modootoday
6
+ Licensed under the Elastic License 2.0; see LICENSE.
7
+
8
+ The licensor is modootoday, the company, not any individual contributor.
9
+ This file names the licensor because the Elastic License 2.0 defines that term
10
+ rather than carrying a copyright line of its own; LICENSE is kept unmodified.
11
+
12
+ What the licence permits, in short, without replacing the terms in LICENSE:
13
+
14
+ - Using this software inside your own company, on any scale.
15
+ - Embedding it in a product you sell, and modifying or forking it.
16
+ - Redistributing it, with these notices intact.
17
+
18
+ What it does not permit:
19
+
20
+ - Offering this software to third parties as a hosted or managed service.
21
+ - Circumventing licence key functionality.
22
+ - Removing or obscuring these notices.
23
+
24
+ LICENSE governs. Where this summary and LICENSE differ, LICENSE is what applies.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @modootoday/envs
2
+
3
+ Environment values in an encrypted catalog rather than scattered `.env` files.
4
+
5
+ **[envs.build](https://envs.build)** has the documentation. This page is the introduction.
6
+
7
+ ```console
8
+ $ npx @modootoday/envs init
9
+ + catalog created
10
+ + added to .gitignore
11
+
12
+ Recovery codes — 5, shown once, not stored
13
+ 1575H-MPBMP-K41AR-PXAVB-NN920-G
14
+ G8T2M-4KQZR-7VXWD-3NBHE-J05YA-P
15
+
16
+
17
+ $ envs load .env
18
+ + .env 12 keys
19
+ + release 8f21ac04 is current
20
+
21
+ $ envs run -- node server.js
22
+ ```
23
+
24
+ ## What it does that a file cannot
25
+
26
+ `dotenv` reads a file into `process.env`, which is all most projects need. This is for
27
+ the ones with four files, three machines, and a credential that gets rotated.
28
+
29
+ - **Tells you which file set a key.** When `.env` and `.env.local` both declare
30
+ `DATABASE_URL`, `envs doctor` names the one that won. It prints the key and the
31
+ source, never the value.
32
+ - **Survives a lost key.** `init` prints five recovery codes once. Any one of them opens
33
+ the catalog on its own, and none of them is ever stored.
34
+ - **Refuses a file that is not env format.** `dotenv` reads a two-line YAML document as
35
+ environment variables. This checks first, and if the file fails, nothing loads.
36
+ - **Rolls back without deleting anything.** Changing a value writes a new release. Going
37
+ back moves a pointer.
38
+
39
+ ## Migrating from dotenv
40
+
41
+ `config()` takes the same options — `path`, `encoding`, `override`, `processEnv` — and it
42
+ is synchronous, so the side-effect import still works.
43
+
44
+ ```diff
45
+ - import "dotenv/config";
46
+ + import "@modootoday/envs/config";
47
+ ```
48
+
49
+ Both module systems resolve, so the preload form works without touching your code:
50
+
51
+ ```console
52
+ $ node -r @modootoday/envs/config server.js
53
+ ```
54
+
55
+ One difference worth knowing before you switch: `dotenv` returns quietly when there is no
56
+ file, while this throws when there is no catalog. A missing store is the failure this
57
+ exists to make visible, so it is loud rather than empty.
58
+
59
+ The command names follow dotenvx: `run -- cmd`, `get`, `set`, `ls`, `rotate`. If you have
60
+ used it, most of this is already familiar.
61
+
62
+ ## Documentation
63
+
64
+ | | |
65
+ | --- | --- |
66
+ | Getting started | <https://envs.build/guide/> |
67
+ | The format rule, measured against dotenv | <https://envs.build/format/> |
68
+ | Recovery codes | <https://envs.build/recovery/> |
69
+ | How it compares to dotenv, dotenvx, Doppler and Infisical | <https://envs.build/compare/> |
70
+ | Sharing a catalog, and what the server cannot see | <https://envs.build/hosted/> |
71
+ | All 28 commands | <https://envs.build/commands/> |
72
+
73
+ ## Requirements
74
+
75
+ Node.js 22 or Bun 1.3. Nothing to sign up for, and no network call unless you ask for one.
76
+
77
+ ## Before you commit to it
78
+
79
+ - **The licence is not OSI open source.** See below.
80
+ - **It starts slower than dotenv** — about 6.6 ms for a hundred keys against 0.2 ms.
81
+ - **One `.env` and one developer does not need this.** dotenv already does that job, with
82
+ no key to manage and nothing to back up.
83
+
84
+ ## Licence
85
+
86
+ Elastic License 2.0. Licensor: **modootoday**. See [`LICENSE`](LICENSE) and
87
+ [`NOTICE`](NOTICE), and <https://envs.build/licence/>.
88
+
89
+ Use it inside your company at any scale, ship it in a product you sell, fork it. What you
90
+ cannot do is offer this software to third parties as a hosted or managed service. It
91
+ carries a patent grant with a retaliation clause: asserting patent claims over this
92
+ software ends your licence.
93
+
94
+ ---
95
+
96
+ Repository <https://github.com/modootoday/envs> · npm
97
+ [`@modootoday/envs`](https://www.npmjs.com/package/@modootoday/envs)