# envapt > Read config as real typed values from any source, process.env, a Cloudflare Workers binding, a browser bundle, or any object you bind. Zero runtime dependencies and zod/valibot/arktype validation. Runs on Node, Bun, Deno, Cloudflare Workers, and the browser. - Import the readers and decorators from `envapt` on every runtime. The package's export conditions pick the Node build or the portable build. The legacy decorators for `experimentalDecorators` come from `envapt/legacy`. - On Node, Bun, and Deno, envapt reads `process.env` and the `.env` files on import. On Cloudflare Workers, in the browser, and on other edge runtimes, call `Envapter.useSource` before the first read. - Method names and import paths changed between majors. Read the page for an API before you write code against it. The migration pages list what changed in each major. - Each docs page has a markdown copy, and https://envapt.materwelon.dev/llms.txt links every one. A request for a docs page with `Accept: text/markdown` also returns the markdown. - The steps for writing envapt code are in the agent skill at https://envapt.materwelon.dev/.well-known/agent-skills/envapt/SKILL.md. # Migration (v4 to v5) Upgrade to envapt v5, where Converters.array() and envFileOptions replace the old names. Most of v5 is renames that TypeScript reports once you upgrade. [Behavior changes to watch](#behavior-changes-to-watch) lists the changes that compile cleanly but run differently. ## Array converters `Converters.Array` and the inline `{ delimiter, type }` object are replaced by the `Converters.array(...)` builder. ```ts // v4 @Envapt('PORTS', { converter: { delimiter: ',', type: Converters.Number }, fallback: [] }) // v5 @Envapt('PORTS', { converter: Converters.array({ of: Converters.Number }), fallback: [] }) ``` `of` defaults to `string`, and `delimiter` defaults to `,`. `of` also accepts a `(raw) => T` function for a typed element. `Converters` is now a plain object, so it isn't usable as a type anymore. Use `ConverterToken` in its place. See [Converters](/docs/converters). ## dotenvConfig is now envFileOptions `Envapter.dotenvConfig` is renamed to `Envapter.envFileOptions` (the type `DotenvConfigOptions` becomes `EnvFileOptions`). Only `encoding` and `override` remain. `quiet`, `DOTENV_KEY`, `debug`, `path`, and `processEnv` are gone. ```ts // v4 Envapter.dotenvConfig = { override: true, quiet: false, debug: true }; // v5 Envapter.envFileOptions = { override: true }; ``` To choose which files load, use [`Envapter.envPaths`](/docs/env-files#replacing-the-cascade). envapt doesn't read `DOTENV_KEY` (dotenv-vault). Decrypt the file yourself, then pass its path to `envPaths`. ## Debug output The `debug` option is now `Envapter.debug`. Output goes to stderr with an `[envapt]` prefix. v4 printed `[dotenv]`. ```ts // v4 Envapter.dotenvConfig = { debug: true }; // v5 Envapter.debug = 'verbose'; // or set ENVAPT_DEBUG=verbose ``` The debug levels are `silent` (default), `warn`, and `verbose`, as [Debug logging](/docs/errors#debug-logging) explains. ## Validation with Standard Schema Custom converters that throw still work. v5 also takes a [Standard Schema](/docs/standard-schema) from zod, valibot, arktype, or your own validator, passed as `schema`. ```ts // v4: a custom converter that throws @Envapt('PORT', { converter: (raw) => { const n = Number(raw); if (Number.isNaN(n)) throw new Error('bad PORT'); return n; } }) // v5 @Envapt('PORT', { schema: z.coerce.number().int().min(1024) }) ``` ## Decorators The classic positional form is deprecated in v5 and removed in v6. Use the options object. See the [v5 to v6 migration](/docs/migration-v5-to-v6). ```ts // v4 (removed in v6) @Envapt('PORT', 8080, Number) // v5 @Envapt('PORT', { converter: Converters.Number, fallback: 8080 }) ``` Declare a static field as `static readonly` and an instance field as `declare readonly`, both with no initializer. v7 later replaced this with the TC39 accessor form, as [Migration (v6 to v7)](/docs/migration-v6-to-v7) explains. [Decorators](/docs/decorators#declaring-decorated-fields) has the current forms. ## Node version v5 lowered the minimum Node version from `>=24` to `>=20`, since the old minimum existed only for an API envapt no longer uses. Bun `>=1.3` and Deno `>=2.5` are unchanged. ## Behavior changes to watch > **DANGER** > > These changes compile without edits but run differently from v4. Check them before upgrading. - **Per-environment files auto-load.** When `envPaths` isn't set, v5 loads a cascade of `.env.${env}.local`, `.env.${env}`, `.env.local`, then `.env`, where `${env}` is the detected environment. A stray `.env.local` or `.env.production` in your working directory now loads. v4 ignored it. The [.env files](/docs/env-files) page has the details. - **Array element failures throw.** If an element fails its converter, the read throws `EnvaptError` with code `ArrayElementConversionFailed` (206). v4 returned a malformed array without an error. - **Time-string fallbacks coerce.** With `CACHE_TTL` unset, `Envapter.getUsing('CACHE_TTL', Converters.Time, '10s')` now returns `10000` milliseconds. v4 returned the string `'10s'`. A malformed time-string fallback throws `MalformedTimeFallback` (105). v4 threw an older, more general code. [Errors](/docs/errors) lists every code. envapt leaves `process.env` unchanged by default, as v4 did. If you relied on loaded keys appearing in `process.env`, opt in with [`Envapter.syncProcessEnv`](/docs/env-files#coming-from-dotenv). --- # Migration (v5 to v6) Upgrade to envapt v6, which removes positional decorators and reads NODE_ENV=test as test. v6 removes the positional decorator form and changes what `NODE_ENV=test` resolves to. > **TIP** > > Upgrading further? v7 makes the decorators TC39 accessor decorators by default. See [Migration (v6 to > v7)](/docs/migration-v6-to-v7). ## Decorators The positional `@Envapt('KEY', fallback, converter)` form, deprecated in v5, is removed. Use the options object, or a [sugar decorator](/docs/decorators) for the common case. The no-argument `@Envapt('KEY')` form is unchanged. ```ts // v5 (removed in v6) @Envapt('PORT', 8080, Number) // v6 options object @Envapt('PORT', { converter: Converters.Number, fallback: 8080 }) // v6 sugar decorator, shorter than the old positional form @EnvNum('PORT', 8080) ``` The sugar decorators `@EnvNum`, `@EnvStr`, `@EnvBool`, `@EnvUrl`, and `@EnvTime` each wrap a built-in converter, so you pass only the key and the fallback. If the second argument isn't an options object, the decorator now throws `EnvaptError` with `InvalidUserDefinedConfig` when the class is defined, so a leftover positional call stops the app at startup. The internal `InferPrimitiveFallbackType` type is removed with the overload. ## Behavior changes to watch > **DANGER** > > This change compiles without edits but runs differently from v5. Check it before upgrading. - **`NODE_ENV=test` resolves to `Environment.Test`.** Jest and vitest set `NODE_ENV=test`, so under either one `Envapter.isTest` returns true and `isDevelopment` returns false. v5.1 and earlier returned true from `isDevelopment` there. Vite's `MODE=test` also resolves to `Environment.Test`. This first shipped by mistake in the 5.2.0 minor, which is deprecated on npm. If you branch on `isDevelopment` inside tests, switch to `isTest` or set `Envapter.environment` yourself, as [Environment](/docs/environment) shows. --- # Migration (v6 to v7) Upgrade to envapt v7, where decorators become TC39 accessors and the old ones move to envapt/legacy. v7 changes the decorators. Everything else (the `Envapter` readers, converters, sources, and types) is unchanged and stays on `envapt`. ## Decorators are now TC39 accessor decorators The default `@Envapt` and the sugar decorators are now TC39 Stage 3 accessor decorators. They don't need the `experimentalDecorators` flag. Bun and Deno run them from a `.ts` file directly, and Node runs them once you compile. Declare the field with the `accessor` keyword. A static field uses `static accessor x: T`. An instance field uses `accessor x!: T` with the definite-assignment `!`. ```ts // v6 (legacy form) import { Envapt, Converters } from 'envapt'; class Config { @Envapt('PORT', { converter: Converters.Number, fallback: 3000 }) static readonly port: number; } // v7 default (accessor form, no experimentalDecorators) import { Envapt, Converters } from 'envapt'; class Config { @Envapt('PORT', { converter: Converters.Number, fallback: 3000 }) static accessor port: number; } ``` For an instance field, the change is from `declare readonly x: T` to `accessor x!: T`. ## Keeping the legacy decorators The v6 decorators move to the `envapt/legacy` subpath, for code that can't switch to the accessor form yet. They keep the `static readonly` / `declare readonly` form and still need `experimentalDecorators`. ```ts // keep the v6 behavior import { Envapt, Converters } from 'envapt/legacy'; class Config { @Envapt('PORT', { converter: Converters.Number, fallback: 3000 }) static readonly port: number; } ``` Only the decorators move. In v7, a module that imports decorators only from `envapt/legacy` must also `import 'envapt'` once so envapt binds `FileSource` on Node. ## Decorated properties are read-only Assigning to a decorated property now throws `EnvaptError` with `InvalidUserDefinedConfig`. Before v7 the legacy decorator installed only a getter, so an assignment did nothing in sloppy mode and threw a native `TypeError` in strict mode. Both the accessor and legacy forms throw `EnvaptError` in v7. ## Decorated fields are typechecked TypeScript checks the decorated field's declared type against the converter's output. If the field can't hold the value, the compiler reports this error: ```txt [envapt] field type must hold the converter output ``` A read with no fallback can be `null` in v7. A field that compiled as `static readonly port: number` for `@EnvNum('PORT')` on v6 now needs `number | null`. In the accessor form that's `static accessor port: number | null`. v8 changes that `null` to `undefined`, as [v7 to v8](/docs/migration-v7-to-v8) explains. This check applies to both the accessor and legacy decorators. --- # Migration (v7 to v8) Upgrade to envapt v8, with shorter source class names and one import for workerd and the browser. v8 has these breaking changes: 1. The source classes are renamed. 2. The `envapt/workerd` and `envapt/browser` subpaths are removed. 3. On the portable build, the filesystem-only config APIs warn once and do nothing by default. 4. The public type exports are trimmed. 5. The required form of `getUsing` and `getWith` moves to `getRequired`. 6. The `Integer` and `Float` converters reject malformed values. 7. One rule decides when a value is missing, so ordered reads skip an empty key and whitespace-only values follow `strict`. 8. Built-in converter fallbacks are checked by value. 9. A missing read with no fallback resolves to `undefined` on every reader. 10. `getWith` passes the raw value to its custom converter. 11. An explicit `undefined` fallback counts as no fallback. Most upgrades touch only the first two. The rest apply only if you imported an internal type by name, typed a decorator field as `| null`, or relied on loose parsing, and each section says when it affects you. ## Source and type renames The source class names lose the `Env` infix. `PortableSource` is the source for any runtime where envapt can't read files, and it replaces both `ManualEnvSource` and `WorkerEnvSource`, which were the same class. `FileSource` replaces `NodeEnvSource`, and the `Source` type replaces `EnvSource`. | v7 | v8 | | --- | --- | | `ManualEnvSource`, `WorkerEnvSource` | `PortableSource` | | `NodeEnvSource` | `FileSource` | | `EnvSource` (type) | `Source` | ```ts // v7 import { Envapter, ManualEnvSource } from 'envapt'; Envapter.useSource(new ManualEnvSource(config)); // v8 import { Envapter, PortableSource } from 'envapt'; Envapter.useSource(new PortableSource(config)); ``` In v7.1, `PortableSource` and `Source` already existed, and `ManualEnvSource` / `WorkerEnvSource` / `EnvSource` were deprecated aliases. If you were already on v7.1, v8 only removes those aliases. `NodeEnvSource` is renamed to `FileSource` in v8. ## One universal `envapt` import The `envapt/workerd` and `envapt/browser` subpaths are gone. Import the main API from `envapt` on every runtime. The package export conditions resolve the portable build on Workers, edge runtimes, the browser, and react-native, and the node build on Node, Bun, and Deno. ```ts // v7 import { Envapter, WorkerEnvSource } from 'envapt/workerd'; import { Envapter, ManualEnvSource } from 'envapt/browser'; // v8 import { Envapter, PortableSource } from 'envapt'; ``` The edge runtimes that used to load the node build now resolve the portable build through the `edge-light`, `fastly`, and `worker` conditions. Vercel Edge and Cloudflare Workers set their conditions without any setup. On Fastly Compute, add the `fastly` condition to your bundler's resolve conditions. `envapt/config` and `envapt/legacy` are unchanged. ## File-only APIs warn and do nothing On the portable build, the filesystem-only config APIs (`envPaths`, `baseDir`, `envFileOptions`, `configureProfiles`, `resetProfiles`) used to throw `FileApiUnsupported`. They now warn once and do nothing by default, so a config module that touches them can be written once and run on both Node and the portable runtimes. To restore the v7 behavior, set `Envapter.fileApiMode = 'throw'`. ```ts // v7: calling a file API on the portable build threw FileApiUnsupported // v8: it warns once and does nothing by default Envapter.envPaths = ['.env']; // warns once, then does nothing on Workers // opt back into throwing Envapter.fileApiMode = 'throw'; Envapter.envPaths = ['.env']; // throws FileApiUnsupported again ``` A read with no bound source still throws `NoSourceBound` on the first access, whatever `fileApiMode` is, so a Worker that never binds a source throws before it serves any config. `fileApiMode` affects only the file APIs on the portable build. On the node build, binding a non-filesystem source and then calling a file API still throws `FileApiUnsupported`. ## The portable types now include the file APIs Before v8 the portable build's types omitted the five file APIs, so calling one was a compile error. v8 adds them to the portable types, so a config module shared between your Node dev setup and a Workers deploy type-checks the same in both places. At runtime each call warns once and does nothing, as above, or throws under `fileApiMode = 'throw'`. ## Required reads move to `getRequired` The `{ required: true }` options-object form of `getUsing` and `getWith` is removed. A required read is now `getRequired(key, converter)`, which takes the converter as its second argument, returns a value that's never `undefined`, and throws `MissingEnvValue` when the key is missing or empty. New in v8, `getRequiredAll(spec)` reads a group of required values in one call and returns a typed record, throwing once with every missing key. ```ts // v7 const url = Envapter.getUsing('DATABASE_URL', { converter: Converters.Url, required: true }); const upper = Envapter.getWith('NAME', { converter: (raw) => (raw ?? '').toUpperCase(), required: true }); // v8 const url = Envapter.getRequired('DATABASE_URL', Converters.Url); const upper = Envapter.getRequired('NAME', (raw) => raw.toUpperCase()); // v8, several at once const { DATABASE_URL: db, PORT: port } = Envapter.getRequiredAll({ DATABASE_URL: Converters.Url, PORT: Converters.Port }); ``` `getUsing` and `getWith` keep their positional fallback forms unchanged. The `@Envapt` decorator keeps its `{ required: true }` option. Only the functional readers lost the options object. ## Trimmed public type exports v8 exports 15 types, down from 36. These internal types that v7 exported are gone: - the source shape interfaces (`BareEnvSource` / `FileEnvSource`) - the decorator return types - the schema brands - the converter and inference helper types - `EnvKeyInput` - `ArrayOf` / `ArrayElement` - the `InferSchemaInput` / `InferSchemaOutput` aliases Inference on the readers and decorators is unchanged, since those types resolve at the call site. This affects you only if you imported one of these by name. - If you typed a custom source against `BareEnvSource` / `FileEnvSource`, type it against the `Source` union. - Replace `InferSchemaOutput` with your validator's own inference (`z.infer`, valibot's `InferOutput`, arktype's `.infer`) or `StandardSchemaV1.InferOutput`. ## Stricter `Integer` and `Float` converters `Converters.Integer` and `Converters.Float` used to parse with `parseInt` and `parseFloat`, which read a leading number and ignored the rest. `42abc` became `42`, `3.9` became `3` under `Integer`, and a value larger than 2^53 lost precision. v8 parses both with `Number` and checks the result. If `Number` rejects a value, the read returns the fallback, or throws under `getRequired`. `Integer` requires `Number.isSafeInteger`, so `42abc`, `3.9`, and values larger than 2^53 in magnitude all fall back. `Float` rejects trailing characters like `3.14xyz`. `Float` still accepts `Infinity`, matching the `number` converter and `getNumber`. ```ts import { Converters, Envapter } from 'envapt'; // with PORT="8080abc" Envapter.getRequired('PORT', Converters.Integer); // v7: 8080 (parseInt read the leading number) // v8: throws MissingEnvValue (Number rejects it) // v9: throws InvalidEnvValue (210) ``` Change something here only if your app relied on the loose parse. A value that's already a clean integer or float reads the same. ## The missing-value rule is unified envapt uses one rule for when a value counts as missing, across ordered-key reads, `getRequired` / `getRequiredAll`, `Envapter.require`, the `@Envapt({ required: true })` decorator, environment detection, and templates. A value is missing if it's unset or an empty string. Under `Envapter.strict = true`, a whitespace-only value is missing too. Two v7 behaviors change. **Ordered reads skip a present-but-empty key.** An ordered read (`get`, `getNumber`, the decorators, or an array of keys) skips a key set to an empty string, the same as an unset key. v7 stopped at the first present key even when its value was blank. ```ts import { Envapter } from 'envapt'; // with PRIMARY="" and FALLBACK="value" Envapter.get(['PRIMARY', 'FALLBACK'], 'default'); // v7: "default" (stopped at the empty PRIMARY) // v8: "value" (falls through to FALLBACK) ``` **Whitespace-only values follow `strict`.** `Envapter.require` and the `@Envapt({ required: true })` decorator keep a whitespace-only value in the default non-strict mode. v7 treated it as missing. Set `Envapter.strict = true` to treat it as missing again. Environment detection skips a whitespace-only key only under strict mode. v7 used the key in every mode. ```ts // with NAME=" " (whitespace only) Envapter.require('NAME'); // v7: throws MissingEnvValue // v8 (non-strict): passes // v8 (strict): throws MissingEnvValue ``` `getRequired` and `getRequiredAll` are new in v8 and use this rule from the start, so nothing changes for them. ## Built-in converter fallbacks are validated by value v7 checked only a fallback's type (a `number` for `Number`, a `Date` for `Date`). v8 also checks the value. A fallback the converter rejects now throws `FallbackConverterTypeMismatch`, even where v7 accepted it. That covers a `NaN` `Number` or `Float` fallback, a non-safe-integer `Integer` fallback, and an `Invalid Date` fallback. The new `Port` and `Email` converters require a valid fallback too. ```ts import { Converters, Envapter } from 'envapt'; // with SAMPLE_RATE unset Envapter.getUsing('SAMPLE_RATE', Converters.Float, NaN); // v7: returned NaN // v8: throws FallbackConverterTypeMismatch ``` This matters only if you passed a placeholder fallback such as `NaN` that isn't a real value. Pass a valid fallback, or omit it to get `T | undefined`. ## A missing read with no fallback resolves to `undefined` A read of a missing key with no fallback resolves to `undefined` everywhere. The functional readers (`get`, `getNumber`, `getUsing`, `getWith`, `getRaw`) already returned `undefined`. The decorators returned `null` on this path. v8 returns `undefined` from the decorators too, so one value marks an absent read across the whole API. A decorator field with no fallback is now typed `T | undefined`. A field typed `T | null` stops type-checking. Retype it to `T | undefined`. ```ts // v7 class Config { @Envapt('OPTIONAL_TOKEN') static accessor token: string | null; } // Config.token is null when OPTIONAL_TOKEN is unset // v8 class Config { @Envapt('OPTIONAL_TOKEN') static accessor token: string | undefined; } // Config.token is undefined when OPTIONAL_TOKEN is unset ``` You only need a change here if you typed a no-fallback decorator field as `| null`, or branched on a decorator read with `=== null`. Nullish handling (`?? fallback`, `== null`) keeps working. ## `getWith` passes the raw value to your converter `getWith` calls your custom converter with the raw value on every read. For a missing key the converter receives `raw === undefined`. v7 returned the fallback for a missing key without running the converter. The converter already received the fallback as the second argument, so you can still return it when `raw` is missing. ```ts const name = Envapter.getWith('NAME', (raw, fallback) => raw?.toUpperCase(), 'ANON'); // with NAME unset // v7: 'ANON' (the converter never ran) // v8: undefined (the converter ran with raw === undefined) ``` A converter that reads `raw` without a guard now runs on a missing key. Guard it with `raw ?? ''` or an early `if (!raw) return fallback`. In v8, the `ConverterFunction` type declared `raw` as `string | undefined` and the fallback as the second parameter. The `@Envapt` custom-converter decorator already ran the converter on a missing key, so v8 makes `getWith` behave the same way. ## An explicit `undefined` fallback counts as no fallback Passing `undefined` as the fallback argument counts as no fallback, across every reader and the `@Envapt` options. `Envapter.parse` previously read an explicit `undefined` as a provided fallback and returned it for a missing key. It now throws `MissingEnvValue`. ```ts // with API_URL unset Envapter.parse('API_URL', schema, undefined); // v7: undefined // v8: throws MissingEnvValue ``` You only need a change here if you passed `undefined` to `parse` to make a key optional. Pass a real fallback, or guard the read with `getRaw` before parsing. --- # Migration (v8 to v9) Move a v8 app to envapt v9, which needs Node 22.23.3 and ESM, and find changes that still compile. v9 requires Node 22.23.3 or newer, and it ships as ESM only. Most of its other changes are [renames and removals](#renames-and-removals), and in TypeScript each one is a compile error. The [behavior changes](#behavior-changes) below are the ones to search for by hand. ## Before you upgrade Node 20 reached end of life in April 2026, so v9 raises the minimum Node version from 20 to 22.23.3. Bun `>=1.3.11` and Deno `>=2.5` haven't changed. envapt also stopped shipping a CommonJS build. The entries Node matches in its [package exports](/docs/compatibility#module-format-and-typescript) only list `import`, which makes `require('envapt')` throw `ERR_PACKAGE_PATH_NOT_EXPORTED`. | v8 | v9 | | --- | --- | | `const { Envapter } = require('envapt')` | `const { Envapter } = await import('envapt')` | | `node -r envapt/config app.js` | `node --import envapt/config app.js` | CommonJS has no top-level `await`, so put the `import()` inside the async function that starts your app. ## Behavior changes Most of the code these sections describe still compiles in v9, so search your project for each one. ### Required reads throw `InvalidEnvValue` In v8, a [required read](/docs/required-values) threw `MissingEnvValue` (305) both for an unset key and for a value it couldn't convert. A `catch` had no way to tell a missing `PORT` from a mistyped one. v9 keeps 305 for the unset key and throws the new [`InvalidEnvValue`](/docs/errors#converter-and-schema) (210) for a value that's set but bad. That covers `getRequired`, `getRequiredAll`, and `@Envapt` with `required: true`. ```ts // with PORT="80a" try { Envapter.getRequired('PORT', Converters.Port); } catch (error) { if (error instanceof EnvaptError && error.code === EnvaptErrorCodes.InvalidEnvValue) { console.error(error.issues?.map((issue) => issue.path)); } } ``` That prints `[['PORT']]`. Each failure is an entry in `issues`, with the env key first in its `path`. `getRequiredAll` throws one `InvalidEnvValue` holding every bad value. v8 stopped at the first one. If a key is also unset, `getRequiredAll` throws `MissingEnvValue` for the unset keys first. With the decorator, v8 didn't throw at all: ```ts class Config extends Envapter { @Envapt('PORT', { converter: Converters.Port, required: true }) static accessor port: number; } ``` For `PORT="80a"`, v8 gave `Config.port` the value `undefined`, even though the field is typed `number`. v9 throws `InvalidEnvValue` the first time you read it. A required read also turns its other failures into issues on the `InvalidEnvValue` it throws. A bad array element used to throw `ArrayElementConversionFailed` or `EmptyArrayElement`. An error from your own parser reached your `catch` unchanged. A schema on a required decorator threw `SchemaValidationFailed` or `SchemaThrew`. Now each becomes an issue that carries the original message, and an error your parser or schema threw stays on that issue as `cause`. A `catch` on your parser's error class stops matching, so check for `InvalidEnvValue` there and read `error.issues`. ### A bad converter throws earlier, with a different code A [converter token](/docs/converters#built-in-tokens) with a typo, like `'nmber'`, now throws `InvalidBuiltInConverter` (202) and lists the valid tokens. v8 threw `InvalidCustomConverter` (203). A mistyped token also throws on every read now. v8 skipped the check for an unset key with no fallback, so `getUsing('CACHE_TTL', 'nmber')` returned `undefined` for as long as `CACHE_TTL` stayed unset. A required read throws that error before it checks whether the key is set. These fallback checks now run on every read: - A malformed time-string fallback, like `'10 seconds'` on `Converters.Time`, throws `MalformedTimeFallback` (105) even when the variable is set. v8 threw only when it used the fallback. - `get` checks that its fallback is a string, which v8 never did. `get('PORT', 80)` throws `FallbackConverterTypeMismatch` (104). TypeScript rejects a mistyped token. It also rejects a fallback of the wrong type on `get`, and on `getAs` with a token. This section mostly matters to JavaScript callers and to code that reads `error.code`. ### A schema read with no fallback returns `undefined` `Envapter.parse` threw `MissingEnvValue` for an unset key when you passed no fallback. Its replacement, `getAs`, returns `undefined` for that key, like every other optional read. A field declared with `@Envapt('PORT', { schema })` reads as `undefined` too. If you relied on the throw, move the read to `getRequired`, which now takes a [Standard Schema](/docs/standard-schema): ```ts const port = Envapter.getRequired('PORT', z.coerce.number().int().min(1024)); ``` `port` is a `number`, and an unset `PORT` throws `MissingEnvValue`. On a decorator, add `required: true`. A schema field with neither a fallback nor `required: true` needs a `T | undefined` type. If such a field is still typed `T`, TypeScript reports an error. ### Schema issues carry the env key Each issue on a `SchemaValidationFailed` from `getAs` or `@Envapt({ schema })` now has the env key first in its `path`, the same as on `InvalidEnvValue`. A zod issue on `PORT` had the path `[]` in v8. It's `['PORT']` now. The message lists only the key, as in `Validation failed for PORT.`, because an issue message can quote the value and the value can be a secret. v8 added the first issue's message after the key. If you parsed the reason from the message, read `error.issues` now. ### Errors from a custom file source This only matters if you wrote your own [source](/docs/sources#any-object-can-be-a-source) with `supportsFiles: true`. In v8, an error thrown by its `readFile` was caught while the `.env` files loaded. envapt then skipped that file and every file after it in the cascade. In v9 envapt doesn't catch it. `Envapter.useSource` throws it, and any later reload of the `.env` files throws it too. ## `Envapter.environment` is a string [`Envapter.environment`](/docs/environment) returns a string like `'production'` in v9. v8 returned a number, like `2` for production. `Environment` is now a plain object of strings, with `Environment.Production` holding `'production'`, so a comparison like `Envapter.environment === Environment.Production` keeps working. Code that turned the number back into a name breaks: ```ts // v8 console.log(Environment[Envapter.environment]); // 'Production' // v9 console.log(Environment[Envapter.environment]); // undefined console.log(Envapter.environment); // 'production' ``` TypeScript reports the v9 `Environment[...]` line as an error. In JavaScript it logs `undefined`. Setting `Envapter.environment` to a number throws `InvalidUserDefinedConfig`. Rewrite a type annotation like `Environment.Test` as `'test'`. ## Renames and removals TypeScript reports each of these in v9, so you can work through them from the compiler output. | v8 | v9 | | --- | --- | | [`Envapter.getUsing(key, token, fallback?)`](/docs/envapter#other-types-with-getas) | [`Envapter.getAs(key, token, fallback?)`](/docs/envapter#other-types-with-getas) | | `Envapter.getWith(key, fn, fallback?)` | `Envapter.getAs(key, fn, fallback?)`, with the [new converter shape](#custom-converters-get-a-string) | | `Envapter.parse(key, schema, fallback?)` | `Envapter.getAs(key, schema, fallback?)` | | `Envapter.getNumber(key, fallback?)` | `Envapter.getAs(key, Converters.Number, fallback?)`, or `Converters.Port` and `Converters.Integer` for a port or a count | | `Envapter.getBoolean(key, fallback?)` | `Envapter.getAs(key, Converters.Boolean, fallback?)` | | `Envapter.getBigInt(key, fallback?)` | `Envapter.getAs(key, Converters.BigInt, fallback?)` | | `@EnvStr(key, fallback?)`, from `envapt` or `envapt/legacy` | [`@Envapt(key, { fallback })`](/docs/decorators) from the same entry | | `@EnvNum`, `@EnvBool`, `@EnvTime`, `@EnvUrl`, from `envapt` or `envapt/legacy` | `@Envapt(key, { converter, fallback })` with `Converters.Number`, `Boolean`, `Time`, or `Url` | | `Converters.Bigint` | [`Converters.BigInt`](/docs/converters#built-in-tokens) | | `Converters.Regexp` | `Converters.RegExp` | | `String`, `Number`, `Boolean`, `BigInt` as a converter | `Converters.String`, `Converters.Number`, `Converters.Boolean`, `Converters.BigInt` | | `Envapter.getSymbol(key, fallback?)`, `Converters.Symbol`, a symbol `fallback` on `@Envapt` | `Symbol.for(Envapter.get(key, 'app'))`, with a string fallback | | `Envapter.resetProfiles()` | [`Envapter.resetPaths()`](/docs/env-files#profiles) | | `Envapter.load()` | remove the call | | an instance member, like `new Envapter().get(key)` or `config.isProduction` | the static member, like `Envapter.get(key)` or `Envapter.isProduction` | | `this.get(key)` in a subclass's instance method | `Config.get(key)`, called on your subclass | | `EnvaptOptions` | pass the options object to [`@Envapt`](/docs/decorators) inline | | `CustomElementConverter` | `ConverterFunction` | | `ConverterFunction` | `ConverterFunction` | | `EnvaptErrorCodes.MissingEnvValue` as a type | `typeof EnvaptErrorCodes.MissingEnvValue`, or `305` | | `getUsing(key, Converters.Json)` typed as `T` | `getAs(key, Converters.Json)` typed as `T \| undefined`, or pass a fallback to get `T` | Without TypeScript, some of these throw an envapt error at runtime: - `String`, `Number`, `Boolean`, or `BigInt` as a converter throws `InvalidUserDefinedConfig` (302), with a message pointing to the matching `Converters` token. - A symbol fallback throws `FallbackConverterTypeMismatch` (104). - `Converters.Bigint`, `Converters.Regexp`, and `Converters.Symbol` are `undefined` now. A reader given one throws `InvalidCustomConverter` (203). On `@Envapt`, `converter: undefined` counts as no converter, so the field reads the raw string with no error. `getAs` accepts a token, a function, or a schema. `Converters.BigInt` and `Converters.RegExp` are now spelled like the JavaScript globals. On Node, envapt binds `FileSource` when you import it, and binding a source loads the `.env` files, so `load()` already did nothing in v8. `resetProfiles()` became `resetPaths()` because it clears `envPaths` along with the profiles. [`EnvaptErrorCodes`](/docs/errors#codes) is a plain object now, like `Environment`, so `EnvaptErrorCodes[305]` returns `undefined`. v9 removed `InvalidFallbackType` (102), `InvalidConverterType` (204), and `PrimitiveCoercionFailed` (205). Every other code kept its number. ## Custom converters get a string A custom converter passed to `getWith` or `@Envapt` ran on every read in v8. It got `raw` as `string | undefined` and the fallback as a second argument, so each one opened with a check for a missing value. In v9 envapt calls it only when the variable is set, with `raw` as a `string`, and applies the fallback itself. ```ts // v8 const hosts = Envapter.getWith('ALLOWED_HOSTS', (raw, fallback) => (raw ? raw.split(',') : fallback), ['localhost']); ``` ```ts const hosts = Envapter.getAs('ALLOWED_HOSTS', (raw) => raw.split(','), ['localhost']); ``` `hosts` is a `string[]`. It's `['localhost']` when `ALLOWED_HOSTS` is unset, and the converter never runs. If your converter returns `undefined` or `null`, envapt returns the fallback, so a custom converter can't return `null` as a real result anymore. A decorated field with a custom converter and no fallback now needs a `T | undefined` type, because an unset variable gives it `undefined` without calling your function. In [`Converters.array`](/docs/converters#arrays), envapt throws `ArrayElementConversionFailed` when an element converter returns `null`. v8 kept the `null` in the array. ## Schemas go in `schema` An arktype type is a function, and it's a Standard Schema too. v8 accepted one as the `converter` on `@Envapt`, or as the `of` in `Converters.array`. envapt called it like any converter and returned arktype's `ArkErrors` object as the value whenever validation failed. v9 rejects a Standard Schema in both places. Passing one fails to compile. At runtime the decorator throws `InvalidUserDefinedConfig`, and the array throws `InvalidArrayConverterType`. Pass the schema to `@Envapt` as `schema`: ```ts import { type } from 'arktype'; import { Envapt, Envapter } from 'envapt'; class Config extends Envapter { @Envapt('PORT', { schema: type('string.integer.parse'), fallback: 3000 }) static accessor port: number; } ``` v8 accepted only an object as `schema`, and an arktype type is a function, so that line threw `InvalidUserDefinedConfig` in v8. ## Also in v9 These don't break existing code: - `getRequired` and `getRequiredAll` take a synchronous Standard Schema. - Each argument to [`Envapter.require`](/docs/required-values#getrequired-require-and-has) can be an ordered key list, which passes when any key in it has a value. Each placeholder in [`Envapter.resolve`](/docs/templates#the-resolve-tagged-template) takes one too and uses the first key with a value. - [`ENVIRONMENT_KEYS`](/docs/environment#detecting-the-environment) is exported. It lists the variables envapt checks to detect the environment, in order, for tools that need to read the same ones. - If a zod `.transform` or `.refine` threw during a read, Node crashed with an unhandled rejection. It no longer does, and with `Envapter.debug = 'warn'`, envapt logs the original error. - `getRequired`, `getRequiredAll`, and a required decorated field return `null` for a `Converters.Json` value of `null`, the same as `getAs`. v8 threw `MissingEnvValue`. --- # Migrations Find the breaking changes in each envapt major from v5 to v9, and the edits each asks of your code. The newest release is first. Find the page that starts at your current version, then work up the list to v9. - **[v8 to v9](/docs/migration-v8-to-v9).** envapt is ESM only and requires Node 22.23.3 or newer. `getUsing`, `getWith`, and `parse` become `getAs`, and required reads throw `InvalidEnvValue` for a value they can't use. - **[v7 to v8](/docs/migration-v7-to-v8).** The source classes get shorter names, and one universal `envapt` import replaces the `workerd` and `browser` subpaths. The portable file APIs warn once and do nothing by default. - **[v6 to v7](/docs/migration-v6-to-v7).** The default decorators become TC39 Stage 3 accessors, and the legacy decorators move to `envapt/legacy`. - **[v5 to v6](/docs/migration-v5-to-v6).** The positional decorator form is removed, and `NODE_ENV=test` now resolves to the test environment. - **[v4 to v5](/docs/migration-v4-to-v5).** `Converters.array()` replaces `Converters.Array`, `dotenvConfig` becomes `envFileOptions`, and per-environment `.env` files load on their own. --- # Structuring your config Read every variable once, in one typed config module that throws at startup. You've read `process.env.DATABASE_URL` in whichever file needed it. That went wrong in three ways: - Each file parsed the string its own way and picked its own default. - A missing variable crashed the first request that touched it. - A test ran against whatever your laptop's `.env.local` held. A config module puts every read in one file that runs when the app starts. [`Envapter`](/docs/envapter) parses each value there with a [converter](/docs/converters), and [`getRequiredAll`](/docs/required-values) checks every required key before it throws. The other files import the typed result. ## One module reads your variables ```ts title="src/config.ts" import { Converters, Envapter } from 'envapt'; const env = Envapter.getRequiredAll( { DATABASE_URL: Converters.Url, PORT: Converters.Port, SESSION_SECRET: Converters.String }, 'camelCase' ); export const config = { ...env, cacheTtl: Envapter.getAs('CACHE_TTL', Converters.Time, '5m'), allowedOrigins: Envapter.getAs('ALLOWED_ORIGINS', Converters.array(), []), isProduction: Envapter.isProduction }; export type Config = typeof config; ``` `Config` shows what the rest of the app gets. `port` is a `number`, `databaseUrl` is a `URL`, and `cacheTtl` is a number of milliseconds, so no other file parses them again. The `'camelCase'` argument turns `DATABASE_URL` into `databaseUrl`, and without it the keys stay as you wrote them. `getRequiredAll` throws one error that lists every bad key: - `MissingEnvValue` lists the required keys that are unset. - `InvalidEnvValue` lists the keys that are set but can't convert, such as `PORT=abc`. `cacheTtl` and `allowedOrigins` take a fallback, the value the read returns when the variable is unset. A fallback also replaces a value that can't convert, without an error, so `CACHE_TTL=5 minutes` reads as the `'5m'` fallback. Give a variable a fallback when the app has a sensible value to run on without it. If a typo in it should stop the app, list it in `getRequiredAll` instead. `ALLOWED_ORIGINS` splits on commas, and [`isProduction`](/docs/environment#branching-on-the-environment) reads `ENVIRONMENT`, `ENV`, `NODE_ENV`, or `MODE`. The module has a cost. Any script, test, or tool that imports `config.ts` needs every required variable set first. A script that reads one or two values can call `Envapter` directly and skip the module. ## Import it from your entry file ```ts title="src/index.ts" import { config } from './config'; import { createServer } from './server'; createServer().listen(config.port); ``` `config.ts` runs before any module that imports it. Imports run in order, so listing `./config` first also covers a module that does work when it loads without importing `config`, such as one that opens a connection pool. A missing `SESSION_SECRET` then throws before that module runs, and the process exits before anything opens a port. > **WARNING** > > If a value is set but can't convert, the uncaught error also prints its `issues`, and an issue can quote the bad value. [Reading the error without leaking values](/docs/required-values#reading-the-error-without-leaking-values) has the `catch` block that logs only the keys. ## Import `config` in every other file ```ts title="src/db.ts" import { config } from './config'; export function connectionOptions() { return { connectionString: config.databaseUrl.href, ssl: config.isProduction }; } ``` `config.databaseUrl` arrives as a `URL`, with no parsing and no default in `db.ts`. Nothing stops a file from reading `process.env` or calling `Envapter.get` itself, so this is a habit to keep. A file that reads on its own parses the string again and picks its own default, and the two can drift apart. To read a different variable later, you change `config.ts`, then set the new name in each environment and in your test setup. ## Grouping settings Once several keys share a prefix, such as `DATABASE_` and `REDIS_`, nested objects read better than one long list. Keep one `getRequiredAll` call, and group its keys when you build `config`: ```ts title="src/config.ts" const env = Envapter.getRequiredAll( { DATABASE_URL: Converters.Url, DATABASE_POOL_SIZE: Converters.Integer, REDIS_URL: Converters.Url, PORT: Converters.Port }, 'camelCase' ); export const config = { db: { url: env.databaseUrl, poolSize: env.databasePoolSize }, redis: { url: env.redisUrl }, http: { port: env.port } }; ``` Two `getRequiredAll` calls, one per group, throw separately. If `DATABASE_URL` and `REDIS_URL` are both missing, the first call throws, its error lists only `DATABASE_URL`, and you find `REDIS_URL` on the next deploy. ## A config class instead If your code is written around classes, declare each setting as a typed field with the [`@Envapt` decorator](/docs/decorators): ```ts title="src/Config.ts" export class Config { @Envapt('DATABASE_URL', { converter: Converters.Url, required: true }) static accessor databaseUrl: URL; @Envapt('CACHE_TTL', { converter: Converters.Time, fallback: '5m' }) static accessor cacheTtl: number; } ``` Each field needs the `accessor` keyword, which [Decorators](/docs/decorators#declaring-decorated-fields) explains. If your project sets `experimentalDecorators`, import from `envapt/legacy` and declare `static readonly` fields instead. The class differs from the module in two ways: - **When it reads:** a decorated field reads its variable the first time something accesses it. A missing `DATABASE_URL` then throws during the first request that touches `Config.databaseUrl`, which brings back the crash from the top of this page, and missing fields show up one at a time. The `getRequiredAll` module checks every required key when the app starts. - **After a new source binds:** if something binds a new source after startup, such as a secrets refresh, a field reads its variable again on its next access. `config` keeps the values it read at load. Start with the module. Pick the class if a field has to re-read after a new source binds. ## Tests `config.ts` reads its variables when a test file first imports it, so the test values have to be bound before that. Bind them in a setup file: ```ts title="tests/setup.ts" import { Envapter, PortableSource } from 'envapt'; Envapter.useSource( new PortableSource({ ENVIRONMENT: 'test', DATABASE_URL: 'postgres://app@localhost:5432/app', PORT: '4000', SESSION_SECRET: 'test-secret' }) ); ``` ```ts title="vitest.config.ts" import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { setupFiles: ['./tests/setup.ts'] } }); ``` vitest runs each file in `setupFiles` before it imports a test file. [`PortableSource`](/docs/sources#the-sources-envapt-ships) replaces the default source, so your shell and `.env` files stay out of the test. `PORT: '4000'` is a string, the same as a real environment variable, and `config.port` still comes out as the number `4000`. To change one value in a single test, see [Testing](/docs/testing#reads-at-module-scope-run-before-the-test). ## On Workers and in the browser On these runtimes envapt reads nothing until you bind the object the platform gives you, so the bind goes at the top of `config.ts`, before the first read: ```ts title="src/config.ts" import { env as workerEnv } from 'cloudflare:workers'; import { Converters, Envapter, PortableSource } from 'envapt'; Envapter.useSource(new PortableSource(workerEnv)); export const config = Envapter.getRequiredAll( { API_BASE_URL: Converters.Url, API_TOKEN: Converters.String }, 'camelCase' ); ``` A module that reads a value before the `useSource` line runs throws [`NoSourceBound`](/docs/sources#no-environment-source-is-bound), which is one more reason every file reads through `config`. On Workers, Cloudflare runs this module when you deploy, so a missing `API_TOKEN` fails the deploy. [Cloudflare Workers](/docs/workers) covers that and the per-environment `vars`. In the browser the bind takes Vite's `import.meta.env` ([Browser](/docs/browser)), and other edge runtimes follow the [edge runtimes](/docs/edge-runtimes) page. ## Different values per environment The keys stay the same in every environment, and the values come from wherever the source reads: - On Node, Bun, and Deno, [`.env.production`](/docs/env-files#which-files-load) loads in production on top of `.env`, and a variable set in the shell wins over both. - Secrets can come from a [secrets manager](/docs/secret-stores), fetched at boot and bound with your `.env` files. - On Workers, each Wrangler environment sets its own `vars` and secrets. --- # Browser Bind Vite's import.meta.env to envapt once, then read each VITE_ value as the type you need. A browser has no `process.env`. When Vite builds the bundle, it replaces `import.meta.env` with an object holding your variables, and every `VITE_` value in it is a string. Read directly, `VITE_ENABLE_BETA` holds `'false'` when the flag is off. That string is truthy, so an `if` on it runs the wrong branch. This page uses Vite. Another bundler hands you its own injected object, which you bind the same way. If you bind `import.meta.env` to envapt, envapt converts each value you read. Bind it before the first read, because a read with no [source](/docs/sources) bound throws `NoSourceBound`. ## Bind in a config module Put the bind and the reads in one `config.ts`, and read every value through `config`. A module that calls [`Envapter.get`](/docs/envapter) directly can run before the bind. ```ts title="src/config.ts" Envapter.useSource(new PortableSource(import.meta.env)); export const config = Envapter.getRequiredAll( { VITE_API_URL: Converters.Url, VITE_ENABLE_BETA: Converters.Boolean }, 'camelCase' ); config.viteEnableBeta; ``` The `'camelCase'` argument turns `VITE_ENABLE_BETA` into `config.viteEnableBeta`, a `boolean`, and `VITE_API_URL` into `config.viteApiUrl`, a `URL`. [`PortableSource`](/docs/sources#the-sources-envapt-ships) takes any object, so `import.meta.env` type-checks with the `ImportMetaEnv` interface above or with Vite's own `vite/client` types. Binding the whole object puts every `VITE_` variable in the bundle, as [Which variables reach the bundle](#which-variables-reach-the-bundle) explains. If a key is missing, `getRequiredAll` throws when the module loads and lists every missing key (see [Required values](/docs/required-values)). ## Which variables reach the bundle Vite exposes variables whose names start with `VITE_`, a prefix its `envPrefix` option sets, plus `MODE`, `BASE_URL`, `PROD`, `DEV`, and `SSR`. If your `.env` file holds `VITE_API_URL` and `API_SECRET`, only `VITE_API_URL` reaches the bundle. `Envapter.get('API_SECRET')` returns `undefined` in the browser, so a variable you forgot to prefix looks the same as one you forgot to set. `PROD`, `DEV`, and `SSR` are booleans. `PortableSource` stringifies them, so `Envapter.getAs('DEV', Converters.Boolean, false)` works. > **WARNING** > > Whatever reaches `import.meta.env` ships in your client bundle, so anyone can read it. Keep secrets and server-side keys out of `VITE_` variables. If you bind the whole `import.meta.env`, the bundle includes every `VITE_` variable in your `.env` files, including ones no code reads. Vite replaces a named read such as `import.meta.env.VITE_API_URL` with its literal value, so the bundle includes only that value. To put only the variables you read in the bundle, list them by name. ```ts title="src/config.ts" Envapter.useSource( new PortableSource({ MODE: import.meta.env.MODE, VITE_API_URL: import.meta.env.VITE_API_URL, VITE_ENABLE_BETA: import.meta.env.VITE_ENABLE_BETA }) ); export const apiUrl = Envapter.getRequired('VITE_API_URL', Converters.Url); ``` The object lists `MODE` because a source holds only the keys you give it, and [`Envapter.environment`](/docs/environment) reads `MODE`. ## Environment `Envapter.environment` and helpers like `Envapter.isProduction` read `MODE`, the key envapt checks last among the [detection keys](/docs/environment). `vite build` sets `MODE` to `production`, and `vite build --mode staging` makes `Envapter.environment` `'staging'`. If the mode is one envapt doesn't recognize, `Envapter.environment` is `'development'`. `Envapter.environment` can disagree with Vite's own flags. `vite build --mode staging` leaves `import.meta.env.PROD` true, while `Envapter.isProduction` is false and `Envapter.isStaging` is true. Use the envapt helpers when you branch on staging. ## Decorators The default `@Envapt` accessor decorators need your toolchain to compile them, and Vite 7 and Vite 8 differ here. - **Vite 7** compiles them when the `target` in your `tsconfig.json` is ES2020 or lower, or when there is no tsconfig. With ES2022 or newer, `vite build` stops with a parse error until you set the esbuild target, as the config below does. - **Vite 8** outputs them uncompiled even if you set `esbuild.target`, `build.target`, or `oxc.target`. Set `experimentalDecorators: true` in `tsconfig.json` and import from `envapt/legacy`, or read through `Envapter` without decorators. ```ts title="vite.config.ts" export default defineConfig({ esbuild: { target: 'es2022' } }); ``` The `esbuild.target` line is the whole fix on Vite 7. [Decorators](/docs/decorators) shows the field forms, and [Compatibility](/docs/compatibility#decorators-per-runtime) lists the legacy ones. --- # Building on envapt Read config from a library or CLI that depends on envapt, using what the app already set up. Your package reads a variable or two, and the app that installs it decides where config comes from. The app may bind a source with `useSource`, pin the environment, or point envapt at another folder for its `.env` files, and it often does that after it imports your package. seedcord, a Discord framework built on envapt, follows each pattern here. ## Declare envapt as a peer dependency envapt keeps its source, its environment, and its cache in module state. Each installed copy keeps its own. If your package installs a second copy, the app's `useSource` call binds the app's copy, and your reads go through the other one. List envapt under `peerDependencies`, and the package manager installs one copy for both: ```json title="package.json" { "peerDependencies": { "envapt": "^9.0.0" } } ``` ## Read inside your functions A read at the top of a module runs when the app imports your package, which is before the app's own setup code. Put the read inside the function that needs it: ```ts title="src/client.ts" export function apiBaseUrl(): string { return Envapter.get('ACME_API_URL', 'https://api.acme.dev'); } ``` If the app binds a [`PortableSource`](/docs/sources#the-sources-envapt-ships) after importing your package, `apiBaseUrl()` reads from it. The same read at module scope ran first, against the default source, and keeps that answer. Branch on the environment the same way. A check like `Envapter.isProduction` inside a function reads the environment the app pinned, while a module-scope constant holds the one detected at import. ## Throw your own error for a missing key `getRequired` throws envapt's `MissingEnvValue` error. If your package documents its own error codes, check the key with `has` first, so your users get your error: ```ts title="src/token.ts" export function apiKey(): string { if (!Envapter.has('ACME_API_KEY')) throw new AcmeError('MissingApiKey'); return Envapter.getRequired('ACME_API_KEY', Converters.String); } ``` `has` and `getRequired` decide whether a key is present the same way, so `has` returns `false` in exactly the cases where `getRequired` would throw `MissingEnvValue`. ## Set the environment before you load the app A CLI that starts the user's app often picks the mode itself, such as `production` for a build. Assign [`Envapter.environment`](/docs/environment#pinning-the-environment) before you import the user's code: ```ts title="src/cli.ts" Envapter.environment = 'production'; await import(userEntry); ``` The assignment reloads the `.env` files on Node, so `.env.production` loads even though envapt read the files for `development` when your CLI imported it. Modules the user's entry imports then read the production values. ## Bind a source for one dev session A dev command runs in Node, and sometimes it needs values that the user's code gets from a file envapt doesn't read, like a platform's local vars file. Layer those values over [`FileSource`](/docs/sources) with [`merge`](/docs/sources#combining-sources). Then put the call that undoes it on the [`AsyncDisposableStack`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack) that closes the rest of the session: ```ts title="src/dev.ts" export async function bindDevVars(stack: AsyncDisposableStack): Promise { const vars = await readDevVars(); Envapter.useSource(merge(new FileSource(), new PortableSource(vars))); stack.defer(() => Envapter.useSource(new FileSource())); } ``` `vars` comes second in `merge`, so a key from that file wins over the same key from `FileSource`. The `defer` line binds a fresh `FileSource` when the stack is disposed. That's the same source the node build binds on import. envapt doesn't expose the current source, so you restore it by binding the default again. The stack runs that line when the session stops. It also runs it if a later startup step throws and your code disposes the stack early. If your tool restarts the session in the same process, or runs more code after it ends, every later read goes through the default source again. Restoring the default fits a process your tool starts, like a CLI. If the app bound its own source before your code runs, the `defer` line replaces that source with the default. ## Set defaults the app can override If your package already has the path to the app's `.env` files, set `Envapter.baseDir` when your entry module loads: ```ts title="src/index.ts" Envapter.baseDir = projectRoot; ``` The app imports your package before its own code runs, so a `baseDir` the app assigns later replaces yours. [Reading from a fixed directory](/docs/env-files#reading-from-a-fixed-directory) shows what `baseDir` changes. ## Write the bind into generated entries On Workers and edge runtimes, envapt reads nothing until something calls `useSource`. If your build step writes the app's entry file, generate the bind as a module of its own: ```ts title="src/workerEntry.ts" export const BIND_ENV = `import { env } from 'cloudflare:workers'; import { Envapter, PortableSource } from 'envapt'; Envapter.useSource(new PortableSource(env)); `; ``` Import that module first in the entry you generate, above the import of the app. JavaScript runs every import before the statements in the same file, so a `useSource` line written into the entry itself would run after the app's modules have loaded and read. With the bind imported first, the app's code reads config with no setup of its own, the same as on Node. [Cloudflare Workers](/docs/workers) explains the bind. ## Testing your package Your tests bind a `PortableSource` with the values each test needs, the same as an app's tests. If your test runner shares modules between files, envapt's state carries over too. [Testing](/docs/testing#with-isolate-turned-off) has the vitest setup for that. --- # Compatibility Look up the Node, Bun, and Deno versions envapt needs, and the TypeScript settings to import it. envapt is ESM only and has [two builds](/docs/sources). The node build imports `node:*` modules, binds a [`FileSource`](/docs/sources#the-sources-envapt-ships) when you import it, and reads `.env` files. The portable build doesn't import from `node:`, so it runs on Workers and in the browser, but it binds nothing until you call [`useSource`](/docs/sources). ## Engine versions | Runtime | Minimum | Tested in CI | | --- | --- | --- | | Node | `22.23.3` | 22, 24, and 26 on Linux. 22 on macOS and Windows | | Bun | `1.3.11` | 1.4.x | | Deno | `2.5` | 2.9.x | | Cloudflare Workers | none set | workerd | | Browser | none set | Chromium | ## Install [Quick Start](/docs/quick-start) has the command for npm, pnpm, yarn, and bun. Deno can install from either registry. | Registry | Deno command | Entries | | --- | --- | --- | | npm | `deno add npm:envapt` | `envapt`, `envapt/legacy`, `envapt/config` | | JSR | `deno add jsr:@materwelon/envapt` | `@materwelon/envapt`, `@materwelon/envapt/legacy` | JSR doesn't have export conditions, and its `@materwelon/envapt` entry is the node build. It also doesn't have the `config` entry. For Workers or the browser, install from npm so the conditions can select the portable build. ## Binding by runtime The `exports` conditions in `package.json` select the build, so you import from `envapt` on every npm install. ### Node build, binds automatically The node build's `FileSource` reads `process.env` plus the [`.env` cascade](/docs/env-files), so the first read works with no setup. | Runtime | Condition | Reads | Note | | --- | --- | --- | --- | | Node and Bun | `node` | `process.env` and the `.env` cascade | | | Deno | `deno` | `process.env` and the `.env` cascade | | | Deno Deploy | `deno` | dashboard variables, which Deno also puts on `process.env` | no `.env` files on disk | ### Portable build, you bind a source Before the first read, bind a source built from the object or function the platform gives you. If you read before then, envapt throws [`NoSourceBound`](/docs/errors) (307), whatever `fileApiMode` is. | Runtime | Condition | Config comes from | Note | | --- | --- | --- | --- | | [Cloudflare Workers](/docs/workers) | `workerd` | the `env` object | bind once at module load | | [Vercel Edge](/docs/edge-runtimes) | `edge-light` | `process.env`, which holds your variables at runtime there | Vercel's Node.js runtime uses the node build | | [Fastly Compute](/docs/edge-runtimes) | `fastly` | `env()`, one key at a time | pass `env` to `useSource`, and add `fastly` to your bundler's conditions | | [Expo and React Native](/docs/edge-runtimes) | `react-native` | `process.env.EXPO_PUBLIC_*` references Expo replaces at build time, or `Constants.expoConfig.extra` | at runtime `process.env` holds only `NODE_ENV`. Metro must have package exports on, which it does by default from Metro 0.82 | | [Browser](/docs/browser) | `browser` | the object your bundler injects, such as `import.meta.env` | the values ship in the client bundle, so bind only public ones | | Web workers | `worker` | an object you pass to `new PortableSource(...)` | no `process.env` to read | ### Which condition selects which build | Conditions your tool sets | Build | | --- | --- | | any of `workerd`, `edge-light`, `fastly`, `worker`, `browser`, `react-native` | portable | | `deno`, `node`, or none of the above | node | - The portable conditions come first in the `exports` map, so the first one your tool sets wins. If your bundler sets both `browser` and `node`, you get the portable build. - Only the node build exports `FileSource`. Under a portable condition, importing `FileSource` from `envapt` fails. - `envapt/legacy` follows the same conditions. `envapt/config` exists only in the node build, so use it on Node, Bun, and Deno. ### File APIs and `fileApiMode` [`Envapter`](/docs/envapter)'s `envPaths`, `baseDir`, `envFileOptions`, `configureProfiles`, and `resetPaths` read the filesystem ([.env files on Node](/docs/env-files)). On the portable build they have nothing to read. `Envapter.fileApiMode` takes a `FileApiMode` and sets what each call does there. | `fileApiMode` | On the portable build | On the node build | | --- | --- | --- | | `'warn'` (default) | each API warns once and does nothing. The `envPaths`, `baseDir`, and `envFileOptions` getters return `[]`, `undefined`, and `{}` | runs normally | | `'throw'` | throws [`FileApiUnsupported`](/docs/errors) (306) | runs normally | Keep `'warn'` when one config module loads on both builds. With `'warn'`, setting `envPaths` does nothing on Workers and still loads files on Node. Choose `'throw'` if you want a file API call on the portable build to stop the app. The error message says which API you called. ```ts Envapter.fileApiMode = 'throw'; ``` On the node build this line has no effect. If you bind a [`PortableSource`](/docs/sources#the-sources-envapt-ships) on Node, the source has no files. Then setting `envPaths` or `baseDir`, or calling `configureProfiles`, throws `FileApiUnsupported` in either mode. ## Module format and TypeScript `require('envapt')` throws `ERR_PACKAGE_PATH_NOT_EXPORTED`, because the entries Node matches (`node` and `default`) are import-only. In a CommonJS project, load the package with `await import('envapt')` inside an async function. Load the config entry with `node --import envapt/config`. ```ts export async function main(): Promise { const { Converters, Envapter } = await import('envapt'); return Envapter.getAs('PORT', Converters.Port, 3000); } ``` `main` loads the package at call time, so a CommonJS file can reach envapt without `require`. ### ERR_PACKAGE_PATH_NOT_EXPORTED from compiled code With `"module": "commonjs"`, `tsc` rewrites `import('envapt')` into `require('envapt')`. The compiled file then throws the error you were avoiding. `"module": "node16"` or `"nodenext"` keeps the `import()` call, so the same sample runs. ### Cannot find module 'envapt' or its corresponding type declarations TypeScript finds `envapt` through the `exports` map only under some module settings. These combinations were tested with TypeScript 6.0.3: | Compiler options | `envapt` | `envapt/legacy` | | --- | --- | --- | | `module: "nodenext"` in a package with `"type": "module"` | resolves | resolves | | `module: "esnext"` or `"preserve"` with `moduleResolution: "bundler"` | resolves | resolves | | `module: "node16"` or `"nodenext"` in a CommonJS package | fails on `import`, resolves on `import()` | fails on `import`, resolves on `import()` | | `module: "commonjs"` with `moduleResolution: "bundler"` | fails | fails | | `moduleResolution: "node10"`, which needs `ignoreDeprecations: "6.0"` in TypeScript 6 | resolves through the top-level `types` field | fails, because `node10` ignores the `exports` map | Use `"type": "module"` with `module: "nodenext"` if you run the compiled output on Node, since `nodenext` in a CommonJS package rejects a static `import` of envapt. Use `moduleResolution: "bundler"` if a bundler builds your app. ## Decorators per runtime `@Envapt` from `envapt` is a TC39 Stage 3 accessor decorator, and with TypeScript 5.0 or later it needs no compiler flag. The older form in `envapt/legacy` needs `experimentalDecorators`. The read methods like `Envapter.getAs` need neither. [Decorators](/docs/decorators) explains both forms. | Where the code runs | Modern `@Envapt` | Legacy `@Envapt` | | --- | --- | --- | | Node | compile first, because `node app.ts` fails with a `SyntaxError` | compile first, with `experimentalDecorators` in `tsconfig.json` | | Bun | runs a `.ts` file directly, `bun test` included | `experimentalDecorators` in `tsconfig.json` | | Deno | runs a `.ts` file directly | `experimentalDecorators` in `deno.json` | | vitest | fails with the same `SyntaxError`, see [Testing](/docs/testing#syntaxerror-invalid-or-unexpected-token) | loads with `experimentalDecorators` on | | Workers, browser, edge | your bundler has to compile them. Wrangler does by default, see [Workers](/docs/workers#decorators) and [Browser](/docs/browser#decorators) | your bundler, with `experimentalDecorators` on | ```json title="tsconfig.json or deno.json" { "compilerOptions": { "experimentalDecorators": true } } ``` The Bun minimum is 1.3.11 because earlier versions emit Stage 3 decorators even with `experimentalDecorators` set, which makes a legacy `@Envapt` field throw a `TypeError` ([bun#27575](https://github.com/oven-sh/bun/issues/27575)). --- # Converters Turn an environment string into a number, URL, duration, list, or your own type. `process.env.CACHE_TTL` is the string `15m`. Turning it into milliseconds took a parser you wrote yourself. A flag and a [comma-separated list](#arrays) each took one too. A converter does that step. Pass one to `getAs`, or set it as the `converter` option on the [`@Envapt`](/docs/decorators) decorator, to make the read return the converted type. Without a converter, `Envapter.get` returns the string as it is. ```ts const timeout = Envapter.getAs('REQUEST_TIMEOUT', Converters.Time, '30s'); const adminEmails = Envapter.getAs('ADMIN_EMAILS', (raw) => new Set(raw.split(','))); ``` The third argument is the fallback, which the read returns when the variable is unset or its value can't convert. An [array](#arrays) read throws on a bad element. `timeout` is a `number` of milliseconds because `Converters.Time` converts `30s`. The fallback removes `undefined` from the type. `adminEmails` is a `Set` built by a function you wrote. It has no fallback, so it can be `undefined`. On Workers, the browser, and edge runtimes, every read needs a bound [source](/docs/sources) first. ## Built-in tokens A token is one of the built-in converters on the `Converters` object. Each token in the table is a plain string underneath (`Converters.Port` is `'port'`). A mistyped string throws [`InvalidBuiltInConverter`](/docs/errors) (202) with the list of valid tokens. Use `Converters`, which gives you autocomplete and catches the typo before you run. | Token | Returns | Accepts | | --- | --- | --- | | `Converters.String` | `string` | any value, unchanged | | [`Converters.Number`](#number-integer-and-float) | `number` | what `Number(raw)` reads. `NaN` can't convert | | [`Converters.Integer`](#number-integer-and-float) | `number` | a safe integer | | [`Converters.Float`](#number-integer-and-float) | `number` | what `Number(raw)` reads, except a whitespace-only value. `NaN` can't convert | | `Converters.Boolean` | `boolean` | `1`/`yes`/`true`/`on` and `0`/`no`/`false`/`off`, in any case | | `Converters.BigInt` | `bigint` | what `BigInt(raw)` reads | | [`Converters.Json`](#json) | `JsonValue` | what `JSON.parse(raw)` reads | | [`Converters.Url`](#url) | `URL` | an absolute URL | | [`Converters.RegExp`](#regexp) | `RegExp` | `/pattern/flags`, or a whole string used as the pattern | | [`Converters.Date`](#date) | `Date` | a millisecond timestamp or a UTC ISO 8601 string | | [`Converters.Time`](#time-and-durations) | `number` | a duration like `30s`, returned as milliseconds | | `Converters.Port` | `number` | an integer from `0` to `65535` | | `Converters.Email` | `string` | an address matching the WHATWG email pattern, so `user@localhost` passes | ```ts const port = Envapter.getAs('PORT', Converters.Port, 3000); const apiUrl = Envapter.getAs('API_URL', Converters.Url); ``` `port` has a fallback, so its type is `number`. `apiUrl` has none, so it's `URL | undefined`. The fallback has to be a valid value for its token, such as a real email, an in-range port, or a non-`NaN` number. An invalid one throws [`FallbackConverterTypeMismatch`](/docs/errors) (104) on every read of that key, whether the variable is set or not. ### Number, Integer, and Float `Number` reads whatever JavaScript's `Number()` reads, including decimals, `0x` hex, and scientific notation. `Integer` accepts the same forms but requires a safe integer, so `3.9` and values larger than 2^53 - 1 can't convert. `Float` matches `Number` except for a whitespace-only value. `Number` reads it as `0`, while `Float`, `Integer`, and `Port` can't convert it, so pick `Float` for a decimal that a blank value must not turn into `0`. Read a count, a retry limit, or a pool size with `Integer`, because `Number` accepts `3.9`. ```ts // MAX_CONNECTIONS=100 const maxConnections = Envapter.getAs('MAX_CONNECTIONS', Converters.Integer); // 100 // RETRIES="8abc" is not a clean integer, so the read returns the fallback const retries = Envapter.getAs('RETRIES', Converters.Integer, 3); // 3 const sampleRate = Envapter.getAs('SAMPLE_RATE', Converters.Float, 1.0); ``` ### JSON `Json` runs `JSON.parse`. The return type is `JsonValue`, the recursive union of every JSON-representable value, so every read needs narrowing before use. `null` is valid JSON, so `FEATURE_FLAGS=null` returns `null`, and a required read returns it too. To type the result yourself, pass the type as a generic. A [schema](/docs/standard-schema) is the other route, and it also checks the shape. ```ts interface FeatureFlags { beta?: boolean; } // FEATURE_FLAGS={"beta":true} const raw = Envapter.getAs('FEATURE_FLAGS', Converters.Json, {}); const flags = Envapter.getAs('FEATURE_FLAGS', Converters.Json, {}); ``` `flags` is typed from your `FeatureFlags` interface, though envapt doesn't check that the JSON matches it. Without the `{}` fallback, the type is `FeatureFlags | undefined`, because an unset variable returns `undefined`. ### URL `Url` builds a `URL`, which needs an absolute value. A relative path makes the constructor throw, but the read catches that and returns the fallback. ### RegExp `RegExp` reads a `/pattern/flags` literal with flags from `g i m s u v y`. A value without the surrounding slashes becomes the whole pattern, with no flags. ### Date `Date` accepts two forms. Any other string can't convert. ```ts // RELEASED_AT=2024-01-15T10:30:00Z const released = Envapter.getAs('RELEASED_AT', Converters.Date); // TRIAL_ENDS_AT=1705314600000 const trialEnds = Envapter.getAs('TRIAL_ENDS_AT', Converters.Date); ``` `RELEASED_AT` uses the UTC ISO 8601 form, which must end in `Z` and may carry `.mmm` milliseconds. `TRIAL_ENDS_AT` uses the other form, an all-digit millisecond timestamp. A date-only string, a local time, and an offset like `+02:00` can't convert, so store dates in UTC. ### Time and durations `Time` reads a duration and returns **milliseconds**. The raw value is a number with an optional unit. Without a unit, the number is milliseconds. The units are `ms`, `s`, `m` (minutes), `h`, `d`, and `w` (weeks). Decimals work too, as in `1.5h`. ```ts // CACHE_TTL=15m const ttl = Envapter.getAs('CACHE_TTL', Converters.Time); // 900000 const grace = Envapter.getAs('GRACE_PERIOD', Converters.Time, '1.5h'); const retryDelay = Envapter.getAs('RETRY_DELAY', Converters.Time, 5000); ``` The fallback is a number in milliseconds, like `5000` in `retryDelay`, or a time string with a unit, like `'1.5h'`. The exported `TimeFallback` type accepts both. A time string without a unit, such as `'90'`, throws [`MalformedTimeFallback`](/docs/errors) (105), though `CACHE_TTL=90` in the variable reads as 90 milliseconds. Pass the number `90` for that. ## When a value can't convert If a token can't convert the value, the read returns the fallback, or `undefined` without one. If a custom function returns `undefined` or `null`, the read returns the fallback too. Neither case throws. Set `Envapter.debug = 'verbose'` to log which key fell back, as [Errors](/docs/errors) describes. ```ts // PORT=80a const port = Envapter.getAs('PORT', Converters.Port, 3000); // 3000 ``` `PORT=80a` isn't a port, yet the app starts on 3000. That suits a value the app can run without. For a value the app can't run without, a [required read](/docs/required-values) throws and reports the key. Some failures throw whether [strict mode](/docs/required-values#strict-mode) is on or off. Among them are a fallback that doesn't fit its token (104), a mistyped token string (202), and a failing [array element](#arrays) (206). An error thrown inside your own converter function propagates out of `getAs` unchanged. A required read reports it as an [issue](/docs/required-values#reading-the-error-without-leaking-values) on its error. ## Arrays Without `Converters.array` you split the value, trim each piece, and skip the empty ones by hand. `Converters.array` splits the value on a delimiter and converts each element. Both options have defaults, so `Converters.array()` splits on commas and returns `string[]`. | Name | Type | Default | Description | | ----------- | ----------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------- | | `of` | `token \| (raw: string) => T` | `Converters.String` | Converts each element. Any token except Converters.Json and Converters.RegExp, or a function of your own. | | `delimiter` | `string` | `','` | The text to split the value on. An empty string throws MissingDelimiter (301). | ```ts // FEATURE_TAGS=beta, checkout, dark-mode const tags = Envapter.getAs('FEATURE_TAGS', Converters.array()); // PLUGIN_DIRS=/opt/app/plugins;/usr/local/share/app/plugins const pluginDirs = Envapter.getAs('PLUGIN_DIRS', Converters.array({ delimiter: ';' })); // CORS_ORIGINS=https://a.example.com,https://b.example.com const origins = Envapter.getAs('CORS_ORIGINS', Converters.array({ of: Converters.Url })); // WORKER_PORTS=3000 3001 3002 const ports = Envapter.getAs('WORKER_PORTS', Converters.array({ of: Converters.Port, delimiter: ' ' })); ``` With strict mode off, envapt trims every element and skips the empty ones, so `FEATURE_TAGS` above yields three tags even with spaces after the commas. A value of only separators or spaces returns `[]`, and the fallback stays unused. An array fallback must be an array. If it isn't one, the read throws [`InvalidFallback`](/docs/errors) (101). If its elements have the wrong type, it throws [`ArrayFallbackElementTypeMismatch`](/docs/errors) (103). With `of: Converters.Time`, the elements must be all time strings with units or all numbers of milliseconds. The `of` option also takes a function, for an element shape the tokens can't produce: ```ts const toUpstream = (raw: string) => { const [host, port] = raw.split(':'); return { host, port: Number(port) }; }; // UPSTREAMS=10.0.0.1:8080,10.0.0.2:8080 const upstreams = Envapter.getAs('UPSTREAMS', Converters.array({ of: toUpstream })); ``` `toUpstream` gets one trimmed, non-empty element at a time. > **WARNING** > > An array read throws when an element fails, even if you passed a fallback. An element that fails its token throws [`ArrayElementConversionFailed`](/docs/errors) (206). If your function returns `undefined` or `null` for an element, that throws the same code. Under [strict mode](/docs/required-values#strict-mode), a trailing comma in a deployed `.env` throws [`EmptyArrayElement`](/docs/errors) (207) at read time, which can stop startup. A required read reports each of these as an issue on one `InvalidEnvValue`. ## Custom converters When no token fits, pass a `(raw: string) => T` function to `getAs`. envapt calls it only when the variable is set, with `${VAR}` [templates](/docs/templates) already expanded. A missing variable returns the fallback, or `undefined` without one. ```ts const toSemver = (raw: string) => { const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(raw); if (!match) return undefined; return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) }; }; // APP_VERSION=2.4.1 const version = Envapter.getAs('APP_VERSION', toSemver); ``` For a value like `2.4`, `toSemver` returns `undefined`. This read passes no fallback, so `version` is `undefined`. Returning `null` works the same way. The exported `ConverterFunction` type describes such a function, for a converter you keep in its own file. A function that checks a rule, like a minimum version, returns `undefined` without saying which rule failed. A [schema](/docs/standard-schema) reports the failed rule. --- # Decorators Declare typed config as class fields with @Envapt, using accessor fields or envapt/legacy. Without a config class, every file that reads `PORT` repeats the key, the converter, and the fallback, so the copies can drift apart. `@Envapt` replaces them with one declaration on a class field. Every file reads `Config.port` and gets the same typed value. ```ts class Config { @Envapt('PORT', { converter: Converters.Port, fallback: 3000 }) static accessor port: number; @Envapt('CACHE_TTL', { converter: Converters.Time, fallback: '15m' }) static accessor cacheTtl: number; } Config.cacheTtl; ``` `cacheTtl` is a `number` of milliseconds even though its fallback is the string `'15m'`, because the Time converter converts the fallback too. A decorated field uses the same keys, [converters](/docs/converters), and cache as `getAs`. A value you read once, in one place, needs no class, and [`Envapter.getAs`](/docs/envapter) is shorter there. The same declaration works on instance fields. If your project already sets `experimentalDecorators`, import from `envapt/legacy` instead ([Legacy decorators](#legacy-decorators)). ## Declaring decorated fields Declare each field with the `accessor` keyword. A decorator on a plain field can only return a starting value for it. That value is computed once when the class is defined (for a static field) or each time you construct an instance. It still holds the old value after a later `Envapter.useSource` call or a change of environment. On a static field, a missing required value would also throw while the module loads. An `accessor` field gives the decorator a getter and a setter to replace, so envapt reads the variable on first access and makes assignment throw `InvalidUserDefinedConfig` (302). Because envapt reads on first access, a class can be defined before `useSource` runs on Workers or in the browser ([Sources](/docs/sources)). TypeScript enforces this. A plain field gives the decorator `undefined`, while an accessor decorator receives the accessor object. ```ts class Config { @Envapt('PORT', { converter: Converters.Port, fallback: 3000 }) static port: number; } ``` Adding `accessor` fixes both errors. The cost is the keyword, plus a `!` on instance fields. TypeScript reports TS2564 on `accessor host: string` until you write `accessor host!: string`. Static fields skip that check. Write no initializer on either form, since envapt supplies the value. ```ts class Server { @Envapt('HOST', { fallback: 'localhost' }) accessor host!: string; @Envapt('SENTRY_DSN') accessor sentryDsn!: string | undefined; @Envapt('REGION', { fallback: 'us-east-1' }) static accessor region: string; } ``` `sentryDsn` has no options, so it is the raw string or `undefined`. This form needs no compiler flag with TypeScript 5.0 or later. Output from `tsc` runs on Node, Bun, and Deno. Bun and Deno also run a `.ts` file with accessor fields directly. On Workers, the browser, and edge runtimes, your bundler has to compile them. [Compatibility](/docs/compatibility#decorators-per-runtime) covers each runtime. ### Legacy decorators Legacy decorators are TypeScript's older property decorators. They are called with `(target, key)`, where modern ones receive `(target, context)`. TypeScript emits and checks that call shape only when `experimentalDecorators` is on. Use the modern form unless your project already sets the flag. | | Modern | Legacy | | --- | --- | --- | | Import from | `envapt` | `envapt/legacy` | | Static field | `static accessor x: T` | `static readonly x: T` | | Instance field | `accessor x!: T` | `declare readonly x: T` | | `tsconfig.json` | nothing | `"experimentalDecorators": true` | ```ts class Config { @Envapt('PORT', { converter: Converters.Port, fallback: 3000 }) static readonly port: number; @Envapt('SESSION_TTL', { converter: Converters.Time, fallback: '15m' }) declare readonly sessionTtl: number; } ``` `Converters` still comes from `envapt`. Only the decorators come from `envapt/legacy`. `sessionTtl` uses `declare` because with `useDefineForClassFields` on (the default for ES2022 and later targets), `tsc` emits a plain instance field as a class field declaration. That gives every instance its own `undefined` that hides the getter. `declare` stops that emit. A `declare static` field reads `undefined` under `tsc`, so write plain `static readonly`. Neither form takes an initializer. ## Options The first argument is the variable name, or an [ordered list of names](/docs/envapter#ordered-keys). The first name that has a value wins. The second is an optional options object. | Name | Type | Default | Description | | ----------- | ----------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `converter` | `Converters member, array(), or function` | `raw string, or the fallback's type` | How to turn the raw string into a value: a Converters member such as Converters.Port, a Converters.array(...) result, or a function (raw: string) => T. | | `fallback` | `T` | `undefined` | Returned when the value is missing or the converter cannot convert it. It removes undefined from the field type. Time converters also take a string like "15m". | | `required` | `boolean` | `false` | Throws on first access when the value is missing or cannot convert. Cannot be combined with fallback. | | `schema` | `StandardSchemaV1` | `undefined` | Any Standard Schema validator, such as zod, valibot, or arktype. Cannot be combined with converter. | If you combine `required` with `fallback`, combine `schema` with `converter`, or give `schema` a value that isn't a Standard Schema, the class throws `InvalidUserDefinedConfig` (302) when it is defined, before any field is read. A `fallback` that doesn't match the converter throws `FallbackConverterTypeMismatch` (104) on first access. With a fallback and no converter, envapt infers the converter from the fallback's type. A string, number, boolean, or bigint fallback converts as that type, and any other fallback is a compile error until you pass a converter. [Converters](/docs/converters) lists the members of `Converters` and the time units. ```ts class Config { @Envapt('ALLOWED_ORIGINS', { converter: Converters.array({ of: Converters.Url }), fallback: [] }) static accessor allowedOrigins: URL[]; @Envapt('FEATURE_FLAGS', { converter: (raw) => raw.split('|').map((flag) => flag.trim()), fallback: [] }) static accessor featureFlags: string[]; @Envapt('TRUSTED_IPS', { converter: (raw) => new Set(raw.split(',').filter(Boolean)) }) static accessor trustedIps: Set | undefined; } ``` `trustedIps` ends in `| undefined` because it has no fallback and isn't required. ### The field type is checked Without a check, a field typed `number` could read as `undefined` at runtime when the value is missing. The declared type has to hold what the converter returns. A wider type compiles, but a narrower one fails with "field type must hold the converter output". The legacy decorators run the same check through `EnvaptFieldDecorator`, which `envapt/legacy` exports. ```ts class Config { @Envapt('SAMPLE_RATE', { converter: Converters.Float }) static accessor sampleRate: number; } ``` `SAMPLE_RATE` has no fallback and isn't required, so its output is `number | undefined`. The field needs that union, or a fallback to remove it. ## Required fields `required: true` makes the field throw on its first access when the value is missing or can't convert. [Required values](/docs/required-values) explains both errors, `MissingEnvValue` (305) and `InvalidEnvValue` (210). ```ts class Config { @Envapt('DATABASE_URL', { converter: Converters.Url, required: true }) static accessor databaseUrl: URL; @Envapt('SIGNING_KEY', { converter: (raw: string) => new TextEncoder().encode(raw), required: true }) static accessor signingKey: Uint8Array; } ``` Nothing reads a field until your code does, so a missing `DATABASE_URL` throws on the first request that touches `Config.databaseUrl`. To make that throw at startup, read each required field once when the app starts, for example `void Config.databaseUrl;`. Each field throws alone, so a deploy missing three variables takes three runs to find. [`getRequiredAll`](/docs/required-values) reports all three at once, but you pass it each key and converter again, which the class already declares. ## Validating with a schema A converter such as `Converters.Port` returns your fallback without an error when it can't convert a value. A schema checks the value against your rules. If it rejects the value, the read throws `SchemaValidationFailed` (208), or `InvalidEnvValue` (210) on a `required` field. A field that isn't required still throws. Pass `schema` for a Standard Schema validator. The field type is the schema's output, with `| undefined` unless you add `required: true` or a `fallback`. ```ts class Config { @Envapt('STRIPE_SECRET_KEY', { schema: z.string().startsWith('sk_'), required: true }) static accessor stripeKey: string; } ``` `startsWith('sk_')` runs on the raw value before `stripeKey` is ever a `string`. A schema in `converter` throws `InvalidUserDefinedConfig` (302). [In a decorator](/docs/standard-schema#in-a-decorator) lists the other schema mistakes. ## One value per field A decorated value is read on first access and cached per class and field. Every instance of a class reads the same cached value, so two instances of `Config` never differ. A static and an instance field with the same name have separate cached values. The cached value clears when you call `Envapter.useSource` or change a setting that rebuilds the [cache](/docs/sources#when-values-are-read), such as `Envapter.environment`, and the next access reads again. In a test, `useSource` gives a field a different value, as [Testing](/docs/testing#decorated-classes) shows. A decorated class doesn't need to extend anything. To call `getAs` and the other read methods on the class too, see [Extending Envapter](/docs/envapter#extending-envapter). ## No environment source is bound If you access a decorated field before any source is bound, it throws `NoSourceBound` (307) with the message "No environment source is bound". On Node, Bun, and Deno, every envapt entry binds `FileSource` when it loads, `envapt/legacy` included. On Workers, the browser, and edge runtimes, nothing is bound until `Envapter.useSource` runs, so access the field after that call. The error is described on [Sources](/docs/sources#no-environment-source-is-bound). --- # Edge runtimes Bind the config from Vercel Edge, Fastly Compute, or Expo, then read typed values as on Node. Vercel Edge gives you `process.env` as an object. Fastly Compute gives you a function that returns one value per call. Expo replaces each `process.env.EXPO_PUBLIC_*` reference with its value when it builds, and it can also hand you an `extra` object. On each of them you call `Envapter.useSource` before the first read, because the portable build they load [starts with no source](/docs/sources). Any other runtime on the portable build works the same way. [Compatibility](/docs/compatibility#portable-build-you-bind-a-source) lists the export condition each runtime resolves to. ## Vercel Edge Vercel's bundler resolves the portable build through the `edge-light` condition. Vercel [recommends migrating to the Node.js runtime](https://vercel.com/docs/functions/runtimes/edge), but if you still use Edge, bind `process.env`. ```ts title="lib/config.ts" Envapter.useSource(new PortableSource(process.env)); export const config = Envapter.getRequiredAll( { API_BASE_URL: Converters.Url, CACHE_TTL: Converters.Time }, 'camelCase' ); ``` [`PortableSource`](/docs/sources#the-sources-envapt-ships) copies `process.env` when you construct it, so a variable you set after that isn't in the source. `config.cacheTtl` is a `number` of milliseconds, as `Converters.Time` always returns it. Vercel's Node.js functions load the node build, which binds `FileSource` on import and needs no `useSource` call. ## Fastly Compute Fastly gives your code a function, `env` from `fastly:env`, that takes one name and returns its value. Pass that function to `useSource`. envapt calls it the first time you read each key and [caches the answer](/docs/sources#a-function-can-be-a-source). Add the `fastly` condition to your bundler's resolve conditions. Without it, an import from `envapt` resolves to the node build, which imports `node:*` modules. Fastly's types say `env()` works only while a request is being handled, and `useSource` calls `env('ENVAPT_DEBUG')` as it binds. If you bind at module scope, that call runs during initialization, so envapt never reads your `ENVAPT_DEBUG` setting. Bind inside the handler, once, and read there too. ```ts title="src/index.ts" let bound = false; export function handle(request: Request): Response { if (!bound) { Envapter.useSource(env); bound = true; } const { originUrl } = Envapter.getRequiredAll({ ORIGIN_URL: Converters.Url }, 'camelCase'); const target = new URL(new URL(request.url).pathname, originUrl); return new Response(null, { status: 302, headers: { location: target.href } }); } addEventListener('fetch', (event) => event.respondWith(handle(event.request))); ``` The `bound` flag keeps `handle` from rebuilding envapt's cache on every request. A missing `ORIGIN_URL` throws on the first request that reads it, since nothing reads before then. Set custom variables when you build, by passing `--env` to the `js-compute-runtime` command in your `build` script: ```sh js-compute-runtime --env ORIGIN_URL=https://origin.example.com src/index.js bin/main.wasm ``` Fastly's [environment variables](https://www.fastly.com/documentation/reference/compute/ecp-env/) docs list the built-in ones, and none of them is `NODE_ENV`. Until you pass one of the [detection keys](/docs/environment#detecting-the-environment) with `--env`, `Envapter.environment` is `'development'`. ## Expo and React Native Metro 0.82 and later read package exports by default and resolve the `react-native` condition, which selects the portable build. Expo recommends referencing `process.env.EXPO_PUBLIC_*` variables directly in your code ([Expo's environment variables](https://docs.expo.dev/guides/environment-variables/)). Expo replaces each reference that uses dot notation with its string when it builds the bundle. Bracket access, as in `process.env['EXPO_PUBLIC_API_URL']`, and destructuring aren't replaced. Metro defines `process.env` at runtime with only `NODE_ENV` in it, so `new PortableSource(process.env)` binds that one key. Reference each variable by name instead. Add `NODE_ENV` to the object too, because `Envapter.environment` reads it from the source. ```ts title="src/config.ts" Envapter.useSource( new PortableSource({ NODE_ENV: process.env.NODE_ENV, API_URL: process.env.EXPO_PUBLIC_API_URL, FEATURE_BETA: process.env.EXPO_PUBLIC_FEATURE_BETA }) ); export const apiUrl = Envapter.getRequired('API_URL', Converters.Url); export const betaEnabled = Envapter.getAs('FEATURE_BETA', Converters.Boolean, false); ``` A variable that is unset when you build is `undefined` here, and `PortableSource` leaves it out. `getAs` then returns its fallback, and `getRequired` throws. If your config already comes from `app.config.js`, put it under `extra`. `expo-constants` returns that object at runtime, so you bind the whole object. ```ts title="src/config.ts" Envapter.useSource(new PortableSource(Constants.expoConfig?.extra ?? {})); export const apiUrl = Envapter.getRequired('API_URL', Converters.Url); ``` > **WARNING** > > Expo puts both the `extra` object and every `EXPO_PUBLIC_` value inside your app. Anyone who downloads the app can read them, so keep secrets and server-side keys out of both. --- # .env file syntax Write .env values with quotes, # characters, or multi-line text and know how envapt parses each. envapt reads [`.env` files](/docs/env-files) with its own parser. The parser runs whenever the bound source reads files, which on the [node build](/docs/sources) is `FileSource`. ## Keys and values Each setting is a `KEY=value` line. An optional `export` prefix is allowed because it lets you load the same file in a shell with the `source` command. ```dotenv APP_NAME=billing-api export PORT=3000 ``` These rules apply to every line: - A key is letters, digits, and underscores, and doesn't start with a digit. - A line with any other key, such as `my-key=1`, is skipped without a warning. - Whitespace before the key, around `=`, and after an unquoted value is ignored. - If a file repeats a key, the last line wins. An empty value parses to an empty string, which [`Envapter.get`](/docs/envapter) and the other read methods treat as [missing](/docs/envapter#fallbacks-and-missing-values). ```dotenv EMPTY= ``` The line still sets `EMPTY`, so a file with lower priority can't supply it. A `PORT=` in `.env.local` keeps the `PORT=3000` in `.env` from loading, and the read finds `PORT` missing. Delete the `PORT=` line to let `.env` supply the value. The parser stores values literally, including `${VAR}`. envapt [expands it](/docs/templates) when you read the value. ## Comments A line whose first non-whitespace character is `#` is a comment. An inline `#` also starts a comment when it follows whitespace, or when it is the first character of an unquoted value. A `#` in the middle of a value, with no whitespace before it, is kept. ```dotenv # full-line comment HOST=localhost # inline comment, stripped TAG=build#42 # the # in build#42 has no whitespace before it COLOR="#ff0000" # quoted, so the # is kept ``` `TAG` reads as `build#42` and `COLOR` reads as `#ff0000`. Without the quotes, `COLOR=#ff0000` reads as an empty string, because a `#` at the start of an unquoted value starts a comment. ## Quoting You can write a value inside single quotes, double quotes, or backticks. The quote style decides whether envapt decodes escapes. | Quote | Escapes | Use it for | | --- | --- | --- | | `"double"` | decodes `\n` `\r` `\t` `\\` `\"` | values where you want `\n` to become a real newline | | `'single'` | none, fully literal | values that should stay exactly as written | | `` `backtick` `` | none, fully literal | same as single, when the value contains `'` | Inside double quotes, any other backslash sequence, such as `\$`, stays as written. ## Multi-line values A quoted value can span several lines: one opening quote, the content, one closing quote. Use this for a certificate or private key. ```dotenv TLS_CERT="-----BEGIN CERTIFICATE----- MIIBkTCB+wIJAKHH... ...more base64... -----END CERTIFICATE-----" ``` You read it back as one string, with its newlines: ```ts const cert = Envapter.get('TLS_CERT'); ``` `cert` is `string | undefined` because the call has no fallback. With double quotes you can keep the value on one line and write `\n` between segments to have envapt decode them. With single quotes or backticks the line breaks must be real, because a literal `\n` stays literal. > **WARNING** > > A quote with no closing quote takes the following lines into the value. It stops at the next line that contains the same quote character, or at the end of the file if no line does. Take this file: > > ```dotenv > A="abc > B=2 > ``` > > `A` holds both lines, and `B` is never set. ## Unescaped inner quotes Like dotenv, envapt ends a quoted value at the last matching quote on the line, so inner quotes need no escaping. ```dotenv FIREBASE_CONFIG="{"projectId":"billing-api","region":"eu"}" ``` `FIREBASE_CONFIG` reads as the JSON text from the fence. Use the [`Json` converter](/docs/converters#json) to parse it. Double quotes also decode `\n` and `\"`, which changes JSON that contains either. Wrap such JSON in single quotes. This works on a single line only. envapt closes a value at the last matching quote on the first line that has one. If a value spans lines and holds inner quotes, it ends at the last quote on that first line, and the text after it is discarded. A trailing comment that contains the same quote character joins the value. For example: ```dotenv A="a" # it's "x" ``` `A` reads as `a" # it's "x`. A trailing comment without that quote isn't part of the value. --- # .env files on Node Load per-environment .env files in the right order on Node, Bun, and Deno, in place of dotenv. dotenv loads the `.env` file you point it at. Once the app has a production setup and a development one, you have to choose the right file yourself, and decide which file wins when two define the same key. On Node, Bun, and Deno, envapt chooses the file for you. It builds a list of files from the current [environment](/docs/environment) and layers them, most specific first. That list is the **cascade**. A **profile** is the list of extra paths you give one environment. Those paths take priority over the cascade files. Reading files is a node-build feature (see [Sources](/docs/sources) for the builds). On the portable build, the file APIs on this page (`envPaths`, `baseDir`, `envFileOptions`, `configureProfiles`, `resetPaths`) warn once and do nothing, or throw `FileApiUnsupported` if [`Envapter.fileApiMode`](/docs/compatibility#file-apis-and-fileapimode) is `'throw'`. `syncProcessEnv` does nothing on the portable build and doesn't warn. There, bind a source with `Envapter.useSource`. `envapt/config` always loads the node build, so leave it out of a Worker or browser bundle. ## Which files load For the current environment, envapt reads these files, highest priority first. ```txt NODE_ENV=production ├─ .env.production.local wins over everything below ├─ .env.production ├─ .env.local └─ .env lowest priority of the files ``` A file that doesn't exist is skipped. By default, a variable already set in the shell wins over every file, so `PORT=7 node app.js` keeps 7 no matter what the files set. - my-app/ - .env - .env.local - .env.production - package.json With those files on disk, each environment reads: | Environment | Files read, highest priority first | | --- | --- | | `production` | `.env.production`, `.env.local`, `.env` | | `development`, `staging`, `test` | `.env.local`, `.env` | envapt reads the files when you import it, from the working directory ([`baseDir`](#reading-from-a-fixed-directory) changes that). [When values are read](/docs/sources#when-values-are-read) explains when a later change takes effect. > **WARNING** > > Set `NODE_ENV` in the shell. envapt picks the files before it reads any of them, from the variables the bound source already holds. With the default [`FileSource`](/docs/sources#the-sources-envapt-ships), those are the shell's variables. A `NODE_ENV=production` line inside `.env` makes `Envapter.environment` return `'production'` but doesn't load `.env.production`. To select the files from code, [pin the environment](/docs/environment#pinning-the-environment). To see which files loaded and which keys came from each, start the process with `ENVAPT_DEBUG=verbose`. An `Envapter.debug = 'verbose'` line in your code runs after the first load, so it logs only later rebuilds. [Errors and debugging](/docs/errors) lists the levels. ## Coming from dotenv If your app starts with `import 'dotenv/config'`, replace it with envapt's `envapt/config`. It loads the cascade as soon as it is imported and writes the loaded keys into `process.env`, so code that reads `process.env.PORT` keeps working. ```ts tab="Import" // before: import 'dotenv/config'; import 'envapt/config'; ``` ```bash tab="Node flag" # before: node -r dotenv/config dist/app.js node --import envapt/config dist/app.js ``` The flag is Node's. On Bun and Deno, use the import line. envapt ships [ESM only](/docs/compatibility#module-format-and-typescript), and `envapt/config` has no `require` entry, so `require('envapt/config')` throws even on a Node that can `require()` ESM. A CommonJS app uses the flag. These things differ from dotenv. 1. **More files load.** dotenv reads `.env`. envapt also reads `.env.local`, `.env.`, and `.env..local`, so a leftover `.env.local` or `.env.production` in your repo now loads. 2. **Values are expanded.** envapt expands a `${VAR}` reference when you read the value, and `envapt/config` writes the expanded value into `process.env`. An unresolved reference follows the rules on [Templates](/docs/templates). 3. **The parser is envapt's own.** It skips a key with `.` or `-` in it. [.env file syntax](/docs/env-file-syntax) has the full rules. These `dotenv.config()` options have counterparts: | dotenv | envapt | | --- | --- | | `path` | [`Envapter.envPaths`](#replacing-the-cascade), which throws at the assignment if a file is missing | | `encoding`, `override` | [`Envapter.envFileOptions`](#file-options), with both set in one object | | `debug` | `ENVAPT_DEBUG=verbose` in the shell, see [Errors and debugging](/docs/errors) | Without `envapt/config`, envapt leaves `process.env` alone and keeps the loaded values in its own cache. Only reads through envapt, including decorated fields, use them. If another library reads `process.env` directly, import `envapt/config` or turn on `syncProcessEnv`. ```ts Envapter.syncProcessEnv = true; ``` `syncProcessEnv` is `false` by default. Turned on, it writes the keys envapt loaded from files into `process.env`. A key already set in the shell is skipped, unless [`override`](#file-options) is on. Turning it back to `false` doesn't remove keys already written. Prefer the import. The `syncProcessEnv` line runs in your module body, after the imports have run, so a library that reads `process.env` while it loads finds no keys. `import 'envapt/config'` writes the keys during the import. ## Choosing the files yourself ### Reading from a fixed directory Relative paths resolve against the working directory. In a monorepo the process often starts at the repository root, so envapt doesn't find a `.env` inside a package folder. Set `Envapter.baseDir` to the folder relative paths start from. `baseDir` applies to the cascade, profile paths, and relative `envPaths`. Absolute paths stay unchanged. ```ts Envapter.baseDir = import.meta.url; ``` `baseDir` takes a directory path, `import.meta.dirname`, a `URL`, or `import.meta.url`. envapt treats a `file:` URL as the folder containing that module, so `import.meta.url` makes `baseDir` the folder of the file that sets it. Assign `undefined` to go back to `process.cwd()`. After `import 'envapt/config'`, a later `baseDir`, `envPaths`, `configureProfiles`, or pinned environment can't undo the first load. The loaded keys are in `process.env` by then, and variables in `process.env` win over files. Start the process in the folder that holds your `.env` files when you use the import. ### Replacing the cascade Set `envPaths` to load an exact list of files in place of the cascade, such as a file whose name the cascade can't produce. It also takes a single path as a string. Earlier paths win. ```ts Envapter.baseDir = import.meta.url; Envapter.envPaths = ['config/app.env', '.env']; ``` Assigning `envPaths` disables the cascade and any profiles. The assignment checks the files immediately, against the folder `baseDir` holds at that moment, so set `baseDir` first. Reading `Envapter.envPaths` returns the list you assigned, or `['.env']` if you never assigned one, so it doesn't show which cascade files loaded (see the debug tip above). #### Environment file not found at path Every path in `envPaths` has to exist, which catches a typo in the list. A missing file throws `EnvaptError` with code `EnvFilesNotFound` (303) at the assignment. In the cascade, a missing file is skipped. ### Profiles Use `configureProfiles` when one environment stores its files somewhere else, such as a mounted secrets folder. It takes a `ProfilesConfig`, and each environment's entry is an `EnvProfile` with a `paths` string or array. Those paths take priority over the cascade files for that environment, and earlier paths win. ```ts Envapter.configureProfiles({ production: { paths: ['config/prod.env', 'secrets/prod.env'] }, staging: { paths: 'config/staging.env' } }); ``` To load no cascade files at all, add `useDefaults: false` beside the environment names. `useDefaults` is a boolean and defaults to `true`. envapt loads only the profile paths. ```ts Envapter.configureProfiles({ production: { paths: 'config/prod.env' }, useDefaults: false }); ``` With that call, envapt reads only `config/prod.env` in production and no files in the other environments. envapt checks only the active environment's profile paths. If production becomes the active environment and its file is missing, envapt throws `EnvFilesNotFound`. ```ts Envapter.environment = 'development'; Envapter.configureProfiles({ development: { paths: 'config/dev.env' }, // throws here if this file is missing production: { paths: 'config/prod.env' } // not checked yet }); Envapter.environment = 'production'; // throws here if config/prod.env is missing ``` `resetPaths()` undoes `envPaths`, profiles, and a pinned environment, and reloads the cascade right away. It keeps `baseDir` and `envFileOptions`. ## File options Set `envFileOptions` when a file isn't UTF-8, or when a file value should replace one set earlier. It takes an `EnvFileOptions`. | Name | Type | Default | Description | | ---------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------- | | `override` | `boolean` | `false` | A later file replaces a key an earlier file set, and a file value replaces a shell variable in what envapt reads. | | `encoding` | `string` | `'utf8'` | The encoding used to read each file. | ```ts Envapter.envPaths = ['config/app.env']; Envapter.envFileOptions = { override: true }; ``` Assigning `envFileOptions` replaces the whole object, so a key you omit goes back to its default. > **WARNING** > > `override: true` reverses the priority order of the cascade. envapt reads the most specific file first, and `override` makes each later file replace earlier values, so `.env` takes priority over `.env.production`. It also makes file values replace shell variables in what envapt reads. A single file from `envPaths`, as in the sample above, is safe because one file has no order to reverse. Each file follows the rules on [.env file syntax](/docs/env-file-syntax), including quotes and values that span lines. --- # Reading values Read an environment variable as a string or through a converter, and see what a bad value returns. A reader is a method on `Envapter` that reads one variable, like `get` for a string or `getAs` for a typed value. On Node, readers read `process.env` and your [`.env` files](/docs/env-files). You call every reader on `Envapter` itself, so you construct nothing. Most readers return the value, or `undefined` when it is missing. The [required readers](/docs/required-values) throw. On Workers, the browser, and edge runtimes, every reader throws [`NoSourceBound`](/docs/errors) until you bind a [source](/docs/sources) with `useSource`. ## Strings and typed values Without a reader you write the truthy list for flags yourself, and you remember that an empty string means unset. `getAs` with `Converters.Boolean` reads `1`, `yes`, `true`, and `on` as true and `0`, `no`, `false`, and `off` as false, in any case. `get` returns the value as a string, and `getAs` returns the type its [converter](/docs/converters) produces. Both take a key and an optional fallback. ```ts const appName = Envapter.get('APP_NAME', 'envapt'); const sampleRate = Envapter.getAs('SAMPLE_RATE', Converters.Number, 0.25); const debug = Envapter.getAs('DEBUG', Converters.Boolean, false); const maxUploadBytes = Envapter.getAs('MAX_UPLOAD_BYTES', Converters.BigInt, 0n); ``` `0n` is the fallback, so `maxUploadBytes` is a plain `bigint`. `sampleRate` uses `Converters.Number` because `0.25` is a fraction. That converter also accepts `3.5` or `99999` as a port. `Converters.Port` takes only whole numbers from 0 to 65535, and `Converters.Integer` takes only whole numbers. [Converters](/docs/converters) lists the rest. ## Fallbacks and missing values Without a fallback, a reader's type includes `undefined`, so you handle that at every call site. A fallback removes `undefined` from the type. ```ts const withFallback = Envapter.get('API_URL', 'http://localhost'); const noFallback = Envapter.get('API_URL'); ``` `withFallback` is a `string` because `'http://localhost'` supplies a value when `API_URL` is missing. `noFallback` can be `undefined`. A variable is missing when it is unset or empty. In strict mode (`Envapter.strict = true`) a value of only spaces is missing too. With strict mode off, it counts as a value, so `Converters.Number` reads it as `0` ([Required values](/docs/required-values#strict-mode) covers strict mode). The readers differ when a value is set but can't convert. | Reader | Result for a value it can't convert | | --- | --- | | `getAs` with a built-in [converter](/docs/converters) | the fallback, or `undefined` without one | | `getAs` with [`Converters.array`](/docs/converters#arrays) | throws [`ArrayElementConversionFailed`](/docs/errors) for a bad element, even with a fallback. In strict mode an empty element throws [`EmptyArrayElement`](/docs/errors) | | `getAs` with your own function | if the function returns `undefined` or `null`, `getAs` returns the fallback, or `undefined` without one. An error it throws reaches your code unchanged | | `getAs` with a [schema](/docs/standard-schema) | throws a schema error such as [`SchemaValidationFailed`](/docs/errors), and the fallback is not used | | [`getRequired`, `getRequiredAll`](/docs/required-values) | throws [`InvalidEnvValue`](/docs/errors) | A fallback can hide a typo. The `sampleRate` read above returns `0.25` for `SAMPLE_RATE=high`, and nothing reports it unless you set `Envapter.debug = 'verbose'` ([debug logging](/docs/errors#debug-logging)). If a wrong value has to stop the app, read it with [`getRequired`](/docs/required-values) instead. If a fallback has the wrong type for its converter, TypeScript reports it before you run the code, as with `'abc'` for `Converters.Port`. A time fallback is milliseconds or a string like `'15m'`, so `'5'` is an error too. In plain JavaScript, or when a cast gets past the types, the call throws instead. The error is `FallbackConverterTypeMismatch` or `MalformedTimeFallback`, and it throws even when the variable is set. [Errors and debugging](/docs/errors) has every code. ## Ordered keys Every reader except `getRequiredAll` accepts a list of keys. The reader uses the first key that has a value, so a renamed variable can keep its old name as a backup in the same read. It skips a key that is unset or empty, while `??` skips only an unset one. ```ts // DATABASE_URL if it has a value, else the name older deploys still set const databaseUrl = Envapter.getAs(['DATABASE_URL', 'LEGACY_DATABASE_URL'], Converters.Url); ``` > **WARNING** > > If you declare the list on its own, it widens to `string[]`, which a reader's key parameter rejects. Write the list inline, or add `as const`. > > ```ts > const keys = ['DATABASE_URL', 'LEGACY_DATABASE_URL'] as const; > ``` ## Other types with getAs Without `getAs`, you check for a missing variable, apply the default, and call `new URL(raw)` yourself, which throws on a bad value. `getAs` takes a converter, which can be: - a built-in converter such as `Converters.Url` or `Converters.Time`. - `Converters.array(...)`, which splits a value on commas into a list, converting each element with the `of` converter you pass. - your own `(raw: string) => T` function, which runs only when the variable has a value. - a [Standard Schema](/docs/standard-schema) validator, such as one from zod, valibot, or arktype. ```ts const origins = Envapter.getAs('ALLOWED_ORIGINS', Converters.array({ of: Converters.Url })); function parseCidr(raw: string) { const [network = '', bits = '32'] = raw.split('/'); return { network, bits: Number(bits) }; } const trustedProxies = Envapter.getAs('TRUSTED_PROXY_CIDR', parseCidr, { network: '127.0.0.1', bits: 32 }); ``` `origins` is a `URL[]`, or `undefined` while `ALLOWED_ORIGINS` is unset. `parseCidr` turns `10.0.0.0/8` into `{ network: '10.0.0.0', bits: 8 }`, a shape no built-in converter produces. ## The raw string Every other reader expands `${NAME}` references inside the value ([Templates](/docs/templates) explains the syntax). Expansion breaks a value your own code sets later, like an email subject holding `${ORDER_ID}`. If a variable named `ORDER_ID` exists, envapt substitutes its value. In strict mode, a reference to a missing variable throws. `getRaw` returns the string exactly as stored. It takes a key or an ordered key list and no fallback. To default an unset key, write `Envapter.getRaw(key) ?? 'default'`. It returns `undefined` when the key is unset and an empty string when the variable is empty. ```ts // ORDER_EMAIL_SUBJECT=Your order ${ORDER_ID} has shipped const subject = Envapter.getRaw('ORDER_EMAIL_SUBJECT'); ``` `subject` keeps the `${ORDER_ID}` text, ready for your own code to replace. ## Extending Envapter Every reader is a static method, so a class that extends `Envapter` inherits all of them. You call them as `Config.get(...)`. A subclass is worth writing when one setting combines several reads. Decorated fields work on any class, so a config class that holds only decorated fields doesn't need to extend `Envapter`. ```ts class Config extends Envapter { static get origin(): string { const host = Config.get('HOST', 'localhost'); const port = Config.getAs('PORT', Converters.Port, 3000); return `http://${host}:${port}`; } } Config.origin; ``` `Config.origin` converts again on every access, while a [decorated field](/docs/decorators) keeps the value from its first read until the [cache](/docs/sources#when-values-are-read) is rebuilt. `Envapter` stores `strict`, the bound source, and the cache, so a subclass shares them. If you set `Config.strict = true`, strict mode turns on for every read in the process. ## More readers - [Required values](/docs/required-values): `getRequired` and `getRequiredAll` throw when a value is missing. `require` throws if any listed key has no value, and `has` returns whether a key has one. - [Templates](/docs/templates): `resolve` builds a string from several keys. - [Environment](/docs/environment): `isProduction`, `isDevelopment`, and the other environment checks. --- # Environment Read the environment from ENVIRONMENT, ENV, NODE_ENV, or MODE, and pin one when a test needs it. You've written `process.env.NODE_ENV === 'production'` before. It reads one variable and compares one exact spelling, so `PRODUCTION` takes the other branch and logs nothing. It also finds nothing if `NODE_ENV` isn't set, and a Vite build exposes `MODE` instead. envapt checks several variables, ignores case, and resolves one of the environments in the table below. Detection works on every runtime once you've bound a [source](/docs/sources). On Node, Bun, and Deno the environment also determines which `.env` files load, as [.env files on Node](/docs/env-files) shows. ## Detecting the environment envapt reads these variables in order and uses the first one that has a value: ```txt ENVIRONMENT → ENV → NODE_ENV → MODE → development ``` The first variable with a value decides, even when its value is unrecognized. With `ENV=prod` and `NODE_ENV=production` both set, the result is `'development'`, because `ENV` comes first and `prod` matches no environment. Set `Envapter.debug = 'warn'` to log that fallback (see [Errors and debugging](/docs/errors)). An empty value counts as unset, so envapt checks the next variable. A whitespace-only value counts as unset only in [strict mode](/docs/required-values#strict-mode). `ENVIRONMENT_KEYS`, exported from `envapt`, is a readonly tuple of these variable names in this order, for tools that need to read the same variables. envapt detects the environment on the first read and keeps the result until the cache rebuilds or you pin one. A test that sets `process.env.NODE_ENV` after something has read `Envapter.environment` still reads the old value until then. envapt matches the value it found, ignoring case, against these environments: | Variable holds | `Envapter.environment` | | --- | --- | | `production` or `PRODUCTION` | `'production'` | | `staging` | `'staging'` | | `test` | `'test'` | | `development` | `'development'` | | anything else, such as `prod` | `'development'` | | no variable set | `'development'` | The variables envapt reads come from the bound [source](/docs/sources). On Node that is `process.env` plus your `.env` files. envapt picks which `.env` files to load before it reads any of them, so a `NODE_ENV` line inside `.env` doesn't change the files (see the [warning on .env files on Node](/docs/env-files#which-files-load)). In a Vite build, [`PortableSource(import.meta.env)`](/docs/sources#the-sources-envapt-ships) makes `MODE` available. envapt checks `MODE` last, so any variable earlier in that order wins over it ([Browser](/docs/browser) has the setup). If you read the environment on the portable build before binding a source, envapt throws `NoSourceBound` (307). ## Branching on the environment `Envapter.isProduction`, `isStaging`, `isDevelopment`, and `isTest` are booleans for the common checks. ```ts const logLevel = Envapter.isProduction ? 'warn' : 'debug'; ``` `logLevel` is the union `'warn' | 'debug'`. When each environment needs its own setting, type a record with `Environment` as its keys, one for each environment in the table above. If you omit one, TypeScript reports an error. ```ts const LOG_LEVELS: Record = { development: 'debug', staging: 'info', production: 'warn', test: 'silent' }; export const logLevel = LOG_LEVELS[Envapter.environment]; ``` ## Pinning the environment A script that takes its mode from a flag can't rely on detection. A test that must always run as `'test'` can't rely on it either, whatever the shell sets. Assign `Envapter.environment` to pin it. ```ts Envapter.environment = Environment.Staging; ``` A string such as `'production'` works too, matched ignoring case. If you assign a name envapt doesn't recognize, the environment becomes `'development'`. If you assign a value that isn't a string, envapt throws `InvalidUserDefinedConfig` (302). Assigning `Envapter.environment` rebuilds the [cache](/docs/sources#when-values-are-read), so on Node the `.env` files for the new environment load when you assign. After `import 'envapt/config'`, the first load has already written keys into `process.env`, and those keys win over the files the new environment loads ([Coming from dotenv](/docs/env-files#coming-from-dotenv)). Pinning changes nothing for them. A pin survives later cache rebuilds. Without a pin, a rebuild detects the environment again. > **TIP** > > Pin it once, in a module your entry file imports before anything that reads config. Imports run before your module body, so a pin in the entry file's body runs after every imported module has loaded. If one of those modules reads the environment while it loads, it gets the old value. On the portable build you can pin before `useSource`, and the bind applies the pin. On Node, `Envapter.resetPaths()` is the only way to remove a pin, and it also removes any `envPaths` and [profiles](/docs/env-files#profiles), the extra file paths you set per environment. On the portable build `resetPaths()` warns and does nothing, so a pin there changes only when you assign again. --- # Errors and debugging Look up an EnvaptError code, and turn on debug logging to see why a read returned your fallback. A read threw an `EnvaptError` with a number in its name? Find it in the [codes](#codes). A read returned something other than what your `.env` file says? Turn on [debug logging](#debug-logging). ## Catching an error When envapt detects a failure, it throws an `EnvaptError` with a numeric `code` from `EnvaptErrorCodes`, so you can branch on the cause. ```ts try { startServer(); } catch (error) { if (error instanceof EnvaptError) { error.code; if (error.code === EnvaptErrorCodes.MissingEnvValue) { console.error(error.message); } } throw error; } ``` `error.code` is typed `EnvaptErrorCodes`, so comparing it with a member such as `MissingEnvValue` narrows it. A `MissingEnvValue` (305) message lists keys only, so printing it is safe. The `instanceof` check matters because a converter function you wrote can throw its own error, which has no `code`. Inside `getAs` or a decorated field, envapt rethrows that error unchanged. A required read puts its message in the `issues` of an `InvalidEnvValue` (210) and keeps your error as that issue's `cause`. ## The error shape | Name | Type | Default | Description | | --------- | --------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `code` | `EnvaptErrorCodes` | | The number that identifies the failure. See the tables below. | | `name` | `string` | | EnvaptError followed by the code in brackets, such as EnvaptError [305]. | | `message` | `string` | | A description of the failure. The 208, 210, and 305 messages list keys only. The 206 and 209 messages can carry the element or the error text your schema threw. | | `issues` | `readonly { message, path, cause? }[] \| undefined` | | One entry per bad value, each with a message and a path that starts with the env key. An entry for a parser or schema that threw keeps the thrown value as its cause. Set only for SchemaValidationFailed (208) and InvalidEnvValue (210). | | `cause` | `unknown` | | The error your schema threw, for SchemaThrew (209). For InvalidEnvValue (210), the cause of the one issue that has one. Undefined otherwise. | An issue's `message` can quote the value it rejected. [Reading the error without leaking values](/docs/required-values#reading-the-error-without-leaking-values) shows what to log instead. ## Codes ### Fallback | Code | Name | Thrown when | | --- | --- | --- | | 101 | `InvalidFallback` | A fallback for an array converter isn't an array. | | 103 | `ArrayFallbackElementTypeMismatch` | An array fallback mixes element types, or its elements don't match the converter. | | 104 | `FallbackConverterTypeMismatch` | The fallback has the wrong type for the converter, such as `'80'` for `Converters.Number` in [`getAs`](/docs/envapter). This throws even when the variable is set. | | 105 | `MalformedTimeFallback` | A time-string fallback isn't ``, such as `'5'` when you meant `'5m'`. [Converters](/docs/converters) lists the units. | Every row here is a mistake in the code that makes the read, found the first time that read runs. ### Converter and schema | Code | Name | Thrown when | | --- | --- | --- | | 201 | `InvalidArrayConverterType` | The [`Converters.array`](/docs/converters#arrays) setup is malformed, such as a schema as its `of`. | | 202 | `InvalidBuiltInConverter` | A converter string isn't a built-in token, like `'nmber'`. The message lists the valid tokens. | | 203 | `InvalidCustomConverter` | The converter argument has an unsupported type, such as a number. | | 206 | `ArrayElementConversionFailed` | An array element can't convert to the element type. | | 207 | `EmptyArrayElement` | [Strict mode](/docs/required-values#strict-mode) only: an array element is empty or whitespace. | | 208 | `SchemaValidationFailed` | A read that isn't required ([`getAs`](/docs/envapter#other-types-with-getas), or a decorator without `required`) gets a value its [schema](/docs/standard-schema) rejects. | | 209 | `SchemaThrew` | A schema's `validate` throws during a read that isn't required. The original error is on `cause`. | | 210 | `InvalidEnvValue` | A [required read](/docs/required-values) gets a value it can't convert, or one your converter or schema rejects. [`getRequiredAll`](/docs/required-values) throws one 210 that lists every bad value. | The 201, 202, and 203 rows are mistakes in the code. The other rows come from the value in your environment. In a required read, 206, 207, and 209 arrive as `issues` of a 210. ### Configuration and lookup | Code | Name | Thrown when | | --- | --- | --- | | 301 | `MissingDelimiter` | An array converter's delimiter is empty or isn't a string. | | 302 | `InvalidUserDefinedConfig` | A setting, option, or converter is invalid. The message states which. See the list below. | | 303 | `EnvFilesNotFound` | A path in `envPaths`, or in the active environment's profile, doesn't exist. See [.env files on Node](/docs/env-files). | | 304 | `InvalidKeyInput` | The key list is empty, or a key isn't a string or is blank. | | 305 | `MissingEnvValue` | A [required read](/docs/required-values) finds no value. In strict mode, a [`${VAR}` reference](/docs/templates#unresolved-references) or a [`resolve`](/docs/templates#the-resolve-tagged-template) slot with no value throws it too. | | 306 | `FileApiUnsupported` | A file API from [.env files on Node](/docs/env-files) runs against a source with no files. The builds differ, see the list below. | | 307 | `NoSourceBound` | A read runs before any source is bound. The portable build starts unbound ([Sources](/docs/sources#no-environment-source-is-bound)), and a decorated field throws it there too ([Decorators](/docs/decorators#no-environment-source-is-bound)). | | 308 | `InvalidMergedSource` | [`merge`](/docs/sources#combining-sources) gets no members, or more than one source that reads files. | 305 is about the value in your environment. The other codes in this table are mistakes in the code or setup. A 306 comes from different calls on each build: - On the node build, `envPaths`, `baseDir`, and `configureProfiles` throw it while a source with no files is bound. - On the [portable build](/docs/sources), those three, `envFileOptions`, and `resetPaths` throw it only when `Envapter.fileApiMode` is `'throw'`. These cause a 302, and the message states which: - `Envapter.environment` set to something that isn't a string - `Envapter.debug` set to an unknown level - `syncProcessEnv` set to a non-boolean - `fileApiMode` set to anything other than `'warn'` or `'throw'` - `envFileOptions` given a key other than `encoding` and `override` - `String`, `Number`, `Boolean`, or `BigInt` passed as a converter. The message gives the matching member, such as `Converters.String` - `Symbol` passed as a converter - a decorator given `required` together with `fallback` - a decorator given `schema` together with `converter`, a `schema` that isn't a Standard Schema, or a schema as `converter` - a schema whose `validate` returns a Promise, which zod does when a `.transform` or `.refine` throws - an assignment to a decorated field ([Decorators](/docs/decorators#declaring-decorated-fields)) - the removed positional `@Envapt(key, fallback, converter)` form - a build tool's Stage 3 decorator transform that leaves out the accessor name ## Debug logging By default, if a converter such as `Converters.Port` can't convert a value, the read returns your fallback and logs nothing. A `PORT` of `80a` reads as `3000`, and nothing shows it happened. At the `verbose` level, debug logging prints a line for each such event, plus the files envapt loaded and the order they loaded in. An array element that can't convert throws `ArrayElementConversionFailed` (206) instead. ### Setting the level The level is a `DebugLevel`: `silent`, `warn`, or `verbose`. Set it in the shell to log from the first load, or in code to log events after that line. ```sh ENVAPT_DEBUG=verbose NODE_ENV=production node app.js ``` ```ts Envapter.debug = 'verbose'; ``` On Node, envapt loads the `.env` files when you import it, so an `Envapter.debug` assignment in your code runs after that load and logs only later events. An assignment to `Envapter.debug` wins over `ENVAPT_DEBUG` whenever it runs. > **WARNING** > > On Node, `ENVAPT_DEBUG` has to be set before the process starts, in the shell or your process manager. A line in a `.env` file is ignored, because envapt reads the level before it loads any file. A value other than the three lowercase levels is ignored as well. If you bind a source yourself, as on Workers or in the browser, `ENVAPT_DEBUG` comes from that source. On Workers a `vars` entry in your Wrangler config works, because `PortableSource` reads it like any other key. ### Reading the output Each line starts with `[envapt]` and a space. It goes to stderr on Node and to `console.error` on the portable build. Take an app that reads the port with a fallback. ```ts const port = Envapter.getAs('PORT', Converters.Port, 3000); ``` With `PORT=8080` in `.env` and `PORT=80a` in `.env.production`, a run with `ENVAPT_DEBUG=verbose` and `NODE_ENV=production` prints: ```txt [envapt] cache cleared, reloading config [envapt] base dir: working directory [envapt] effective .env paths: .env.production, .env [envapt] loaded .env.production: 1 key [envapt] .env.production -> PORT [envapt] loaded .env: 3 keys [envapt] .env -> DATABASE_URL [envapt] .env -> CACHE_TTL [envapt] cache populated: 67 keys total [envapt] could not convert PORT as port, using the fallback ``` `.env.production -> PORT` shows the production file set `PORT`. No `.env -> PORT` line follows. envapt keeps the first value it loads for a key, so it skipped the valid `PORT=8080` in `.env`. The last line says why `PORT` reads as 3000. The key total counts your shell's variables too. | Level | Logs | | --- | --- | | `silent` (default) | nothing from reads. The portable build still prints its file-API warnings ([Compatibility](/docs/compatibility)) | | `warn` | a `.env` file that couldn't be read, a `${VAR}` reference left unresolved, an unset or empty variable read through `get`, a typed getter, `getAs`, or a decorated field without `required` (whether it returns a fallback or `undefined`), an environment name envapt doesn't recognize or no environment name at all, and the error from a schema that returned a Promise | | `verbose` | everything in `warn`, plus the base directory, the effective paths, every file loaded with its key count, each key a file sets, cache rebuilds, keys mirrored to `process.env`, and a present value a converter couldn't convert | Lines carry keys and file paths. They leave values out, except the unrecognized environment name and the error text from a rejected schema. Use `verbose` for one run while you debug, since it prints a line for every key in every file. --- # Introduction Read environment variables as typed values, and stop the app at startup when one is missing or malformed. Every value in `process.env` is a string, so you've written lines like these before: ```ts const port = Number(process.env.PORT ?? 3000); const debug = process.env.DEBUG === 'true'; const cacheTtl = Number(process.env.CACHE_TTL ?? 60_000); const databaseUrl = process.env.DATABASE_URL; ``` Each line breaks on a value you didn't expect: - `port` is `NaN` for `PORT=80a`, and `0` for `PORT=` because `??` keeps an empty string. - `debug` is `false` for `DEBUG=1`. - `cacheTtl` is `NaN` for `CACHE_TTL=10m`. - `databaseUrl` is a `string | undefined`, so you still check it and parse the URL yourself. None of these stop the app. It starts with the bad value and fails later, somewhere far from the line that read it. Every file that reads one of these variables also carries its own copy of the line. ## Stop at startup on a bad value [`getRequiredAll`](/docs/required-values) reads a group of variables and converts each one to the type you ask for. If any are missing or can't be converted, it throws one error that lists every bad key: ```ts title="config.ts" export const config = Envapter.getRequiredAll({ DATABASE_URL: Converters.Url, PORT: Converters.Port, CACHE_TTL: Converters.Time }); config.CACHE_TTL; ``` ```txt title="terminal" $ PORT=80a CACHE_TTL=soon DATABASE_URL=postgres://db/app node config.ts EnvaptError [210]: Validation failed for PORT, CACHE_TTL. ``` `PORT=80a` and `CACHE_TTL=soon` both show up in that one error, so you fix both before the next deploy. When the values are good, `config.PORT` is a `number`, `config.DATABASE_URL` is a `URL`, and `CACHE_TTL=10m` comes back as `600000` milliseconds. If your entry file imports `config` first, a bad deploy stops before the app serves anything. ## Fall back to a default Some values have a safe default, like a debug flag or a timeout. [`Envapter.getAs`](/docs/envapter) takes the key, a [converter](/docs/converters), and the default: ```ts const debug = Envapter.getAs('DEBUG', Converters.Boolean, false); const timeout = Envapter.getAs('REQUEST_TIMEOUT', Converters.Time, '30s'); ``` `Converters.Boolean` reads `1`, `yes`, `true`, and `on` as true, so `DEBUG=1` gives `true`. `timeout` is `30000` when `REQUEST_TIMEOUT` is unset. A fallback also removes `undefined` from the type, so `debug` is a `boolean`, while a read with no fallback, like `Envapter.getAs('DATABASE_URL', Converters.Url)`, is `URL | undefined` and you handle the missing case yourself. The default also covers a value that can't convert, so `REQUEST_TIMEOUT=soon` gives `30000` with no error. Use `getRequiredAll` for anything where a typo should stop the app. [Fallbacks and missing values](/docs/envapter#fallbacks-and-missing-values) has the exact rules. Reading through envapt is optional, and code that reads `process.env` directly keeps working. A script that reads one string can stay on `process.env`. envapt helps most once several files read config, since one call gives each read its type, its default, and its check. ## Functions or decorators [`Envapter`](/docs/envapter) reads one value per call, as above. The [`@Envapt`](/docs/decorators) decorator reads a variable into a class field. A decorated field reads on first access and keeps that value until the cache is rebuilt, for example by a `useSource` call ([When values are read](/docs/sources#when-values-are-read)). A reader call converts again each time. ```ts class Config { @Envapt('PORT', { converter: Converters.Port, fallback: 3000 }) static accessor port: number; @Envapt('REDIS_URL', { converter: Converters.Url, fallback: new URL('redis://localhost:6379') }) static accessor redisUrl: URL; } ``` Use a decorator for settings that many files read, since the class declares each one once and `Config.port` works anywhere. Use a reader for a one-off read, or for a check that has to run at startup, since a decorated field reads only when something first accesses it. The cost is the `accessor` keyword on every field, which [Decorators](/docs/decorators) explains. Both forms share the same [converters](/docs/converters) and accept a [Standard Schema](/docs/standard-schema) validator such as zod, valibot, or arktype. A decorator takes the schema through its `schema` option. ## Where values come from A source is the object envapt reads values from. On Node, Bun, and Deno the source is `process.env` plus your `.env` files. It binds when you import `envapt`, so the first read works with no setup. If other code reads `process.env` directly, import `envapt/config` to put your `.env` values there too ([.env files on Node](/docs/env-files#coming-from-dotenv)). On Workers, in the browser, and on edge runtimes, envapt reads nothing until you bind a source. Your config arrives there as an object, such as the Workers `env` or `import.meta.env` in a bundler like Vite. Wrap that object in a [`PortableSource`](/docs/sources#the-sources-envapt-ships) and pass it to `Envapter.useSource` once, before the first read. If a read runs before that, it throws `NoSourceBound`. [Sources](/docs/sources) shows the full setup. ## Where to start - [Node, Bun, and Deno](/docs/quick-start): Install envapt and read a typed value from your .env file. - [Cloudflare Workers](/docs/workers): Bind the Worker env object once and read typed config. - [Browser](/docs/browser): Read typed values from the config your bundler injects. - [Edge runtimes](/docs/edge-runtimes): Bind the platform's config object on Vercel Edge, Fastly, and similar. - [Upgrading from v8](/docs/migrations): What changed in v9 and how to upgrade. --- # Next.js Read typed config in Next.js server code, client components, and edge routes, with startup checks. Next.js loads your `.env` files into `process.env`, and with plain `process.env` every value is a string. `process.env.MAX_CONNECTIONS` held `'10'`. A typo in `DATABASE_URL` appeared on the first query that used it. Each file that read a variable set its own default. [`Envapter`](/docs/envapter) reads that same `process.env` and converts each value when you read it. Code on the Node.js runtime loads envapt's node build, which reads `process.env` and your `.env` files with no setup. Client components and edge routes load the [portable build](/docs/compatibility#portable-build-you-bind-a-source), which reads nothing until you pass it a source with [`Envapter.useSource`](/docs/sources). ## Server code ```ts title="lib/config.ts" import { Converters, Envapter } from 'envapt'; export const config = Envapter.getRequiredAll( { DATABASE_URL: Converters.Url, SESSION_TTL: Converters.Time, MAX_CONNECTIONS: Converters.Integer }, 'camelCase' ); ``` [`getRequiredAll`](/docs/required-values) converts each key with its [converter](/docs/converters), and `'camelCase'` turns `MAX_CONNECTIONS` into `config.maxConnections`, a `number`. `config.sessionTtl` is a number of milliseconds and `config.databaseUrl` is a `URL`. If keys are missing, it throws one `MissingEnvValue` error that lists them all. [Structuring your config](/docs/app-config) explains why every read goes through one module. ### Failed to collect page data `next build` imports every route module while it collects page data, including dynamic routes, so `config` runs during the build too. If a required variable is missing on the build machine, the build stops: ```txt Error: Failed to collect page data for /api/orders [cause]: EnvaptError [305]: Missing required environment variables: DATABASE_URL. ``` 305 is `MissingEnvValue`. If the build machine has the same variables as the server, that build failure catches a missing variable before you deploy. If you build somewhere without your secrets, such as a Docker image that gets them when it starts, export a function: ```ts title="lib/config.ts" export function serverConfig() { return Envapter.getRequiredAll( { DATABASE_URL: Converters.Url, SESSION_TTL: Converters.Time, MAX_CONNECTIONS: Converters.Integer }, 'camelCase' ); } ``` `serverConfig()` reads the variables each time you call it. When Next.js collects page data, it imports the module without calling the function, so the build no longer checks your config. Call `serverConfig()` in `register()` to check it at server start, as [the next section](#checking-config-when-the-server-starts) shows. ### Static pages keep the values from the build If a page reads nothing from the request, Next.js renders it once during `next build` and serves that HTML. A value it reads through `config` or `Envapter.get` stays at whatever the build machine had until you build again. To read the value on each request, call `connection()` first: ```tsx title="app/status/page.tsx" export default async function StatusPage() { await connection(); return

Serving from {Envapter.get('DEPLOY_REGION', 'local')}

; } ``` `DEPLOY_REGION` then comes from the environment the server is running with, and `'local'` is the fallback when it's unset. ## Checking config when the server starts Without a startup check, a missing variable fails only the first request that imports `config`. That route returns 500 while the others keep working. Next.js calls `register()` in `instrumentation.ts` when the server starts, before it serves a request, so importing `config` there checks the required keys at startup: ```ts title="instrumentation.ts" export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./lib/config'); } } ``` If your config module exports `serverConfig()`, call it inside the `if` in place of the import. Next.js also runs `register()` on the edge runtime, where `envapt` resolves to the portable build. A read there before `useSource` throws [`NoSourceBound`](/docs/sources#no-environment-source-is-bound), and the `NEXT_RUNTIME` check keeps the import on the Node.js server. If `register()` throws, Next.js logs "Failed to prepare server", but the process keeps listening. Every request returns 500 with the same error. A health check that only tests whether the port is open still passes, so point it at a route that imports `config`. ## Which .env file wins Next.js picks its `.env` files by `NODE_ENV` and copies them into `process.env` before any of your code runs. By default, envapt ranks a value already in `process.env` above every file, so for each key that Next.js loads, this order sets the value: ```txt next start (NODE_ENV=production) ├─ the shell highest priority ├─ .env.production.local ├─ .env.local ├─ .env.production └─ .env ``` `.env.local` wins over `.env.production` here, the reverse of [envapt's own order](/docs/env-files#which-files-load). `next dev` reads the `.env.development` files in place of the `.env.production` ones. `NODE_ENV` also sets [`Envapter.environment`](/docs/environment) when `ENVIRONMENT` and `ENV` are unset. `next build` and `next start` set it to `production`, and `next dev` sets `development`. With `ENVIRONMENT=staging`, `Envapter.environment` is `'staging'`, so envapt reads `.env.staging`, a file Next.js never loads. Next.js still loads `.env.production`, because it picks files only by `NODE_ENV`. If a key is set in both files, envapt reads the `.env.production` value. Keep staging values out of `.env.production`, or set them in your host's environment. ## Client components A client component runs in the browser. When Next.js builds the bundle, it replaces each `process.env.NEXT_PUBLIC_*` reference that spells out the variable's name with its value, and `process.env.NODE_ENV` too. The client bundle loads the portable build, which reads nothing until you bind a source. Bind an object that spells out each name: ```ts title="lib/public-config.ts" Envapter.useSource( new PortableSource({ NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_BETA: process.env.NEXT_PUBLIC_BETA }) ); export const apiUrl = Envapter.getRequired('NEXT_PUBLIC_API_URL', Converters.Url); export const betaEnabled = Envapter.getAs('NEXT_PUBLIC_BETA', Converters.Boolean, false); ``` [`PortableSource`](/docs/sources#the-sources-envapt-ships) holds only the keys in that object. A variable you leave out reads as unset, so `getRequired` throws for it and `getAs` returns its fallback. When you add a `NEXT_PUBLIC_` variable, add it to the object too. `betaEnabled` is a `boolean`, where `process.env.NEXT_PUBLIC_BETA` holds the string `'false'` when the flag is off. `NODE_ENV` is in the object because `Envapter.environment` reads it from the source. Without it, `Envapter.environment` is `'development'` in the browser. Import `public-config.ts` only from files that start with `'use client'`, for the reason in [the next section](#keep-envapt-out-of-serverexternalpackages). If one component needs one public value, read it in a server component and pass it down as a prop, which keeps envapt out of the client bundle. ### Keep envapt out of serverExternalPackages Next.js also renders client components on the server, and it bundles a separate copy of envapt for that render. The `useSource` call in `public-config.ts` replaces the source only in that copy, so your server code keeps reading `process.env` and your `.env` files. If you list `envapt` in `serverExternalPackages`, both renders load one shared copy. The client's `useSource` call then replaces the source your server code reads. `next build` stops with a missing-variable error for server keys like `DATABASE_URL`. ## Edge routes A route that exports `runtime = 'edge'` loads the portable build. Bind `process.env` before the first read: ```ts title="app/api/geo/route.ts" export const runtime = 'edge'; Envapter.useSource(new PortableSource(process.env)); const geoApiUrl = Envapter.getRequired('GEO_API_URL', Converters.Url); export function GET() { return fetch(geoApiUrl); } ``` `getRequired` comes after `useSource`. If you swap the two lines, the read throws `NoSourceBound` (307). The read runs at module scope, so a missing `GEO_API_URL` stops `next build` the same way a server route does. Outside Next.js, [Vercel's Edge runtime](/docs/edge-runtimes#vercel-edge) binds the same way. ## Decorators Next.js compiles your TypeScript with SWC. Next.js turns on SWC's decorator support when `tsconfig.json` sets `experimentalDecorators`, and it compiles only legacy decorators. Set the flag, then import `@Envapt` from `envapt/legacy` and declare `static readonly` fields: ```json title="tsconfig.json" { "compilerOptions": { "experimentalDecorators": true } } ``` ```ts title="lib/AppConfig.ts" import { Converters } from 'envapt'; import { Envapt } from 'envapt/legacy'; export class AppConfig { @Envapt('DATABASE_URL', { converter: Converters.Url, required: true }) static readonly databaseUrl: URL; @Envapt('SESSION_TTL', { converter: Converters.Time, fallback: '15m' }) static readonly sessionTtl: number; } ``` `Converters` comes from `envapt`, and only `Envapt` comes from `envapt/legacy`. A decorated field reads its variable the first time you access it, so importing `AppConfig` in `register()` checks nothing. Read `AppConfig.databaseUrl` there to check it at startup. The [legacy decorators](/docs/decorators#legacy-decorators) section has the instance-field form. ### not implemented: ClassMember::AutoAccessor `next build` stops with this error on a modern `static accessor` field imported from `envapt`. In Next.js 16.4, Turbopack, the bundler `next build` uses by default, can't parse the `accessor` keyword. Switch the field to the `static readonly` form above. --- # SvelteKit, Astro, and Nuxt Run typed envapt config in a SvelteKit, Astro, or Nuxt server on Node, with startup checks. This page covers SvelteKit 3 with `adapter-node`, Astro 7 with `@astrojs/node`, and Nuxt 4's Node server. Each one runs your server code on Node, where envapt loads its [node build](/docs/compatibility#node-build-binds-automatically) and reads `process.env` and your `.env` files with no setup. An adapter for Workers or an edge runtime loads the portable build instead, which [Cloudflare Workers](/docs/workers) and [Edge runtimes](/docs/edge-runtimes) explain. Next.js has [its own page](/docs/nextjs). | | SvelteKit 3 | Astro 7 | Nuxt 4 | | --- | --- | --- | --- | | Build runs your server modules | yes | prerendered pages only | no | | `NODE_ENV` when the built server starts | unset | unset | unset | | Hook that runs before the first request | `init` in `hooks.server.ts` | none | a Nitro plugin | | `@Envapt` from `envapt` | build stops | first request throws | works | | `@Envapt` from `envapt/legacy` | works | works | works | Each framework also declares variables its own way, with SvelteKit's `defineEnvVars`, Astro's `astro:env`, and Nuxt's `runtimeConfig`. Use those for values the framework passes to its own APIs and to the browser. Use envapt when code outside the framework, such as a worker or a script, imports the same config module. It also gives you converters like [`Converters.Time`](/docs/converters) and the [file order](/docs/env-files#which-files-load) that follows `ENVIRONMENT=staging`. ## Start the server with NODE_ENV set With `NODE_ENV` unset, [`Envapter.environment`](/docs/environment) returns `'development'`, so envapt skips `.env.production`. Set it in the start command: ```sh NODE_ENV=production node build # SvelteKit with adapter-node NODE_ENV=production node dist/server/entry.mjs # Astro with @astrojs/node NODE_ENV=production node .output/server/index.mjs # Nuxt ``` Nuxt makes this easy to miss. Its build replaces each `process.env.NODE_ENV` in your server code with the string `"production"`, so a check in your own code says production. envapt reads `process.env` at runtime, where `NODE_ENV` is still unset. None of these servers copies your `.env` files into `process.env`. envapt reads the files itself, in its own order, and `ENVIRONMENT=staging` in the start command makes it read `.env.staging` too. ## SvelteKit Put your reads in one server module, so the `init` hook below can import the same file your routes do: ```ts title="src/lib/server/config.ts" import { Converters, Envapter } from 'envapt'; export const config = Envapter.getRequiredAll( { DATABASE_URL: Converters.Url, SESSION_TTL: Converters.Time }, 'camelCase' ); ``` [`getRequiredAll`](/docs/required-values) turns `SESSION_TTL=15m` into `config.sessionTtl`, the number `900000`. If `DATABASE_URL` and `SESSION_TTL` are both unset, it throws one `MissingEnvValue` error (305) that lists both. `vite build` runs your server modules during its analysis step. If a route imports `config` and a variable is unset on the build machine, the build stops: ```txt error during build: EnvaptError [305]: Missing required environment variables: DATABASE_URL. ``` Set the variables where you build. If your build machine can't have them, export a function that calls `getRequiredAll` and call it from your routes, the way [Next.js](/docs/nextjs#failed-to-collect-page-data) does. The server can still start with values that differ from the build machine's. To check them at startup, import `config` in the `init` hook: ```ts title="src/hooks.server.ts" import type { ServerInit } from '@sveltejs/kit/hooks'; export const init: ServerInit = async () => { await import('./lib/server/config'); }; ``` SvelteKit runs `init` before it serves a request. If `init` throws, the process exits with code 1 and never starts listening. SvelteKit's own `defineEnvVars` reads only `process.env`. If you use it alongside envapt, SvelteKit can't find a variable defined only in `.env`, and the server stops with `env_invalid`. envapt reads the same variable from the file. ### SyntaxError: Invalid or unexpected token `vite build` stops with this error if a class uses `@Envapt` from `envapt`. Vite 8 leaves those decorators uncompiled in the server bundle, and Node can't parse them. To get them compiled, set `experimentalDecorators` in `tsconfig.json` and switch to [`envapt/legacy`](/docs/decorators#legacy-decorators), which declares each field as `static readonly` in place of `static accessor`. The [Browser](/docs/browser#decorators) page has the same fix for Vite 8 in client code. ## Astro When the first request for a route arrives, Astro imports that route's module. If `config` is missing a variable, that route returns 500, but the rest of the site keeps working. Astro has no hook that runs when the server starts, and `src/middleware.ts` also loads on the first request. To catch a bad config right after a deploy, point your health check at a route that imports `config`. During `astro build`, `NODE_ENV` is `production`. If a page exports `prerender = true`, Astro renders it then, and the values it reads come from `.env.production` on the build machine. They stay in the HTML until you build again. Astro builds its server bundle with Vite 8 too. The build passes, but the first request to a route that imports a class using `@Envapt` from `envapt` throws `SyntaxError: Invalid or unexpected token`. Switch to `envapt/legacy` with `experimentalDecorators`, the same fix as for SvelteKit. ## Nuxt Nuxt's build doesn't run your server routes, so it passes even if variables are unset. To catch them when the server starts, add a Nitro plugin. Nitro is the server Nuxt builds, and it runs each file in `server/plugins` once at startup, before it listens: ```ts title="server/plugins/check-config.ts" import '../lib/config'; export default defineNitroPlugin(() => {}); ``` `server/lib/config.ts` holds the same `getRequiredAll` call as the SvelteKit sample. The import runs that call. If a variable is missing or doesn't convert, the plugin throws and the process exits with code 1. Nuxt's server build compiles `@Envapt` from `envapt`, so `static accessor` fields work without `experimentalDecorators`. --- # Quick Start Install envapt and read your first typed value on Node, Bun, Deno, Workers, or the browser. > **WARNING** > > envapt is ESM only. Node throws `ERR_PACKAGE_PATH_NOT_EXPORTED` on `require('envapt')`. In a CommonJS project, load it with `await import('envapt')` inside an async function. [Compatibility](/docs/compatibility) lists the runtime versions. ### Install ```package-install npm install envapt ``` ### Read a value On Node, Bun, and Deno, importing envapt binds `process.env` and your `.env` files, so there's nothing to set up. Workers and Vite give you config as an object, so on those and [other edge runtimes](/docs/edge-runtimes) the tab below binds it with [`useSource`](/docs/sources) before the first read. A read before that throws [`NoSourceBound`](/docs/sources#no-environment-source-is-bound). **Node, Bun, Deno** envapt loads the `.env` files from your working directory. This step reads `CACHE_TTL` from the file below. The startup check further down reads the other three keys. ```dotenv title=".env" PORT=8080 DATABASE_URL=postgres://app:secret@localhost:5432/app CACHE_TTL=10m ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com ``` ```ts title="src/config.ts" const cacheTtl = Envapter.getAs('CACHE_TTL', Converters.Time, '1m'); ``` [`getAs`](/docs/envapter#other-types-with-getas) turns `10m` into `600000` milliseconds. The `'1m'` fallback applies if `CACHE_TTL` is missing, empty, or isn't a duration. Coming from dotenv? [.env files on Node](/docs/env-files#coming-from-dotenv) has the `envapt/config` import that replaces `dotenv/config`. **Workers** A Worker's bindings arrive on the `env` object it imports from `cloudflare:workers`, so bind that object at the top of a config module, before the first read. ```ts title="src/config.ts" import { env } from 'cloudflare:workers'; import { Converters, Envapter, PortableSource } from 'envapt'; Envapter.useSource(new PortableSource(env)); const maintenanceMode = Envapter.getAs('MAINTENANCE_MODE', Converters.Boolean, false); ``` A `vars` entry in your Wrangler config can be a boolean or an object. `PortableSource` turns each value in `env` that isn't a string into JSON text, so a boolean `MAINTENANCE_MODE` arrives as `'true'` and `Converters.Boolean` reads it. Local dev and secrets are on the [Cloudflare Workers](/docs/workers) page. **Browser** A bundler writes your config into the client code at build time. Vite exposes that config as `import.meta.env`, so bind that object before the first read. ```ts title="src/config.ts" import { Converters, Envapter, PortableSource } from 'envapt'; Envapter.useSource(new PortableSource(import.meta.env)); const betaEnabled = Envapter.getAs('VITE_ENABLE_BETA', Converters.Boolean, false); ``` Vite puts every variable it exposes into the client bundle, so expose public values only. [Browser](/docs/browser) explains the `VITE_` prefix. [Reading values](/docs/envapter) covers the other readers. [Converters](/docs/converters) has the full set of converters, including URLs, durations, and arrays. To check a value against a schema from a library such as zod, valibot, or arktype, see [Validating with a schema](/docs/standard-schema). ### Check required config at startup A fallback hides a bad value. If the app can't run without a setting, call [`getRequiredAll`](/docs/required-values) for the group. It throws one error that lists every bad key: - `MissingEnvValue` lists the keys that are unset or empty. - `InvalidEnvValue` lists the keys that are set but can't convert. For a single setting, use [`getRequired`](/docs/required-values#getrequired-require-and-has). **Node, Bun, Deno** ```ts title="src/config.ts" export const config = Envapter.getRequiredAll( { PORT: Converters.Port, DATABASE_URL: Converters.Url, ALLOWED_ORIGINS: Converters.array() }, 'camelCase' ); config; ``` **Workers** Add this below the `useSource` line in the same module. ```ts title="src/config.ts" export const config = Envapter.getRequiredAll( { API_BASE_URL: Converters.Url, API_TOKEN: Converters.String }, 'camelCase' ); config; ``` **Browser** Add this below the `useSource` line in the same module. ```ts title="src/config.ts" export const config = Envapter.getRequiredAll( { VITE_API_URL: Converters.Url, VITE_STRIPE_PUBLISHABLE_KEY: Converters.String }, 'camelCase' ); config; ``` The result is typed from the object you pass. `Converters.Url` gives a `URL`, and `Converters.array()` gives a `string[]` split on commas. The `'camelCase'` argument turns `DATABASE_URL` into `config.databaseUrl`, and without it the keys stay as you wrote them. [Required values](/docs/required-values) describes the other casings and the error codes. Import `config` from your app's entry point. The module throws when it loads, so a missing or bad value stops the app at startup. > **WARNING** > > If a value can't convert and nothing catches the error, Node prints the error's `issues`, and an issue can quote a secret such as the password in `DATABASE_URL`. [Required values](/docs/required-values#reading-the-error-without-leaking-values) shows a `catch` block that logs only the keys. Prefer class fields? The [`@Envapt` decorator](/docs/decorators) declares the same settings on a class. --- # Required values Stop the app at startup when a variable is missing or malformed, with every bad key in one error. If a value is wrong, a plain read often returns something and the app keeps running. `Envapter.getAs('DATABASE_URL', Converters.Url)` returns `undefined` for a missing variable, so the app fails later, in whatever request touches the database first. A *required read* throws on a bad value, while a plain read of a token or function returns the fallback or `undefined`. `getRequired`, `getRequiredAll`, and a decorated field with [`required: true`](/docs/decorators#required-fields) are required reads. `require` is one too, but it only checks that each value is present. If the read runs at boot, a bad deploy stops the app before it serves anything. With one required read per variable, only the first bad value threw. You fixed it, deployed again, and hit the next one. `getRequiredAll` reads the whole group and reports every missing key in one error, or every malformed key in one error: ```ts export const config = Envapter.getRequiredAll( { DATABASE_URL: Converters.Url, PORT: Converters.Port, CACHE_TTL: Converters.Time, ALLOWED_ORIGINS: Converters.array({ of: Converters.Url }), LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']) }, 'camelCase' ); export const port = config.port; ``` The object you pass is the *spec*. Each key is an env name, and each value is a [token](/docs/converters), an `array()` converter, your own converter function, or a [schema](/docs/standard-schema) like the `LOG_LEVEL` enum. A spec key is a single env name. The returned record holds every value already converted. None of the fields read with a token or function can be `undefined`, which is why `port` is a `number`. A schema field holds whatever its schema returns. The optional second argument renames the record's keys. envapt splits each name on underscores: | Casing | `DATABASE_URL` becomes | | --- | --- | | none | `DATABASE_URL` | | `'camelCase'` | `databaseUrl` | | `'PascalCase'` | `DatabaseUrl` | | `'kebab-case'` | `database-url` | It also lowercases each word after its first letter. `MY_KEY` becomes `myKey` under `'camelCase'`, while a mixed-case name like `myKey` has no underscore and becomes `mykey`. ## Missing and invalid are different failures `MissingEnvValue` (305) means the variable is unset or empty. `InvalidEnvValue` (210) means it's set but the converter or schema rejected it. The two codes let a `catch` tell them apart. `getRequiredAll` checks in a fixed order: ```txt getRequiredAll(spec) ├─ a bad converter in the spec ... throws first (a mistyped token is 202) ├─ any key missing ............... one MissingEnvValue, every missing key └─ every key present └─ any failed ................. one InvalidEnvValue, every failed key ``` The order has a cost. If one key is missing and another is malformed, you see only the missing key. After you set it, the next run reports the malformed one. A run never mixes the two codes. [Strict mode](#strict-mode) adds one exception. If a value holds an unresolved `${VAR}`, `require` and `getRequiredAll` throw `MissingEnvValue` for that key right away. Keys that were missing earlier go unreported, and later keys go unchecked. If `DATABASE_URL`, `PORT`, and `LOG_LEVEL` are unset, the example above throws: ```txt EnvaptError [305]: Missing required environment variables: DATABASE_URL, PORT, LOG_LEVEL. ``` If every variable is set but three values are malformed, among them `ALLOWED_ORIGINS=https://app.example.com,example.com`, it throws: ```txt EnvaptError [210]: Validation failed for DATABASE_URL, PORT, ALLOWED_ORIGINS. ``` ## Reading the error without leaking values The `MissingEnvValue` and `InvalidEnvValue` messages list only keys. An `InvalidEnvValue` also carries `error.issues`, an array of *issues* in the [Standard Schema](/docs/standard-schema) shape. Each has a `message` and a `path` that starts with the env key. For the 210 run above, `issues` holds: ```json [ { "message": "could not be converted", "path": ["DATABASE_URL"] }, { "message": "could not be converted", "path": ["PORT"] }, { "message": "Element \"example.com\" at index 1 could not be converted to url.", "path": ["ALLOWED_ORIGINS"] } ] ``` The third issue quotes `example.com`. An issue message can quote the value it rejected, and a value can be a secret, such as the password inside `DATABASE_URL`. The error text leaves the messages out for that reason. Node still prints an uncaught error with its properties, so an uncaught `InvalidEnvValue` prints `issues` and those messages. To keep them out of your logs, catch the error, log its message, and exit: ```ts function loadConfig() { try { return Envapter.getRequiredAll({ DATABASE_URL: Converters.Url, PORT: Converters.Port }); } catch (error) { if (error instanceof EnvaptError) { console.error(error.message); process.exit(1); } throw error; } } ``` `error.message` is the line below, which carries every key you need. ```txt Validation failed for DATABASE_URL, PORT. ``` Log `error.issues` only when you know your converters and schemas never quote a value. The sample exits with `process.exit(1)` on Node, Bun, and Deno. ## getRequired, require, and has | Reader | Returns | When the value is missing | When it can't convert | | --- | --- | --- | --- | | `getRequired(key, converter)` | the converted value | throws `MissingEnvValue` | throws `InvalidEnvValue` | | `getRequiredAll(spec, casing?)` | a record of values | one `MissingEnvValue` for every missing key | one `InvalidEnvValue` for every bad key | | `require(key, ...keys)` | nothing | one `MissingEnvValue` for every missing key | not checked | | `has(key)` | a boolean | `false` | not checked | `getRequired` takes the same converters and schemas as `getRequiredAll`. Its key can also be an [ordered key list](/docs/envapter#ordered-keys), which a spec key can't. For a plain string, pass `Converters.String`. A bad converter throws before `MissingEnvValue` does. `require` converts nothing, so use it for values that other code reads later, such as a decorated class. Each argument can be an ordered key list too, and an argument passes if any of its keys has a value: ```ts Envapter.require('API_KEY', 'SESSION_SECRET', ['READONLY_URL', 'DATABASE_URL']); ``` This passes when `API_KEY` and `SESSION_SECRET` have values and at least one of `READONLY_URL` and `DATABASE_URL` does. `has` takes an ordered key list as well. It treats an unset or empty value as missing, and a whitespace-only value too when strict mode is on, the same as a required read. If you call `has` before `getRequired`, the value is optional but still checked when it is set: ```ts export function sentryDsn(): URL | undefined { if (!Envapter.has('SENTRY_DSN')) return undefined; return Envapter.getRequired('SENTRY_DSN', Converters.Url); } ``` `sentryDsn` returns `undefined` for an unset `SENTRY_DSN` but throws for a malformed one. A `getAs` would return its fallback, or `undefined`, and hide the typo. ## Where to put the check Put `getRequiredAll` at the top level of one config module that your entry file imports first. Module code runs at import, so the throw happens before the app serves anything. Every other file reads from the exported `config` record. The cost is that every test importing that module needs the variables set first. Set them in a [setup file](/docs/testing#reads-at-module-scope-run-before-the-test) that runs before the import. A library, or a CLI whose `--help` must work without config, should read inside a function and skip this. On Workers, the browser, and edge runtimes, call [`useSource`](/docs/sources) before the read, in the same module, because a read before it throws `NoSourceBound`. A decorated field with `required: true` reads the first time something accesses it, so a missing value only throws when that happens. Read the field once at startup to catch it earlier. ## Strict mode A variable counts as missing when it's unset or empty. Strict mode adds whitespace-only values to that rule and changes the other behaviors in the table below. It's off by default and applies to every read in the process. Without it, a value of only spaces counts as set. That happens when a CI secret expands to spaces, or when a `.env` file quotes the value, as in `API_KEY=" "`. `Envapter.require('API_KEY')` passes, and `getRequired` with `Converters.String` returns the spaces. Strict mode makes both throw `MissingEnvValue`. ```ts Envapter.strict = true; ``` When you set it, envapt rebuilds the [cache](/docs/sources#when-values-are-read) from the bound source. On Workers, the browser, and edge runtimes, you can set it before `useSource` too, and `useSource` applies it. **What strict mode changes, case by case** | Behavior | Strict off | Strict on | | --- | --- | --- | | A whitespace-only value | counts as set and reaches the converter. `Converters.Number` reads it as `0`, and `Converters.Port` can't convert it | counts as missing, so readers use the fallback and required reads throw `MissingEnvValue` | | A `${VAR}` whose variable is missing | stays in the text as `${VAR}` | the readers that expand templates throw `MissingEnvValue`. `has` returns `false` for that key, and `getRaw` returns the text | | An empty array element, like a trailing comma | skipped | a read with `Converters.array` throws `EmptyArrayElement` (207), and a required read reports it as an issue | Strict mode also applies to [templates](/docs/templates) and the `resolve` tagged template. An ordered key list skips a whitespace-only key and uses the next one. Every later read follows the new rule. A value you already read into a variable keeps its old result, so set it before those reads. With strict mode on, a plain reader still returns the fallback for an unset or whitespace-only value. It throws for an unresolved `${VAR}` in any reader that expands templates, and for an empty array element in any array read. Turn it on if your values come from files with `${VAR}` templates or from CI secrets, because a blank value there is probably a mistake. The cost is that a stray trailing comma in a deployed `.env` makes a startup read throw `EmptyArrayElement`. Leave it off if a whitespace-only value is meaningful in your app. --- # Secret stores Read secrets from a secrets manager as typed values, through a wrapper CLI or fetched at startup. You've copied a database password out of a secrets manager and into a `.env` file or a host's settings page before. The next rotation changed the store, and your copy went stale. envapt reads secrets from whichever [source](/docs/sources) you bind, so you never write a secret into a file. | Route | How the values arrive | What you write | Cost | | --- | --- | --- | --- | | Wrapper CLI | the CLI sets real environment variables, then starts your process | nothing on Node, Bun, and Deno, because [`FileSource`](/docs/sources#the-sources-envapt-ships) reads `process.env` | a rotation needs a process restart | | SDK or HTTP fetch | your code fetches at boot | a fetch function and one `useSource` call | a bootstrap credential (the token your fetch logs in with), and a network call that can stop startup | ## Wrapper CLIs A wrapper CLI resolves the secrets, then starts your process with them set as environment variables. On Node, Bun, and Deno the app reads them like any other variable. ```bash op run --env-file=secrets.env -- node dist/server.js # 1Password doppler run -- node dist/server.js # Doppler infisical run -- node dist/server.js # Infisical bws run -- node dist/server.js # Bitwarden ``` With 1Password, `secrets.env` holds `op://` references and no secret value, so you can commit it. ```dotenv title="secrets.env" DB_PASSWORD=op://prod/db/password API_BASE_URL=op://prod/api/base-url ``` ```ts const dbPassword = Envapter.getRequired('DB_PASSWORD', Converters.String); const apiBase = Envapter.getRequired('API_BASE_URL', Converters.Url); ``` With `op run`, `DB_PASSWORD` and `API_BASE_URL` already hold the resolved values when the process starts, so the app never calls `useSource`. ## Fetch at boot, then bind Reduce what your SDK or HTTP call returns to a flat `Record`. A nested value would reach envapt as JSON text that only `Converters.Json` parses. Then pass `FileSource` and the secrets to [`merge`](/docs/sources#combining-sources) in one [`useSource`](/docs/sources) call, with the secrets second so they win a shared key. ```ts import { Converters, Envapter, FileSource, PortableSource, merge } from 'envapt'; import { fetchSecrets } from './secrets'; export async function bindSecrets(): Promise { Envapter.useSource(merge(new FileSource(), new PortableSource(await fetchSecrets()))); } await bindSecrets(); export const config = Envapter.getRequiredAll( { DATABASE_URL: Converters.Url, DB_POOL_SIZE: Converters.Integer, CACHE_TTL: Converters.Time }, 'camelCase' ); ``` `bindSecrets` awaits the fetch before it calls `useSource`, since envapt reads a source synchronously and can't wait for a fetch. With [`merge`](/docs/sources#combining-sources), a key from the fetched secrets overrides the same key in `process.env`. It also wins over the `.env` files unless you turn on `override` in the [file options](/docs/env-files#file-options). Any key the store omits still comes from `process.env` and the files. On Node, if a read runs before `bindSecrets` finishes, it gets values from `process.env` and the files without the secrets, because the node build bound `FileSource` on import. That read doesn't throw `NoSourceBound`, so nothing tells you it ran too early. Import `config.ts` before any module that reads config, as [when values are read](/docs/sources#when-values-are-read) explains. > **WARNING** > > If you bind only a `PortableSource` on Node, it replaces `FileSource`. The app reads only the secrets. `process.env` and the [`.env` cascade](/docs/env-files) are gone. `Envapter.environment` returns `'development'` unless the secrets include one of the [detection keys](/docs/environment#detecting-the-environment). Setting `baseDir` or `envPaths`, or calling `configureProfiles`, throws [`FileApiUnsupported`](/docs/errors). Merge with `FileSource` to keep `process.env`, the cascade, environment detection, and the file APIs. On Workers, there is no `FileSource`. Merge two [`PortableSource`](/docs/sources#the-sources-envapt-ships) objects instead, one built from the [Worker's `env`](/docs/workers) and one from the fetched secrets. Do it inside the request handler, because Cloudflare throws if a Worker calls `fetch()` while its top-level code runs. Each bind rebuilds envapt's cache, as [Reading fresh values](/docs/workers#reading-fresh-values-on-every-request) explains. The secrets you fetched are a snapshot from boot. To refresh them, call `bindSecrets()` again. A value you already read keeps its old result, so `config` above stays as it was. [Decorated fields](/docs/decorators) read the new secrets on their next access, because `useSource` clears their cached values. ### The fetch, per store Pin and verify every SDK against its own docs before shipping, since each snippet matches the SDK as it was when this page was written. The snippets read their own settings through envapt too. Those reads run before `useSource`, so they come from `process.env` and the files. If a key is missing, `getRequired` throws `MissingEnvValue` (305) at boot. **AWS** ```ts import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; import { Converters, Envapter } from 'envapt'; export async function fetchSecrets(): Promise> { const client = new SecretsManagerClient(); const secretId = Envapter.getRequired('SECRET_ID', Converters.String); const { SecretString } = await client.send(new GetSecretValueCommand({ SecretId: secretId })); if (!SecretString) throw new Error(`${secretId} has no SecretString`); return JSON.parse(SecretString) as Record; } ``` Store the secret as one flat JSON object with string values, since the cast hides any other type. The client resolves credentials through the standard AWS credential chain. **Vault** ```ts import vault from 'node-vault'; import { Converters, Envapter } from 'envapt'; export async function fetchSecrets(): Promise> { const client = vault({ endpoint: Envapter.getRequired('VAULT_ADDR', Converters.String) }); await client.approleLogin({ role_id: Envapter.getRequired('VAULT_ROLE_ID', Converters.String), secret_id: Envapter.getRequired('VAULT_SECRET_ID', Converters.String) }); const { data } = await client.read('secret/data/myapp'); return data.data as Record; } ``` HashiCorp has no official Node client. `node-vault` is a community library. KV v2 puts the payload at `data.data`, and the read path carries a `data/` segment (`secret/data/myapp`). **1Password** ```ts import sdk from '@1password/sdk'; import { Converters, Envapter } from 'envapt'; const REFS = { DB_PASSWORD: 'op://prod/db/password', API_KEY: 'op://prod/api/key' }; export async function fetchSecrets(): Promise> { const client = await sdk.createClient({ auth: Envapter.getRequired('OP_SERVICE_ACCOUNT_TOKEN', Converters.String), integrationName: 'my-app', integrationVersion: 'v1.0.0' }); const secrets: Record = {}; for (const [key, ref] of Object.entries(REFS)) { secrets[key] = await client.secrets.resolve(ref); } return secrets; } ``` `OP_SERVICE_ACCOUNT_TOKEN` is the bootstrap credential, a service-account token. The SDK is pre-1.0, so its API can change between releases. Use the `op run` route from the wrapper CLI section to skip the dependency. **Doppler** ```ts import { Converters, Envapter } from 'envapt'; export async function fetchSecrets(): Promise> { const url = new URL('https://api.doppler.com/v3/configs/config/secrets/download'); url.searchParams.set('format', 'json'); const project = Envapter.get('DOPPLER_PROJECT'); if (project) url.searchParams.set('project', project); const config = Envapter.get('DOPPLER_CONFIG'); if (config) url.searchParams.set('config', config); const token = Envapter.getRequired('DOPPLER_TOKEN', Converters.String); const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); if (!response.ok) throw new Error(`Doppler answered ${response.status}`); return (await response.json()) as Record; } ``` A service token already identifies its project and config, so `DOPPLER_PROJECT` and `DOPPLER_CONFIG` are optional here. A personal token needs both. Check the auth scheme against Doppler's API reference, since older docs used HTTP Basic with the token as the username and an empty password. Doppler returns a 401 for a wrong scheme. With `doppler run` you don't need this code. **Infisical** ```ts import { InfisicalSDK } from '@infisical/sdk'; import { Converters, Envapter } from 'envapt'; export async function fetchSecrets(): Promise> { const client = new InfisicalSDK(); await client.auth().universalAuth.login({ clientId: Envapter.getRequired('INFISICAL_CLIENT_ID', Converters.String), clientSecret: Envapter.getRequired('INFISICAL_CLIENT_SECRET', Converters.String) }); const { secrets } = await client.secrets().listSecrets({ projectId: Envapter.getRequired('INFISICAL_PROJECT_ID', Converters.String), environment: Envapter.get('INFISICAL_ENVIRONMENT', 'prod') }); return Object.fromEntries(secrets.map((secret) => [secret.secretKey, secret.secretValue])); } ``` Each secret has a `secretKey` and a `secretValue`. If the identity can't read values, Infisical masks them and sets `secretValueHidden` on each secret. Use `@infisical/sdk` v5 or later, since `infisical-node` is deprecated. **Bitwarden** ```ts import { BitwardenClient, DeviceType, LogLevel } from '@bitwarden/sdk-napi'; import { Converters, Envapter } from 'envapt'; export async function fetchSecrets(): Promise> { const client = new BitwardenClient( { apiUrl: 'https://api.bitwarden.com', identityUrl: 'https://identity.bitwarden.com', deviceType: DeviceType.SDK }, LogLevel.Info ); await client.auth().loginAccessToken(Envapter.getRequired('BW_ACCESS_TOKEN', Converters.String)); const { data } = await client.secrets().list(Envapter.getRequired('BW_ORG_ID', Converters.String)); const full = await client.secrets().getByIds(data.map((secret) => secret.id)); return Object.fromEntries(full.data.map((secret) => [secret.key, secret.value])); } ``` Fetch the values with `getByIds`, since `list()` returns identifiers only. The package ships native binaries and runs on Node. Bitwarden's npm description still calls it a beta release. --- # Sources Bind your platform's config object with useSource so typed reads work on Workers and the browser. Node, Bun, and Deno keep config in a global, `process.env`, so envapt can read it the moment you import it. Other platforms, such as a Cloudflare Worker, a browser bundle, or an edge function, give your code an object instead (the Worker `env`, Vite's `import.meta.env`). envapt can't reach that object, so you pass it to `useSource`. A **source** is what envapt reads variables from: an object with a `readVars()` method, a function that looks up one key, or the result of `merge`. To **bind** one, call `Envapter.useSource(source)` before your first read. The cost is one line per app. The binding is process-wide, so every read after it, in every module, uses that source. | Build | Runs on | At import | `.env` file APIs | | --- | --- | --- | --- | | **Node build** | Node, Bun, Deno | binds a `FileSource` | work, see [.env files on Node](/docs/env-files) | | **Portable build** | [Workers](/docs/workers), [edge runtimes](/docs/edge-runtimes), the [browser](/docs/browser), React Native | binds nothing | warn once and do nothing, or throw when `fileApiMode` is `'throw'` | The `exports` conditions in `package.json` pick the build, and [Compatibility](/docs/compatibility) lists which build each runtime gets. The portable build starts with an unbound source whose `readVars()` throws, because envapt has no object to read on those platforms until you pass one. On the node build, call `useSource` only to replace the default source. On the portable build, call it once, in the module that exports your values. ## No environment source is bound A read on the portable build before `useSource` throws `EnvaptError` with code `NoSourceBound` (307). Reading `Envapter.environment` counts as a read. Setting `Envapter.strict` or pinning `Envapter.environment` before binding works, and `useSource` applies the setting when it builds the [cache](#when-values-are-read). Imports run before the module body. If a module reads at load time, it can run before your `useSource` call. Put the bind in the module that exports your values, and import that module wherever you read. ## The sources envapt ships | Source | Reads | Files | Where | | --- | --- | --- | --- | | `FileSource` | a copy of `process.env`, plus the [`.env` cascade](/docs/env-files#which-files-load) (the shell wins by default) | yes | node build only, bound for you | | `PortableSource` | the object you pass | no | every runtime | `FileSource` is exported by the node build only, so the samples below that import it are for Node, Bun, and Deno. `PortableSource` copies the object when you construct it, so a later change to the object leaves envapt's copy as it was. To change a value, bind a new `PortableSource`. It JSON-stringifies any value that isn't a string, so a Worker `vars` entry holding an object reaches [`Converters.Json`](/docs/converters#json) as JSON text. A `null` becomes the string `'null'`, and a function or `undefined` is skipped. A `BigInt` or a circular object makes the constructor throw a `TypeError`. ```ts declare const env: Record; // the object the platform hands you Envapter.useSource(new PortableSource(env)); const upstream = Envapter.getAs('UPSTREAM_URL', Converters.Url); const cacheTtl = Envapter.getAs('CACHE_TTL', Converters.Time, '5m'); ``` `upstream` is `URL | undefined` because the call has no fallback. `cacheTtl` is a `number` of milliseconds because `Converters.Time` converts the fallback `'5m'`. ## Any object can be a source A source is anything with a `readVars()` method that returns the variables as strings. That covers a secrets payload or an RPC response, with no new class to write. To fetch such a payload from a secrets manager, see [Secret stores](/docs/secret-stores). ```ts declare const secrets: Record; // fetched from your secrets manager const source: Source = { readVars: () => secrets }; Envapter.useSource(source); const apiBase = Envapter.getAs('API_BASE_URL', Converters.Url); const poolSize = Envapter.getAs('DB_POOL_SIZE', Converters.Integer, 10); ``` | Name | Type | Default | Description | | --------------------- | --------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------- | | `readVars` (required) | `() => Record` | | The variables envapt copies when it builds its cache. | | `readVar` | `(key: string) => string \| undefined` | | Looks up one key that readVars() did not return. | | `supportsFiles` | `boolean` | | Set true, together with the four file members below, to make envapt load the .env cascade. Default false. | | `readFile` | `(path: string, encoding: string) => string \| undefined` | | With supportsFiles: true. Returns the file text, or undefined when it is missing. | | `resolvePath` | `(baseDir: string, candidate: string) => string` | | With supportsFiles: true. Joins a relative .env path onto baseDir. | | `normalizeBaseDir` | `(value: string \| URL) => string` | | With supportsFiles: true. Turns a directory path or module URL into a directory path. | | `writeVars` | `(vars: Record) => void` | | With supportsFiles: true. Receives the keys that syncProcessEnv writes out. | Only `readVars` is required. Set `supportsFiles: true` when your runtime has files envapt should read, and supply `readFile`, `resolvePath`, `normalizeBaseDir`, and `writeVars`. Of the sources envapt ships, `FileSource` sets it. ### A function can be a source Some stores return one key at a time and can't list their keys. Pass a `(key) => string | undefined` function to `useSource` to have envapt call it the first time you read each key. A function is shorthand for a source whose `readVars()` returns `{}` and whose `readVar` is your function. ```ts declare const store: { get(key: string): string | undefined }; // a keyed config store Envapter.useSource((key) => store.get(key)); const region = Envapter.getRequired('REGION', Converters.String); ``` If the store has no `REGION` on the first read, `getRequired` throws `MissingEnvValue` (305). Adding `REGION` to the store afterwards changes nothing until the cache rebuilds, because envapt stores the miss in its cache (see [When values are read](#when-values-are-read)). ## Combining sources `useSource` binds one source and replaces the previous binding. `merge` combines several sources into one. When two sources return the same key from `readVars()`, the later source wins. ### Add to Node's default source Node binds a `FileSource` when you import envapt. Use `Envapter.source` to keep that source while adding fetched secrets, without copying the secrets into `process.env`. ```ts declare const secrets: Record; // fetched and awaited Envapter.useSource(merge(Envapter.source, new PortableSource(secrets))); const dbUrl = Envapter.getAs('DATABASE_URL', Converters.Url); ``` Here `Envapter.source` keeps the `.env` cascade. With the default [file options](/docs/env-files#file-options), a `DATABASE_URL` in `secrets` wins over the same key in `process.env` or an `.env` file. Setting `override: true` makes an `.env` value win instead. ### Bind several portable sources Workers, browsers, and edge runtimes start without a bound source. Pass their config object and your secrets together when you bind: ```ts declare const env: Record; // the Worker env declare const secrets: Record; // fetched and awaited Envapter.useSource(merge(new PortableSource(env), new PortableSource(secrets))); const apiKey = Envapter.getRequired('API_KEY', Converters.String); ``` The `secrets` source wins when both objects contain `API_KEY`. If you have already bound a source, pass `Envapter.source` as the first member to extend it. Bind a source before doing that on a portable build. ### Add a key reader A `(key) => string | undefined` function can be a member too. It fills keys missing from every member's `readVars()` result, regardless of where it appears in the list. ```ts declare const store: { get(key: string): string | undefined }; Envapter.useSource(merge(new FileSource(), (key) => store.get(key))); const redisUrl = Envapter.getAs('REDIS_URL', Converters.Url); ``` This example creates a fresh `FileSource` explicitly. `FileSource` is available on the node build only. Furthermore, `merge` throws `InvalidMergedSource` (308) if you pass no members or more than one filesystem-backed source, because the `.env` cascade needs a single file source. ## When values are read If a test setup sets `process.env.API_URL` after envapt was imported, the next read still returns the old value. envapt keeps a **cache**, the variables it builds from the bound source and holds until the next rebuild. If the cache lacks a key, envapt calls the source's `readVar` once for that key. Binding builds the cache, and each read goes through it: ```txt Envapter.useSource(source), or importing envapt on Node ├─ source.readVars() .......... copied into the cache └─ .env cascade ............... loaded only when supportsFiles is true Envapter.getAs('DATABASE_URL', Converters.Url) ├─ cache lookup .................. or source.readVar('DATABASE_URL') when the cache lacks it ├─ ${VAR} expansion .............. happens on read, see Templates └─ converter or schema ........... gives a URL, or the fallback ``` These rules decide what the cache holds. 1. **The cache builds when you bind.** `useSource` builds it when you call it, and on the node build importing envapt does the same. The `.env` files are read then. 2. **`source.readVars()` runs once per cache build.** `FileSource` copies `process.env` at that point, so if code sets `process.env.FEATURE_FLAGS = 'on'` afterwards, the cache keeps the old value. To make envapt read such a change, bind again with `Envapter.useSource(new FileSource())`. 3. **Some calls rebuild the cache.** The rebuild runs inside the call, so a failing rebuild throws at the assignment. **Every call that rebuilds the cache** | Call that rebuilds the cache | On the portable build | | --- | --- | | `Envapter.useSource(source)` | works | | [`Envapter.strict = true`](/docs/required-values#strict-mode) | works. Before a source is bound, it stores the setting and the bind rebuilds | | [`Envapter.environment = 'staging'`](/docs/environment#pinning-the-environment) | works. Before a source is bound, it stores the pin and the bind rebuilds | | `Envapter.envPaths`, `baseDir`, `envFileOptions` ([file APIs](/docs/env-files)) | warn once, do nothing | | `Envapter.configureProfiles(...)`, `resetPaths()` ([file APIs](/docs/env-files)) | warn once, do nothing | [Compatibility](/docs/compatibility#file-apis-and-fileapimode) covers when the file APIs throw `FileApiUnsupported` (306) in place of the warning. On the node build, if you bind a source that doesn't read files, `envPaths`, `baseDir`, and `configureProfiles` throw `FileApiUnsupported`. `envFileOptions` and `resetPaths` still rebuild the cache. A rebuild also clears the converted values of [decorated fields](/docs/decorators), because envapt stores them in this cache. After a rebuild, envapt converts the value again the next time you access a field you already read. --- # Validating with a schema Check a value against a zod, valibot, or arktype schema and get the schema's output type back. A converter that can't convert returns `undefined`, with no reason attached. A rule like "an integer from 1 to 100" became an `if` inside your function, so a `DATABASE_POOL_SIZE` outside the range returned the fallback without saying which rule failed. A schema carries the rule itself. Pass one to [`getAs`](/docs/envapter#other-types-with-getas) to have envapt validate the value, return the schema's output type, and throw with the schema's own issues if the value fails. An issue is one `{ message, path }` entry, and envapt starts every `path` with the env key. [`getRequired`, `getRequiredAll`](/docs/required-values), and the decorator's `schema` option take a schema too. Zod, valibot, arktype, and others implement [Standard Schema](https://standardschema.dev), a shared interface under the key `~standard`. envapt reads that interface and imports none of them, so the validator is the only package you install. If a token like `Converters.Port` already covers the rule, you don't need a schema. **zod** ```ts const poolSize = Envapter.getAs('DATABASE_POOL_SIZE', z.coerce.number().int().min(1).max(100)); ``` **valibot** ```ts import * as v from 'valibot'; import { Envapter } from 'envapt'; const poolSize = Envapter.getAs( 'DATABASE_POOL_SIZE', v.pipe(v.string(), v.digits(), v.transform(Number), v.minValue(1), v.maxValue(100)) ); ``` **arktype** ```ts import { type } from 'arktype'; import { Envapter } from 'envapt'; const poolSize = Envapter.getAs('DATABASE_POOL_SIZE', type('string.integer.parse').to('1 <= number.integer <= 100')); ``` `poolSize` is `number | undefined` because an unset `DATABASE_POOL_SIZE` returns `undefined`. A fallback as the third argument removes that case. Your schema receives the value as a string, so each one turns it into a number (`z.coerce.number()`, `v.transform(Number)`, `string.integer.parse`). ## The schema must be synchronous Every reader returns its value directly, and a decorated field is a getter. Neither can wait on a Promise. If a schema's `validate` returns only a Promise, TypeScript rejects the call. If you bypass the type check, the read throws `InvalidUserDefinedConfig` (302). The cost is that a check needing I/O, like asking a database whether a tenant id exists, can't run in the schema. Run that check in your own code once config has loaded. ```ts const tenantRule = { '~standard': { version: 1 as const, vendor: 'app', validate: async (value: unknown) => ({ value: String(value) }) } }; const tenantId = Envapter.getAs('TENANT_ID', tenantRule); ``` `tenantRule`'s `validate` is `async`, so it returns a Promise. The last line fails to compile. Zod types `validate` as returning a result or a Promise of a result, so that type passes the check and the failure is the 302 when the read runs. An async `refine` throws 302. A `.transform` that throws does too, because zod returns a rejected Promise for the throw. The 302 doesn't carry your original error. Set `Envapter.debug = 'warn'` to log the error the schema threw. ## Fallbacks skip the schema A missing or empty value returns the fallback, or `undefined` without one. The schema runs only on a value that is set, so the fallback never goes through it. A default on the schema, like zod's `.default()`, never applies either. TypeScript checks the fallback's type against the schema's output, but nothing checks the value at runtime. A token's fallback is different, because envapt checks it against the [token](/docs/converters). ```ts const poolSize = Envapter.getAs('DATABASE_POOL_SIZE', z.coerce.number().int().min(1).max(100), 500); ``` `500` is above the schema's `max(100)`, yet an unset `DATABASE_POOL_SIZE` returns it, so write a fallback that already passes. If a value is set and fails the schema, the read throws `SchemaValidationFailed` (208), even when you pass a fallback. ## What throws | `getAs` with a schema, when | Code | Carries | | --- | --- | --- | | the schema reports issues | [`SchemaValidationFailed`](/docs/errors) (208) | the issues | | the schema's `validate` throws | [`SchemaThrew`](/docs/errors) (209) | the original error on `.cause` | | `validate` returns a Promise | [`InvalidUserDefinedConfig`](/docs/errors) (302) | a message that the schema must be synchronous | A 208 message lists only the key, because an issue message can quote the value. Valibot's message reads `Received "abc"`, and arktype's ends in `(was "abc")`. A 209 message includes the text of the error your schema threw, which can quote the value too. [Required values](/docs/required-values#reading-the-error-without-leaking-values) shows how to log a failure without the values. A `${VAR}` [template](/docs/templates) expands before validation, so the schema receives the expanded value. To require the value, or to check a group of variables in one call, pass the schema to `getRequired` or `getRequiredAll`. [Required values](/docs/required-values) covers both. In `getRequired`, `getRequiredAll`, and a decorator with `required: true`, a failing schema throws `InvalidEnvValue` (210), and so does a schema whose `validate` throws, with the message as an issue. A schema that returns a Promise still throws 302. ## In a decorator [`@Envapt`](/docs/decorators) takes the schema through its `schema` option. Its `converter` option already accepts functions, and an arktype type is a function, so a schema in `converter` would look like a custom converter. envapt checks for a schema there and throws. | If you | envapt throws | | --- | --- | | put a schema in `converter` | [`InvalidUserDefinedConfig`](/docs/errors) (302) | | set `converter` and `schema` together | [`InvalidUserDefinedConfig`](/docs/errors) (302) | | pass a schema as `Converters.array({ of })`, in a decorator or any reader | [`InvalidArrayConverterType`](/docs/errors) (201) | | set `required: true` and the schema rejects the value | [`InvalidEnvValue`](/docs/errors) (210) | ```ts class Config extends Envapter { @Envapt('LOG_LEVEL', { schema: z.enum(['debug', 'info', 'warn', 'error']), fallback: 'info' }) static accessor logLevel: 'debug' | 'info' | 'warn' | 'error'; } ``` ## Any conformant validator, or none Any synchronous validator with a `~standard` interface works, including one you write by hand. A team with its own validation rules can write one by hand, as in the sample below, and get the list of issues that a converter can't produce. Annotate the object as `StandardSchemaV1` to give the read the output type. ```ts const positiveInt: StandardSchemaV1 = { '~standard': { version: 1, vendor: 'app', validate(value) { const n = Number(value); if (Number.isInteger(n) && n > 0) return { value: n }; return { issues: [{ message: 'must be a positive integer' }] }; } } }; const maxConnections = Envapter.getAs('MAX_CONNECTIONS', positiveInt, 10); ``` `validate` returns `{ value }` on success and `{ issues }` on failure. `maxConnections` is a `number` because the schema's output is a `number`. --- # Templates Build one variable from others by reference, or assemble strings with resolve(). A database URL needs a host and a port, and each can come from a different place, such as a default in `.env` and an override from your platform. Without references, every place that sets one of them has to write out the whole URL again. With `${VAR}` references in the URL, [`Envapter`](/docs/envapter) joins whichever values are set when you read it. To build a string from several keys in your code, use the [`resolve`](#the-resolve-tagged-template) tagged template. ```dotenv title=".env" DB_HOST=localhost DB_PORT=5432 DATABASE_URL=postgres://${DB_HOST}:${DB_PORT}/app ``` ```ts const url = Envapter.get('DATABASE_URL'); // "postgres://localhost:5432/app" // with DB_HOST=db.internal set by your platform: "postgres://db.internal:5432/app" ``` `get('DATABASE_URL')` expands both references. Every other reader does too, except `getRaw`, which returns [the raw string](/docs/envapter#the-raw-string) as written. A converter or schema receives the expanded text. A decorated field expands once, on its first access, because it caches the result ([Decorators](/docs/decorators#one-value-per-field)). Expansion works on any [source](/docs/sources), including one you bind with `useSource`. ## Nested references A variable you reference can hold references of its own. envapt expands them until it reaches a cycle or a missing variable. ```dotenv PROTOCOL=https HOST=api.example.com BASE_URL=${PROTOCOL}://${HOST} HEALTHCHECK=${BASE_URL}/health ``` Reading `HEALTHCHECK` returns `https://api.example.com/health`. ## Unresolved references If a variable you reference is unset or empty, envapt leaves its `${VAR}` text in the value. A cycle stays as text too. When a referenced variable's own value holds references and none of them resolve, the outer reference stays as text. If some of them resolve, the outer reference becomes the partly resolved text, such as `x-${C}`. ```dotenv DB_HOST=localhost DATABASE_URL=postgres://${DB_HOST}:${DB_PORT}/app CACHE_URL=redis://${CACHE_HOST} CACHE_HOST=${REDIS_HOST} PRIMARY_URL=${REPLICA_URL} REPLICA_URL=${PRIMARY_URL} ``` With `DB_PORT` and `REDIS_HOST` unset, the three reads return: ```txt DATABASE_URL -> postgres://localhost:${DB_PORT}/app CACHE_URL -> redis://${CACHE_HOST} PRIMARY_URL -> ${REPLICA_URL} ``` `${DB_HOST}` resolves, but `${DB_PORT}` stays as written. `CACHE_URL` shows `${CACHE_HOST}`, a variable that is set, because `CACHE_HOST` can't finish expanding. `PRIMARY_URL` and `REPLICA_URL` reference each other, so the read stops at the first reference and returns `${REPLICA_URL}`. Code that uses a value with an unresolved reference fails later. Set `Envapter.debug = 'warn'` to log each unresolved reference when it's read ([Debug logging](/docs/errors#debug-logging)). > **TIP** > > Set `Envapter.strict = true` ([strict mode](/docs/required-values#strict-mode)) to make a reference to a missing variable throw `EnvaptError` with code `MissingEnvValue`. A cycle still stays as text. Under strict mode a whitespace-only variable counts as unset too. ## Syntax limits envapt recognizes `${NAME}`, where `NAME` is letters, digits, and underscores. A reference like `${VAR:-fallback}` or an empty `${}` doesn't match that pattern, so it stays in the value as written. envapt logs nothing for it, even with `Envapter.debug = 'warn'`. To default a missing variable, pass a fallback to the method, as in `Envapter.get('VAR', 'fallback')`. A `$` that isn't followed by `{...}` stays as it is, as in `price: $5`. No escape sequence exists. To read a value that contains a literal `${...}`, use `getRaw`, which skips expansion for the whole value. ## The resolve tagged template If you build a string from two reads by hand and a key is unset, a template literal prints the text `undefined`, because `Envapter.get` without a fallback returns `undefined`. `resolve` builds the string from the keys you list in the template. Each `${...}` slot takes an env var name, or an [ordered key list](/docs/envapter#ordered-keys), and the first name with a value wins. A slot has no fallback. If a value contains `${VAR}` references, envapt expands them first. ```ts // API_HOST=api.example.com, API_PORT=8080 const endpoint = Envapter.resolve`https://${'API_HOST'}:${'API_PORT'}`; // reads READONLY_URL when present, otherwise DATABASE_URL const target = Envapter.resolve`Connecting to ${['READONLY_URL', 'DATABASE_URL']}`; ``` `endpoint` is a plain `string`. `target` uses `READONLY_URL` when it has a value and `DATABASE_URL` otherwise. It holds an empty string when neither has a value. A slot with a missing key behaves differently from a `${VAR}` reference inside a value. | Key unset or empty | `${VAR}` inside a value | `resolve` slot | | --- | --- | --- | | [Strict mode](/docs/required-values#strict-mode) off | keeps the text `${VAR}` | inserts an empty string | | Strict mode on | throws [`MissingEnvValue`](/docs/errors) | throws [`MissingEnvValue`](/docs/errors) | An empty string can hide a missing key inside a message that looks fine. If the string is a URL or a path, turn strict mode on or check the key with [`require`](/docs/required-values) first. If your platform sets `DATABASE_URL` itself, that value wins over the templated line in `.env` by default, so `DB_HOST` and `DB_PORT` no longer affect it. A `resolve` call reads `DB_HOST` and `DB_PORT` directly, so it uses their values whatever `DATABASE_URL` holds. --- # Testing Bind a fresh config for each vitest test, then restore the real source afterward. Without a source of your own, a test that reads config reads the machine it runs on. On Node, envapt reads a copy of `process.env` from [`FileSource`](/docs/sources#the-sources-envapt-ships) and loads the `.env` files in the working directory, `.env.local` included. Your laptop has a `.env.local` and CI doesn't, so the same test can pass in one place and fail in the other. If you assign `process.env.PORT = '4000'` inside the test, nothing changes until the cache rebuilds. `FileSource` takes its copy when the cache is built, so a later assignment isn't in it (see [When values are read](/docs/sources#when-values-are-read)). To give a test its own values, bind a source with [`Envapter.useSource`](/docs/sources#the-sources-envapt-ships). It rebuilds the cache as soon as you call it, so the next read uses your values. The cost is that the binding is process-wide. Tests that run concurrently in one file share it, so run these sequentially. The examples use vitest. In another runner that loads ESM, such as `bun test`, the envapt calls stay the same and only the hook and stub names change. ## Where a test's values come from **Fixture object** Bind a [`PortableSource`](/docs/sources#the-sources-envapt-ships) built from a plain object. You control every key, so no shell variable or `.env` file reaches envapt. ```ts import { afterEach, expect, it } from 'vitest'; import { Envapter, EnvaptErrorCodes, FileSource, PortableSource } from 'envapt'; import { loadConfig } from './config'; afterEach(() => { Envapter.useSource(new FileSource()); }); it('converts the values it is given', () => { Envapter.useSource( new PortableSource({ ENVIRONMENT: 'test', DATABASE_URL: 'postgres://app@localhost:5432/app', PORT: '4000', CACHE_TTL: '30s' }) ); expect(loadConfig().cacheTtl).toBe(30_000); expect(Envapter.isTest).toBe(true); }); it('stops when a value is missing', () => { Envapter.useSource(new PortableSource({})); expect(() => loadConfig()).toThrow(expect.objectContaining({ code: EnvaptErrorCodes.MissingEnvValue })); }); ``` `loadConfig` is `getRequiredAll` over the three keys in `config.ts`. vitest sets `NODE_ENV=test`, but the fixture is the only thing envapt reads now. Without `ENVIRONMENT: 'test'`, [`Envapter.environment`](/docs/environment) would be `'development'` and the `isTest` check in the first test would fail. The second test binds an empty fixture to check the `MissingEnvValue` failure that [Required values](/docs/required-values) describes. After each test, `afterEach` binds a fresh `FileSource`, which reads `process.env` and the files again. **Shell variables** When the code under test also reads `process.env` itself, stub the variables with `vi.stubEnv`, then rebind a `FileSource` to make envapt copy the stubbed values. ```ts import { afterEach, expect, it, vi } from 'vitest'; import { Envapter, FileSource } from 'envapt'; import { loadConfig } from './config'; function setShellVars(vars: Record): void { for (const [key, value] of Object.entries(vars)) vi.stubEnv(key, value); Envapter.useSource(new FileSource()); } afterEach(() => { vi.unstubAllEnvs(); Envapter.useSource(new FileSource()); }); it('reads the stubbed shell variables', () => { setShellVars({ DATABASE_URL: 'postgres://app@localhost:5432/app', PORT: '4000', CACHE_TTL: '30s' }); expect(loadConfig().port).toBe(4000); }); ``` `setShellVars` stubs all three keys and rebinds, and `afterEach` calls `vi.unstubAllEnvs()` before it rebinds again. The `.env` files in the working directory still load, and a shell variable wins over the same key in a file ([.env files on Node](/docs/env-files#which-files-load)). A stub therefore wins over your `.env.local`, though a key you don't stub still comes from the shell or the files. **A few keys over the real files** To change one or two keys and keep everything else, pass a `FileSource` and then a `PortableSource` to [`merge`](/docs/sources#combining-sources). The later source wins, so your values replace the same keys from the shell or the files. `process.env` stays untouched. ```ts import { afterEach, expect, it } from 'vitest'; import { Envapter, FileSource, PortableSource, merge } from 'envapt'; import { loadConfig } from './config'; afterEach(() => { Envapter.useSource(new FileSource()); }); it('uses the port from the later source', () => { Envapter.useSource(merge(new FileSource(), new PortableSource({ PORT: '4000' }))); expect(loadConfig().port).toBe(4000); }); ``` `loadConfig` also requires `DATABASE_URL` and `CACHE_TTL`. Those two still come from your shell or your real `.env` files here, so this test needs them on every machine that runs it. If the test shouldn't need them, use the fixture object. **Fixture .env files** To test the [cascade](/docs/env-files#which-files-load) itself, point envapt at a folder of fixture files and [pin the environment](/docs/environment#pinning-the-environment) by assigning `Envapter.environment`. Without the pin, vitest's `NODE_ENV=test` makes envapt load `.env.test`. - tests/ - fixtures/ - .env - .env.production - config.test.ts ```dotenv title="tests/fixtures/.env" DATABASE_URL=postgres://app@localhost:5432/app PORT=3000 CACHE_TTL=10m ``` ```dotenv title="tests/fixtures/.env.production" PORT=8443 ``` ```ts import { join } from 'node:path'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { Envapter, FileSource } from 'envapt'; import { loadConfig } from './config'; beforeEach(() => { for (const key of ['DATABASE_URL', 'PORT', 'CACHE_TTL']) vi.stubEnv(key, undefined); Envapter.useSource(new FileSource()); Envapter.baseDir = join(import.meta.dirname, 'fixtures'); Envapter.environment = 'production'; }); afterEach(() => { vi.unstubAllEnvs(); Envapter.resetPaths(); Envapter.baseDir = undefined; }); it('layers .env.production over .env', () => { expect(loadConfig().port).toBe(8443); }); ``` `beforeEach` removes the three keys from `process.env` first, since an exported `PORT` would win over both files and make the test read your shell. It then binds a `FileSource` because `baseDir` throws `FileApiUnsupported` while a `PortableSource` is bound. [`resetPaths`](/docs/env-files#profiles) clears the pinned environment but keeps `baseDir`, so `afterEach` also sets `Envapter.baseDir = undefined`. [Choosing the files yourself](/docs/env-files#choosing-the-files-yourself) covers both settings. ## Put the real config back If you leave a source bound, it stays bound for the later tests in the same file. vitest runs each test file in isolation by default, so a bound source doesn't carry over to the next file. These settings keep their value after `useSource`, so each one you change needs its own line in your cleanup. | You changed | Undo it with | Note | | --- | --- | --- | | [`Envapter.useSource(source)`](/docs/sources) | `Envapter.useSource(new FileSource())` | rebuilds the cache from `process.env` and the files | | [`Envapter.environment = '...'`](/docs/environment#pinning-the-environment) | `Envapter.resetPaths()` | the pin survives `useSource`. `resetPaths` also clears `envPaths` and profiles | | [`Envapter.baseDir = '...'`](/docs/env-files#reading-from-a-fixed-directory) | `Envapter.baseDir = undefined` | `resetPaths` keeps `baseDir`, and a file source has to be bound first | | [`Envapter.strict = true`](/docs/required-values#strict-mode) | `Envapter.strict = false` | `useSource` leaves it as it was | | [`Envapter.envFileOptions = {...}`](/docs/env-files#file-options) | `Envapter.envFileOptions = {}` | `resetPaths` keeps it | | [`Envapter.debug = 'verbose'`](/docs/errors#debug-logging) | `Envapter.debug = 'silent'` | `useSource` leaves it as it was | | [`Envapter.fileApiMode = 'throw'`](/docs/compatibility#file-apis-and-fileapimode) | `Envapter.fileApiMode = 'warn'` | `useSource` leaves it as it was | Leave [`Envapter.syncProcessEnv`](/docs/env-files#coming-from-dotenv) off in tests, which is its default. When it's on, envapt writes the keys it loaded from `.env` files into `process.env`. Turning it off again doesn't remove keys already written, so they stay in `process.env` until the process exits. Put the cleanup lines you use in one `afterEach` inside a setup file, so no test file repeats them. vitest runs a setup file's hooks around every test in every file. ```ts title="tests/setup.ts" import { afterEach } from 'vitest'; import { Envapter, FileSource } from 'envapt'; afterEach(() => { Envapter.useSource(new FileSource()); Envapter.resetPaths(); Envapter.baseDir = undefined; Envapter.strict = false; }); ``` `useSource` comes first because `baseDir` throws `FileApiUnsupported` unless a file source is bound. List the file under `setupFiles` in your vitest config: ```ts title="vitest.config.ts" import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { setupFiles: ['./tests/setup.ts'] } }); ``` `tests/setup.ts` imports `FileSource`, so it can't run on a Workers or browser test runner. Those runners resolve the [portable build](/docs/compatibility#binding-by-runtime), which exports no `FileSource`. Restore by binding the platform's object again, such as `new PortableSource(env)` in a Worker test ([Cloudflare Workers](/docs/workers)) or `new PortableSource(import.meta.env)` in a browser test ([Browser](/docs/browser)). On the portable build, `resetPaths`, `baseDir`, `envPaths`, `envFileOptions`, and `configureProfiles` warn and do nothing under the default [`fileApiMode`](/docs/compatibility#file-apis-and-fileapimode) of `'warn'`, and throw `FileApiUnsupported` under `'throw'`. A pinned environment therefore stays pinned. Put `ENVIRONMENT` in the fixture there and skip the pin. ### With isolate turned off If you set vitest's `isolate: false`, test files that run in one worker share their modules, including envapt and its state. A source, a `strict` setting, or a pinned environment set in one file is still there when the next file starts. If a setup file calls `vi.resetModules()`, each file gets fresh modules, but only for modules that vitest loads itself. Envapt isn't one of those, because Node loads packages from `node_modules` without passing them through vitest. List envapt under `server.deps.inline` so vitest loads it: ```ts title="vitest.config.ts" import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { isolate: false, setupFiles: ['./tests/fresh-modules.ts', './tests/setup.ts'], server: { deps: { inline: ['envapt'] } } } }); ``` ```ts title="tests/fresh-modules.ts" import { vi } from 'vitest'; vi.resetModules(); ``` `fresh-modules.ts` comes first in `setupFiles`. If `setup.ts` ran first, it would bind its source on a copy of envapt that the reset then discards. The test file would import a fresh copy, which binds `FileSource` on import and reads your machine's values. ## Reads at module scope run before the test If you read a value outside a function, the read runs when its module first loads, before any `beforeEach`. A fixture bound later can't change it. ```ts export const port = Envapter.getAs('PORT', Converters.Port, 3000); export function getPort(): number { return Envapter.getAs('PORT', Converters.Port, 3000); } ``` Under vitest, `port` holds whatever the real source gave when the module loaded, so every test reads that same number. `getPort()` reads when a test calls it. If a test needs to vary a constant, turn it into a function like `getPort`. If no test varies it, module scope is fine, as long as the values exist when the module loads. [Required values](/docs/required-values#where-to-put-the-check) recommends calling `getRequiredAll` at the top level of a config module. That module reads when the test file imports it. Bind the test values in a setup file, because vitest runs `setupFiles` before it imports each test file: ```ts title="tests/setup.ts" import { afterEach } from 'vitest'; import { Envapter, PortableSource } from 'envapt'; const testEnv = new PortableSource({ ENVIRONMENT: 'test', DATABASE_URL: 'postgres://app@localhost:5432/app', PORT: '4000', CACHE_TTL: '30s' }); Envapter.useSource(testEnv); afterEach(() => { Envapter.useSource(testEnv); Envapter.strict = false; }); ``` The config module reads `testEnv` once, when the test file imports it, so every test in that file gets the same values. This `tests/setup.ts` replaces the one in [Put the real config back](#put-the-real-config-back). Its `afterEach` binds `testEnv` again, because binding `FileSource` there would make the next test read your machine's values. It leaves out the `baseDir` line because `baseDir` throws `FileApiUnsupported` while a `PortableSource` is bound, so a test that pins `environment` has to call `Envapter.resetPaths()` itself. ## Decorated classes A decorated field throws `EnvaptError` with code `InvalidUserDefinedConfig` (302) when you assign to it, so a test binds a source to change it. A decorated field stores the first value it reads. `useSource` clears that stored value, so the next read of the field converts the value from the new source. If you leave a source bound, a field you read in one test keeps that value in the tests after it. These samples use the `envapt/legacy` decorators, because vitest's default transform can't load the modern ones ([the error below](#syntaxerror-invalid-or-unexpected-token)). ```ts // config.ts import { Converters } from 'envapt'; import { Envapt } from 'envapt/legacy'; export class Config { @Envapt('CACHE_TTL', { converter: Converters.Time, fallback: '5m' }) static readonly cacheTtl: number; } ``` ```ts // config.test.ts import { afterEach, expect, it } from 'vitest'; import { Envapter, FileSource, PortableSource } from 'envapt'; import { Config } from './config'; afterEach(() => { Envapter.useSource(new FileSource()); }); it('reads the field again after the source changes', () => { Envapter.useSource(new PortableSource({ CACHE_TTL: '30s' })); expect(Config.cacheTtl).toBe(30_000); Envapter.useSource(new PortableSource({ CACHE_TTL: '1m' })); expect(Config.cacheTtl).toBe(60_000); }); ``` The second `Config.cacheTtl` read converts `'1m'` because the second `useSource` call cleared the first value. `afterEach` binds `FileSource` again, so the next test reads `Config.cacheTtl` from your real config. ### SyntaxError: Invalid or unexpected token > **WARNING** > > With vitest's default transform, if a test file imports a class that uses `@Envapt` on a `static accessor` field, the file fails to load with this error. Node can't parse the decorator syntax, and vitest leaves it unchanged. The `envapt/legacy` decorators load when `experimentalDecorators` is on in `tsconfig.json`. They import from `envapt/legacy` and declare fields as `static readonly` ([Decorators](/docs/decorators)). Bun (`bun test` included) and Deno run the modern form directly ([Compatibility](/docs/compatibility#decorators-per-runtime)). --- # Cloudflare Workers Read typed config from a Worker's vars and secrets by binding the Cloudflare env object once. Every `vars` entry and secret reaches a Worker as a property on `env`. If a handler reads `env.ORIGIN_URL` directly, it gets text and has to parse it into a URL itself. A JSON `vars` entry arrives as an object, and your code never checks its shape. Binding `env` to envapt once gives you typed values instead. The portable build that a Worker loads reads nothing until you [bind a source](/docs/sources#no-environment-source-is-bound). Wrangler resolves envapt's portable build through the `workerd` export condition, so the import itself needs no setup. ## Bind env in a config module `cloudflare:workers` exports `env`, and you can read it at module scope. Put the bind and the reads in one `config.ts`. Every module that imports `config` reads after the bind. A module that calls [`Envapter.get`](/docs/envapter) directly can run before the bind and throw [`NoSourceBound`](/docs/errors), so read through `config` everywhere. ```ts title="src/config.ts" import { env } from 'cloudflare:workers'; import { Converters, Envapter, PortableSource } from 'envapt'; Envapter.useSource(new PortableSource(env)); const values = Envapter.getRequiredAll( { ORIGIN_URL: Converters.Url, API_TOKEN: Converters.String }, 'camelCase' ); export const config = { ...values, cacheTtl: Envapter.getAs('CACHE_TTL', Converters.Time, '1m') }; ``` `ORIGIN_URL` is a plain `vars` entry and `API_TOKEN` is a secret, but both are properties of `env`, so one [`getRequiredAll`](/docs/required-values) call reads them. The `'camelCase'` argument turns them into `config.originUrl`, a `URL`, and `config.apiToken`. `cacheTtl` is optional, so it falls back to one minute when `CACHE_TTL` is unset. ```ts title="src/index.ts" import { config } from './config'; export default { async fetch(request: Request): Promise { if (request.headers.get('authorization') !== `Bearer ${config.apiToken}`) { return new Response('Unauthorized', { status: 401 }); } const { pathname, search } = new URL(request.url); return fetch(new URL(pathname + search, config.originUrl), request); } } satisfies ExportedHandler; ``` The handler only imports `config`, so no request converts or validates an env value. The `satisfies ExportedHandler` line and the `cloudflare:workers` import need Cloudflare's types, or TypeScript can't resolve them. Run `wrangler types` to generate them, or install `@cloudflare/workers-types` and add it to `types` in `tsconfig.json`. This shape has two costs. If `API_TOKEN` is unset, the config module throws `MissingEnvValue` and `wrangler dev` stops with this error: ```txt Uncaught Error: EnvaptError [305]: Missing required environment variables: API_TOKEN. ``` The same throw can fail a deploy that goes out before the secret exists, because Cloudflare runs a Worker's top-level code when you deploy it and reports a throw there as a validation error (code 10021). [Required values](/docs/required-values) shows how to read the error. The second cost is that a running instance can keep the values it bound after a deploy, which [Reading fresh values](#reading-fresh-values-on-every-request) covers. > **WARNING** > > `useSource` sets state for the whole Worker instance, the isolate that serves many requests, so every request it serves shares the bound source. Keep per-request data, such as the `request` or a user id, out of the source. ## Where the values come from You declare `vars` and secrets in your Cloudflare config file. `src/index.ts` reads either form the same way. **wrangler.jsonc** ```jsonc title="wrangler.jsonc" { "name": "origin-proxy", "main": "src/index.ts", "compatibility_date": "2026-10-01", "vars": { "ENVIRONMENT": "production", "ORIGIN_URL": "https://origin.example.com", "CACHE_TTL": "5m", "FEATURE_FLAGS": { "beta": true } } } ``` `wrangler.toml` takes the same fields. Keep `API_TOKEN` out of the file. Store the production secret with this command: ```sh wrangler secret put API_TOKEN ``` **cloudflare.config.ts** ```ts title="cloudflare.config.ts" import { bindings, defineConfig } from 'cf/config'; export default defineConfig({ worker: { name: 'origin-proxy', compatibilityDate: '2026-10-01', entrypoint: 'src/index.ts', env: { ENVIRONMENT: bindings.text('production'), ORIGIN_URL: bindings.text('https://origin.example.com'), CACHE_TTL: bindings.text('5m'), FEATURE_FLAGS: bindings.json({ beta: true }), API_TOKEN: bindings.secret() } } }); ``` `cloudflare.config.ts` is the typed config file for Cloudflare's `cf` CLI. Cloudflare says `cf` is in beta and its configuration can change before the stable release. Write a `vars` entry as `bindings.text()`, or as `bindings.json()` for a non-string value. `bindings.secret()` declares that the Worker requires `API_TOKEN`. You upload the value separately, for example by passing `--secrets-file` to `cf deploy` ([Cloudflare's migration guide](https://developers.cloudflare.com/cf/wrangler/migrate/)). In local development, `wrangler dev` and `cf dev` read secrets from `.dev.vars`: ```txt title=".dev.vars" API_TOKEN=local-dev-token ENVIRONMENT=development ``` Under `wrangler dev`, a key in `.dev.vars` replaces a `vars` entry of the same name, which is why `ENVIRONMENT` appears here too. `ENVIRONMENT` is the first of the [detection keys](/docs/environment). If none of those keys is set, `Envapter.environment` is `'development'`, so set `ENVIRONMENT` in your config file for production. Cloudflare's [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) docs explain per-environment `vars` and `wrangler secret bulk`. ## JSON vars and resource bindings A JSON `vars` entry such as `FEATURE_FLAGS` in the config above arrives on `env` as an object. [`PortableSource`](/docs/sources#the-sources-envapt-ships) stringifies it, and [`Converters.Json`](/docs/converters#json) parses that string back. ```ts import { env } from 'cloudflare:workers'; import { Converters, Envapter, PortableSource } from 'envapt'; Envapter.useSource(new PortableSource(env)); const flags = Envapter.getAs('FEATURE_FLAGS', Converters.Json); // { "beta": true } in vars ``` `flags` is `JsonValue | undefined`, the type `Converters.Json` returns for any JSON. To check the shape, pass a [schema](/docs/standard-schema) to `getAs` or `getRequired` in place of `Converters.Json`. A resource binding such as a KV namespace is a property of `env` as well. `PortableSource` runs it through `JSON.stringify`, so for a KV namespace `Envapter.get('CACHE')` returns `'{}'`. Read `vars` and secrets through envapt, and use bindings from `env` directly. ## Reading fresh values on every request Cloudflare may keep a Worker instance running after a deploy that changes only bindings ([Cloudflare's note on bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/#making-changes-to-bindings)). The `config` from the module above keeps its old values until Cloudflare replaces the instance. If you change `vars` or secrets without changing code and need the new values right away, bind the handler's `env` argument on every request. Each bind rebuilds envapt's cache, so every request pays for a rebuild. ```ts import { Converters, Envapter, PortableSource } from 'envapt'; export default { async fetch(request: Request, env: Env): Promise { Envapter.useSource(new PortableSource(env)); const origin = Envapter.getRequired('ORIGIN_URL', Converters.Url); return fetch(new URL(new URL(request.url).pathname, origin), request); } } satisfies ExportedHandler; ``` `PortableSource` accepts the typed `env` argument because its constructor takes any object. ## Decorators Wrangler's bundler compiles the default `@Envapt` accessor decorators, so they run on Workers. A decorated field reads its variable on first access. If any code reads the field before your bind runs, that read throws `NoSourceBound`. [Compatibility](/docs/compatibility#decorators-per-runtime) lists the legacy form. --- # API reference Every export of envapt and envapt/legacy, with signatures, parameters, and return types generated from the library's type definitions. These pages come from the type definitions of the envapt version this site was built from. The [guide](/docs) explains when and why to use each piece. | entry | what it exports | | --- | --- | | [`envapt`](/docs/api/reference/envapt) | `Envapter`, `Converters`, the `@Envapt` decorators, the sources, `EnvaptError`, and the types | | [`envapt/legacy`](/docs/api/reference/legacy) | the same decorators for projects that compile with `experimentalDecorators` | | `envapt/config` | nothing. Importing it loads your `.env` files into `process.env` | The `Envapter` page documents the Node, Bun, and Deno build. On Workers, the browser, and edge runtimes, `envapt` resolves to the portable build, where `Envapter` has the same members and its file APIs (`envPaths`, `baseDir`, `envFileOptions`, `configureProfiles`, `resetPaths`) warn once and do nothing. [Sources](/docs/sources) explains the two builds. --- # EnvaptError The error envapt throws. Its `code` identifies the failure. ## Example ```ts try { Envapter.require('DATABASE_URL', 'SESSION_SECRET'); } catch (error) { if (error instanceof EnvaptError && error.code === EnvaptErrorCodes.MissingEnvValue) { console.error(error.message); process.exit(1); } throw error; } ``` ## See [https://envapt.materwelon.dev/docs/errors#the-error-shape](https://envapt.materwelon.dev/docs/errors#the-error-shape) ## Extends - `Error` ## Constructors ### Constructor ```ts new EnvaptError( code, message, options? ): EnvaptError; ``` #### Parameters | Parameter | Type | | ------ | ------ | | `code` | [`EnvaptErrorCodes`](../type-aliases/EnvaptErrorCodes.mdx) | | `message` | `string` | | `options?` | `EnvaptErrorOptions` | #### Returns `EnvaptError` #### Overrides ```ts Error.constructor ``` ## Properties | Property | Modifier | Type | Description | Inherited from | | ------ | ------ | ------ | ------ | ------ | | `cause?` | `public` | `unknown` | - | `Error.cause` | | `code` | `readonly` | [`EnvaptErrorCodes`](../type-aliases/EnvaptErrorCodes.mdx) | The [EnvaptErrorCodes](../variables/EnvaptErrorCodes.mdx) value identifying what failed. | - | | `issues` | `readonly` | readonly [`EnvaptIssue`](../interfaces/EnvaptIssue.mdx)[] \| `undefined` | Populated only for [EnvaptErrorCodes.SchemaValidationFailed](../variables/EnvaptErrorCodes.mdx#property-schemavalidationfailed) (208) and [EnvaptErrorCodes.InvalidEnvValue](../variables/EnvaptErrorCodes.mdx#property-invalidenvvalue) (210). For every other code this is `undefined`. If a custom parser or schema throws during a required read, the issue for that key keeps the thrown value as `cause`. If exactly one issue has a `cause`, the error's own `cause` is that value too. | - | | `message` | `public` | `string` | - | `Error.message` | | `name` | `public` | `string` | - | `Error.name` | | `stack?` | `public` | `string` | - | `Error.stack` | --- # Envapter Reads typed environment variables on Node, Bun, and Deno. It adds the filesystem-only configuration APIs: `.env` path selection, base directory, dotenv options, and per-environment profiles. On the portable build (Workers, the browser, edge) these same APIs warn once and do nothing, unless `Envapter.fileApiMode` is `'throw'`. ## Accessors ### baseDir #### Get Signature ```ts get static baseDir(): string | undefined; ``` The configured base directory, or `undefined` when relative paths resolve against the working directory. ##### See [https://envapt.materwelon.dev/docs/env-files#reading-from-a-fixed-directory](https://envapt.materwelon.dev/docs/env-files#reading-from-a-fixed-directory) ##### Returns `string` \| `undefined` #### Set Signature ```ts set static baseDir(value): void; ``` Set the base directory that relative `.env` paths resolve against. Without one they resolve against `process.cwd()`. It covers the auto-cascade, `Envapter.configureProfiles` paths, and relative `envPaths`. Absolute paths always bypass it. Pass a directory path (`import.meta.dirname`) or a module URL (`import.meta.url`) to resolve paths next to the calling file regardless of launch directory. Set this before `envPaths` so relative `envPaths` validate against the right directory. Setting `undefined` restores `process.cwd()` resolution. ##### See [https://envapt.materwelon.dev/docs/env-files#reading-from-a-fixed-directory](https://envapt.materwelon.dev/docs/env-files#reading-from-a-fixed-directory) ##### Parameters | Parameter | Type | | ------ | ------ | | `value` | `string` \| `URL` \| `undefined` | ##### Returns `void` *** ### debug #### Get Signature ```ts get static debug(): DebugLevel; ``` ##### Returns [`DebugLevel`](../type-aliases/DebugLevel.mdx) #### Set Signature ```ts set static debug(level): void; ``` Set the debug log level. Defaults to `silent`. When unset, reads `ENVAPT_DEBUG` from the bound source on first access. The setter overrides any env-var value. Output goes to stderr on Node (the console elsewhere), prefixed with `[envapt]`. ##### See [https://envapt.materwelon.dev/docs/errors#debug-logging](https://envapt.materwelon.dev/docs/errors#debug-logging) ##### Parameters | Parameter | Type | | ------ | ------ | | `level` | [`DebugLevel`](../type-aliases/DebugLevel.mdx) | ##### Returns `void` #### Inherited from ```ts Envapter.debug ``` *** ### envFileOptions #### Get Signature ```ts get static envFileOptions(): EnvFileOptions; ``` The env file loader options. ##### See [https://envapt.materwelon.dev/docs/env-files#file-options](https://envapt.materwelon.dev/docs/env-files#file-options) ##### Returns [`EnvFileOptions`](../interfaces/EnvFileOptions.mdx) #### Set Signature ```ts set static envFileOptions(config): void; ``` Set the env file loader options (`encoding`, `override`). Refreshes the cache. Any other key throws `InvalidUserDefinedConfig`. ##### See [https://envapt.materwelon.dev/docs/env-files#file-options](https://envapt.materwelon.dev/docs/env-files#file-options) ##### Parameters | Parameter | Type | | ------ | ------ | | `config` | [`EnvFileOptions`](../interfaces/EnvFileOptions.mdx) | ##### Returns `void` *** ### environment #### Get Signature ```ts get static environment(): Environment; ``` The current application environment, detected on first read unless you set it. ##### See [https://envapt.materwelon.dev/docs/environment#detecting-the-environment](https://envapt.materwelon.dev/docs/environment#detecting-the-environment) ##### Returns [`Environment`](../type-aliases/Environment.mdx) #### Set Signature ```ts set static environment(env): void; ``` Set the application environment to an [Environment](../variables/Environment.mdx) value such as `'production'`, matched case-insensitively. An unknown name selects `'development'`. Setting it reloads the `.env` files for that environment. ##### See [https://envapt.materwelon.dev/docs/environment#detecting-the-environment](https://envapt.materwelon.dev/docs/environment#detecting-the-environment) ##### Parameters | Parameter | Type | | ------ | ------ | | `env` | [`Environment`](../type-aliases/Environment.mdx) \| `string` & `object` | ##### Returns `void` #### Inherited from ```ts Envapter.environment ``` *** ### envPaths #### Get Signature ```ts get static envPaths(): string[]; ``` The configured .env file paths. ##### See [https://envapt.materwelon.dev/docs/env-files#choosing-the-files-yourself](https://envapt.materwelon.dev/docs/env-files#choosing-the-files-yourself) ##### Returns `string`[] #### Set Signature ```ts set static envPaths(paths): void; ``` Set custom .env file paths. Setting new paths clears the cache and reloads environment variables. Throws `EnvFilesNotFound` when a path does not exist. Once set, these paths replace the dotenv-flow auto-cascade and any `Envapter.configureProfiles` paths. ##### See [https://envapt.materwelon.dev/docs/env-files#choosing-the-files-yourself](https://envapt.materwelon.dev/docs/env-files#choosing-the-files-yourself) ##### Parameters | Parameter | Type | | ------ | ------ | | `paths` | `string` \| `string`[] | ##### Returns `void` *** ### fileApiMode #### Get Signature ```ts get static fileApiMode(): FileApiMode; ``` ##### Returns [`FileApiMode`](../type-aliases/FileApiMode.mdx) #### Set Signature ```ts set static fileApiMode(mode): void; ``` On the portable build, this controls the filesystem-only config APIs (`envPaths`, `baseDir`, `envFileOptions`, `configureProfiles`, `resetPaths`). `'warn'` (the default) warns once and does nothing. `'throw'` throws [EnvaptError](./EnvaptError.mdx) `FileApiUnsupported`. The node build runs these APIs normally and this value has no effect there. ##### See [https://envapt.materwelon.dev/docs/compatibility#file-apis-and-fileapimode](https://envapt.materwelon.dev/docs/compatibility#file-apis-and-fileapimode) ##### Parameters | Parameter | Type | | ------ | ------ | | `mode` | [`FileApiMode`](../type-aliases/FileApiMode.mdx) | ##### Returns `void` #### Inherited from ```ts Envapter.fileApiMode ``` *** ### isDevelopment #### Get Signature ```ts get static isDevelopment(): boolean; ``` Whether the current environment is development. ##### See [https://envapt.materwelon.dev/docs/environment#branching-on-the-environment](https://envapt.materwelon.dev/docs/environment#branching-on-the-environment) ##### Returns `boolean` #### Inherited from ```ts Envapter.isDevelopment ``` *** ### isProduction #### Get Signature ```ts get static isProduction(): boolean; ``` Whether the current environment is production. ##### See [https://envapt.materwelon.dev/docs/environment#branching-on-the-environment](https://envapt.materwelon.dev/docs/environment#branching-on-the-environment) ##### Returns `boolean` #### Inherited from ```ts Envapter.isProduction ``` *** ### isStaging #### Get Signature ```ts get static isStaging(): boolean; ``` Whether the current environment is staging. ##### See [https://envapt.materwelon.dev/docs/environment#branching-on-the-environment](https://envapt.materwelon.dev/docs/environment#branching-on-the-environment) ##### Returns `boolean` #### Inherited from ```ts Envapter.isStaging ``` *** ### isTest #### Get Signature ```ts get static isTest(): boolean; ``` Whether the current environment is test. ##### See [https://envapt.materwelon.dev/docs/environment#branching-on-the-environment](https://envapt.materwelon.dev/docs/environment#branching-on-the-environment) ##### Returns `boolean` #### Inherited from ```ts Envapter.isTest ``` *** ### source #### Get Signature ```ts get static source(): Source; ``` The currently bound [Source](../type-aliases/Source.mdx). Use it with `merge` to add another source without replacing the existing one. On portable builds, bind a source before composing it. ##### See [https://envapt.materwelon.dev/docs/sources#combining-sources](https://envapt.materwelon.dev/docs/sources#combining-sources) ##### Returns [`Source`](../type-aliases/Source.mdx) #### Inherited from ```ts Envapter.source ``` *** ### strict #### Get Signature ```ts get static strict(): boolean; ``` ##### Returns `boolean` #### Set Signature ```ts set static strict(value): void; ``` When `true`, a whitespace-only value counts as missing, a `${VAR}` that can't resolve throws `MissingEnvValue`, and an empty array element throws `EmptyArrayElement`. Default `false`. Setting it clears and rebuilds the cache. ##### See [https://envapt.materwelon.dev/docs/required-values#strict-mode](https://envapt.materwelon.dev/docs/required-values#strict-mode) ##### Parameters | Parameter | Type | | ------ | ------ | | `value` | `boolean` | ##### Returns `void` #### Inherited from ```ts Envapter.strict ``` *** ### syncProcessEnv #### Get Signature ```ts get static syncProcessEnv(): boolean; ``` ##### Returns `boolean` #### Set Signature ```ts set static syncProcessEnv(value): void; ``` When `true`, copies dotenv-loaded keys into `process.env`. Default `false`. `envFileOptions.override` controls collisions because only keys the loader wrote are copied. With the default `false`, existing `process.env` values stay. With `true`, the file value replaces them in both the cache and `process.env`. Flipping `false → true` copies the loaded keys immediately without a cache refresh. Flipping `true → false` leaves copied keys in `process.env` until the process exits. ##### See [https://envapt.materwelon.dev/docs/env-files#file-options](https://envapt.materwelon.dev/docs/env-files#file-options) ##### Parameters | Parameter | Type | | ------ | ------ | | `value` | `boolean` | ##### Returns `void` #### Inherited from ```ts Envapter.syncProcessEnv ``` ## Methods ### configureProfiles() ```ts static configureProfiles(config): void; ``` Configure per-environment `.env` path overrides on top of the dotenv-flow auto-cascade. When set, each `Environment` key's `paths` load ahead of the cascade for that environment and win over it, unless `envFileOptions.override` is `true`. Unspecified environments still use the cascade as-is. Set `useDefaults: false` to disable the cascade entirely (load only the configured paths). Setting an explicit `Envapter.envPaths` value at any point overrides this configuration. #### Parameters | Parameter | Type | | ------ | ------ | | `config` | [`ProfilesConfig`](../type-aliases/ProfilesConfig.mdx) | #### Returns `void` #### Example ```ts Envapter.configureProfiles({ staging: { paths: 'config/staging.env' }, production: { paths: ['config/prod.env', 'secrets/prod.env'] } }); ``` #### See [https://envapt.materwelon.dev/docs/env-files#profiles](https://envapt.materwelon.dev/docs/env-files#profiles) *** ### get() ```ts static get(key, fallback?): ConditionalReturn; ``` Get a string environment variable, or `fallback` when it is missing or empty. Expands `${VAR}` templates in the value. Accepts a single key or an ordered array of keys, read until one has a value. Read any other type with `getAs` and a converter. #### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `Default` *extends* `string` \| `undefined` | `undefined` | #### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `fallback?` | `Default` | #### Returns `ConditionalReturn`\<`string`, `Default`\> #### See [https://envapt.materwelon.dev/docs/envapter#strings-and-typed-values](https://envapt.materwelon.dev/docs/envapter#strings-and-typed-values) #### Inherited from ```ts Envapter.get ``` *** ### getAs() #### Call Signature ```ts static getAs( key, converter, fallback? ): AdvancedConverterReturn; ``` Get an environment variable through a converter: a built-in token such as `Converters.Number`, an `ArrayOf` token from `Converters.array(...)`, a custom `(raw: string) => T` function, or a synchronous Standard Schema (zod, valibot, arktype). A missing value returns the fallback, or `undefined` without one. A scalar token or a function that can't convert the value returns the fallback too. A bad array element throws `ArrayElementConversionFailed`. A schema that rejects the value throws `SchemaValidationFailed`. envapt never runs the schema on the fallback. The key can be a single name or an ordered list, read until one key has a value. A bad converter or fallback throws even when the value is missing. ##### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `TConverter` *extends* `ConverterToken` \| `ArrayOf`\<`ArrayElement`\> | - | | `TFallback` *extends* \| `string` \| `number` \| `bigint` \| `boolean` \| `RegExp` \| `string`[] \| `JsonArray` \| `JsonObject` \| `URL` \| `Date` \| `unknown`[] \| `number`[] \| `bigint`[] \| `boolean`[] \| `URL`[] \| `Date`[] \| [`TimeFallback`](../type-aliases/TimeFallback.mdx)[] \| `object`[] \| `null` \| `undefined` | `undefined` | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `converter` | `TConverter` | | `fallback?` | `TFallback` | ##### Returns `AdvancedConverterReturn`\<`TConverter`, `TFallback`\> ##### See - [https://envapt.materwelon.dev/docs/envapter#other-types-with-getas](https://envapt.materwelon.dev/docs/envapter#other-types-with-getas) - [https://envapt.materwelon.dev/docs/converters#custom-converters](https://envapt.materwelon.dev/docs/converters#custom-converters) ##### Inherited from ```ts Envapter.getAs ``` #### Call Signature ```ts static getAs( key, converter, fallback ): TReturn; ``` Get an environment variable through a converter: a built-in token such as `Converters.Number`, an `ArrayOf` token from `Converters.array(...)`, a custom `(raw: string) => T` function, or a synchronous Standard Schema (zod, valibot, arktype). A missing value returns the fallback, or `undefined` without one. A scalar token or a function that can't convert the value returns the fallback too. A bad array element throws `ArrayElementConversionFailed`. A schema that rejects the value throws `SchemaValidationFailed`. envapt never runs the schema on the fallback. The key can be a single name or an ordered list, read until one key has a value. A bad converter or fallback throws even when the value is missing. ##### Type Parameters | Type Parameter | | ------ | | `TReturn` | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `converter` | `"json"` | | `fallback` | `TReturn` | ##### Returns `TReturn` ##### See - [https://envapt.materwelon.dev/docs/envapter#other-types-with-getas](https://envapt.materwelon.dev/docs/envapter#other-types-with-getas) - [https://envapt.materwelon.dev/docs/converters#custom-converters](https://envapt.materwelon.dev/docs/converters#custom-converters) ##### Inherited from ```ts Envapter.getAs ``` #### Call Signature ```ts static getAs( key, converter, fallback? ): TReturn | undefined; ``` Get an environment variable through a converter: a built-in token such as `Converters.Number`, an `ArrayOf` token from `Converters.array(...)`, a custom `(raw: string) => T` function, or a synchronous Standard Schema (zod, valibot, arktype). A missing value returns the fallback, or `undefined` without one. A scalar token or a function that can't convert the value returns the fallback too. A bad array element throws `ArrayElementConversionFailed`. A schema that rejects the value throws `SchemaValidationFailed`. envapt never runs the schema on the fallback. The key can be a single name or an ordered list, read until one key has a value. A bad converter or fallback throws even when the value is missing. ##### Type Parameters | Type Parameter | | ------ | | `TReturn` | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `converter` | `"json"` | | `fallback?` | `TReturn` | ##### Returns `TReturn` \| `undefined` ##### See - [https://envapt.materwelon.dev/docs/envapter#other-types-with-getas](https://envapt.materwelon.dev/docs/envapter#other-types-with-getas) - [https://envapt.materwelon.dev/docs/converters#custom-converters](https://envapt.materwelon.dev/docs/converters#custom-converters) ##### Inherited from ```ts Envapter.getAs ``` #### Call Signature ```ts static getAs( key, schema, fallback? ): ConditionalReturn, TFallback>; ``` Get an environment variable through a converter: a built-in token such as `Converters.Number`, an `ArrayOf` token from `Converters.array(...)`, a custom `(raw: string) => T` function, or a synchronous Standard Schema (zod, valibot, arktype). A missing value returns the fallback, or `undefined` without one. A scalar token or a function that can't convert the value returns the fallback too. A bad array element throws `ArrayElementConversionFailed`. A schema that rejects the value throws `SchemaValidationFailed`. envapt never runs the schema on the fallback. The key can be a single name or an ordered list, read until one key has a value. A bad converter or fallback throws even when the value is missing. ##### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `Schema` *extends* [`StandardSchemaV1`](../interfaces/StandardSchemaV1.mdx)\<`unknown`, `unknown`\> | - | | `TFallback` *extends* `unknown` | `undefined` | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `schema` | `SchemaConstraint`\<`Schema`\> | | `fallback?` | `TFallback` | ##### Returns `ConditionalReturn`\<`InferSchemaOutput`\<`Schema`\>, `TFallback`\> ##### See - [https://envapt.materwelon.dev/docs/envapter#other-types-with-getas](https://envapt.materwelon.dev/docs/envapter#other-types-with-getas) - [https://envapt.materwelon.dev/docs/converters#custom-converters](https://envapt.materwelon.dev/docs/converters#custom-converters) ##### Inherited from ```ts Envapter.getAs ``` #### Call Signature ```ts static getAs( key, converter, fallback? ): ConditionalReturn, TFallback>; ``` Get an environment variable through a converter: a built-in token such as `Converters.Number`, an `ArrayOf` token from `Converters.array(...)`, a custom `(raw: string) => T` function, or a synchronous Standard Schema (zod, valibot, arktype). A missing value returns the fallback, or `undefined` without one. A scalar token or a function that can't convert the value returns the fallback too. A bad array element throws `ArrayElementConversionFailed`. A schema that rejects the value throws `SchemaValidationFailed`. envapt never runs the schema on the fallback. The key can be a single name or an ordered list, read until one key has a value. A bad converter or fallback throws even when the value is missing. ##### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `TReturn` | - | | `TFallback` *extends* \| \{ \} \| `undefined` | `undefined` | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `converter` | [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TReturn`\> | | `fallback?` | `TFallback` | ##### Returns `ConditionalReturn`\<`NonNullable`\<`TReturn`\>, `TFallback`\> ##### See - [https://envapt.materwelon.dev/docs/envapter#other-types-with-getas](https://envapt.materwelon.dev/docs/envapter#other-types-with-getas) - [https://envapt.materwelon.dev/docs/converters#custom-converters](https://envapt.materwelon.dev/docs/converters#custom-converters) ##### Inherited from ```ts Envapter.getAs ``` *** ### getRaw() ```ts static getRaw(key): string | undefined; ``` Read an environment variable as its raw string, skipping parsing and conversion. A `${NAME}` reference in the value comes back unexpanded. #### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | #### Returns `string` \| `undefined` #### See [https://envapt.materwelon.dev/docs/envapter#the-raw-string](https://envapt.materwelon.dev/docs/envapter#the-raw-string) #### Inherited from ```ts Envapter.getRaw ``` *** ### getRequired() #### Call Signature ```ts static getRequired(key, converter): InferConverterReturnType; ``` Read a required environment variable and convert it, throwing `MissingEnvValue` when the value is missing or empty. A value it cannot convert throws `InvalidEnvValue` with issues keyed by the env name. A bad array element, an error a custom parser or schema throws, and a schema's own issues all become issues on that error. Accepts a built-in or `ArrayOf` token, a custom parser function, or a synchronous Standard Schema. The key can be a single name or an ordered list. A bad converter throws before `MissingEnvValue`. ##### Type Parameters | Type Parameter | | ------ | | `TConverter` *extends* `ConverterToken` \| `ArrayOf`\<`ArrayElement`\> | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `converter` | `TConverter` | ##### Returns `InferConverterReturnType`\<`TConverter`\> ##### See [https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has](https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has) ##### Inherited from ```ts Envapter.getRequired ``` #### Call Signature ```ts static getRequired(key, schema): InferSchemaOutput; ``` Read a required environment variable and convert it, throwing `MissingEnvValue` when the value is missing or empty. A value it cannot convert throws `InvalidEnvValue` with issues keyed by the env name. A bad array element, an error a custom parser or schema throws, and a schema's own issues all become issues on that error. Accepts a built-in or `ArrayOf` token, a custom parser function, or a synchronous Standard Schema. The key can be a single name or an ordered list. A bad converter throws before `MissingEnvValue`. ##### Type Parameters | Type Parameter | | ------ | | `Schema` *extends* [`StandardSchemaV1`](../interfaces/StandardSchemaV1.mdx)\<`unknown`, `unknown`\> | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `schema` | `SchemaConstraint`\<`Schema`\> | ##### Returns `InferSchemaOutput`\<`Schema`\> ##### See [https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has](https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has) ##### Inherited from ```ts Envapter.getRequired ``` #### Call Signature ```ts static getRequired(key, converter): NonNullable; ``` Read a required environment variable and convert it, throwing `MissingEnvValue` when the value is missing or empty. A value it cannot convert throws `InvalidEnvValue` with issues keyed by the env name. A bad array element, an error a custom parser or schema throws, and a schema's own issues all become issues on that error. Accepts a built-in or `ArrayOf` token, a custom parser function, or a synchronous Standard Schema. The key can be a single name or an ordered list. A bad converter throws before `MissingEnvValue`. ##### Type Parameters | Type Parameter | | ------ | | `TReturnType` | ##### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `converter` | [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TReturnType`\> | ##### Returns `NonNullable`\<`TReturnType`\> ##### See [https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has](https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has) ##### Inherited from ```ts Envapter.getRequired ``` *** ### getRequiredAll() ```ts static getRequiredAll(spec, casing?): { [K in string | number | symbol as RecaseKey]: InferSpecField }; ``` Read a group of required environment variables in one call. Each key in `spec` maps to a converter (a token, an `array()` token, or a custom parser) or a synchronous Standard Schema (zod, valibot, arktype, etc). The returned record holds every value. A converter field is never undefined. A schema field holds whatever its schema returns. A bad converter in `spec` throws first. envapt then collects every missing or empty key and throws one `MissingEnvValue` listing them all. envapt never runs the schema for a missing key, so a schema default such as zod's `.default()` does not apply here. If no key is missing, envapt reads every field and throws one `InvalidEnvValue` for all the bad values, schema fields included. Its `issues` hold every failed field's issues, each with its env key first in `path`. A converter that cannot convert adds the issue `could not be converted`. A bad array element and an error a custom parser or schema throws each become an issue carrying their message. The error message lists only the keys, since an issue message can quote the value. Pass a `casing` (`'camelCase'`, `'PascalCase'`, or `'kebab-case'`) to rename the record keys. The rename splits each key on underscores, as in SCREAMING_SNAKE env-var names. With no casing the keys stay as-is. #### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `Spec` *extends* `RequiredSpec` | - | | `Casing` *extends* `KeyCasing` \| `undefined` | `undefined` | #### Parameters | Parameter | Type | | ------ | ------ | | `spec` | `Spec` & `SpecSchemaConstraint`\<`Spec`\> | | `casing?` | `Casing` | #### Returns ```ts { [K in string | number | symbol as RecaseKey]: InferSpecField } ``` #### See [https://envapt.materwelon.dev/docs/required-values](https://envapt.materwelon.dev/docs/required-values) #### Inherited from ```ts Envapter.getRequiredAll ``` *** ### has() ```ts static has(key): boolean; ``` Check whether `key` has a value. An unset or empty value counts as missing, the same as in `getRequired`. Under strict mode, a value whose `${VAR}` can't resolve counts as missing too, and an ordered key list returns `false` at that key without checking the later ones. #### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | #### Returns `boolean` #### Example ```ts const cache = Envapter.has('REDIS_URL') ? createRedisCache() : createMemoryCache(); ``` #### See [https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has](https://envapt.materwelon.dev/docs/required-values#getrequired-require-and-has) #### Inherited from ```ts Envapter.has ``` *** ### require() ```ts static require(...keys): void; ``` Assert that one or more environment variables are present and non-empty after template resolution. Throws `MissingEnvValue` listing every missing key. A whitespace-only value counts as missing only under strict mode. Each argument can be an ordered key list, which passes when any of its keys has a value. For a typed required read in functional code, use `Envapter.getRequired(key, converter)`. #### Parameters | Parameter | Type | | ------ | ------ | | ...`keys` | \[`EnvKeyInput`, `...EnvKeyInput[]`\] | #### Returns `void` #### Example ```ts Envapter.require('DATABASE_URL'); Envapter.require('DATABASE_URL', 'API_KEY', 'SENTRY_DSN'); Envapter.require(['READONLY_URL', 'DATABASE_URL'], 'API_KEY'); ``` #### Inherited from ```ts Envapter.require ``` *** ### resetPaths() ```ts static resetPaths(): void; ``` Clears any `Envapter.configureProfiles` call, any explicit `Envapter.envPaths` assignment, and any environment set through `Envapter.environment`. `Envapter.baseDir` keeps its value. The call reloads the dotenv-flow cascade right away, choosing the files from the environment set in your env vars. #### Returns `void` #### See [https://envapt.materwelon.dev/docs/env-files#profiles](https://envapt.materwelon.dev/docs/env-files#profiles) *** ### resolve() ```ts static resolve(strings, ...keys): string; ``` Tagged template literal for resolving environment variables in template strings. A missing value resolves to an empty string. In strict mode an empty or whitespace-only value throws `MissingEnvValue`. #### Parameters | Parameter | Type | | ------ | ------ | | `strings` | `TemplateStringsArray` | | ...`keys` | `EnvKeyInput`[] | #### Returns `string` #### Example ```ts // Given API_HOST=api.example.com and API_PORT=8080 in environment const endpoint = Envapter.resolve`Connecting to ${'API_HOST'}:${'API_PORT'}`; // Returns: "Connecting to api.example.com:8080" // Works with template variables in .env too: // API_URL=https://${API_HOST}:${API_PORT} const message = Envapter.resolve`Service endpoint: ${'API_URL'}`; // Returns: "Service endpoint: https://api.example.com:8080" // reads READONLY_URL when present, otherwise DATABASE_URL const db = Envapter.resolve`Connecting to ${['READONLY_URL', 'DATABASE_URL']}`; ``` #### See [https://envapt.materwelon.dev/docs/templates#the-resolve-tagged-template](https://envapt.materwelon.dev/docs/templates#the-resolve-tagged-template) #### Inherited from ```ts Envapter.resolve ``` *** ### useSource() ```ts static useSource(source): void; ``` Bind the environment [Source](../type-aliases/Source.mdx). On Node the entry binds [FileSource](./FileSource.mdx) for you (a `process.env` snapshot plus the `.env` cascade). On the browser or Workers, pass a `PortableSource` (or any `Source`) before reading. Pass a `(key) => string | undefined` reader for a source that reads one key at a time and cannot list its keys. Clears and rebuilds the cache. #### Parameters | Parameter | Type | | ------ | ------ | | `source` | \| [`Source`](../type-aliases/Source.mdx) \| ((`key`) => `string` \| `undefined`) | #### Returns `void` #### See [https://envapt.materwelon.dev/docs/sources#the-sources-envapt-ships](https://envapt.materwelon.dev/docs/sources#the-sources-envapt-ships) #### Inherited from ```ts Envapter.useSource ``` --- # FileSource Default environment source on Node, Bun, and Deno: a snapshot of `process.env`. Because `supportsFiles` is `true`, envapt also loads the `.env` cascade and resolves `baseDir`. A key set in `process.env` wins over the same key in a file unless `envFileOptions.override` is `true`. `Envapter.syncProcessEnv` can copy loaded keys back to `process.env`. ## See [https://envapt.materwelon.dev/docs/sources#the-sources-envapt-ships](https://envapt.materwelon.dev/docs/sources#the-sources-envapt-ships) ## Implements - `FileCapableSource` ## Constructors ### Constructor ```ts new FileSource(): FileSource; ``` #### Returns `FileSource` ## Properties | Property | Modifier | Type | Default value | Description | | ------ | ------ | ------ | ------ | ------ | | `supportsFiles` | `readonly` | `true` | `true` | Always `true`. | ## Methods ### normalizeBaseDir() ```ts normalizeBaseDir(value): string; ``` Resolves a directory path or a module / `file:` URL to an absolute directory. A `file:` URL resolves to its containing directory. A plain path (`import.meta.dirname`) is taken as the directory itself. #### Parameters | Parameter | Type | | ------ | ------ | | `value` | `string` \| `URL` | #### Returns `string` #### Implementation of ```ts FileCapableSource.normalizeBaseDir ``` *** ### readFile() ```ts readFile(path, encoding): string | undefined; ``` Reads a file with `fs.readFileSync`, or `undefined` when it is missing or unreadable. #### Parameters | Parameter | Type | | ------ | ------ | | `path` | `string` | | `encoding` | `string` | #### Returns `string` \| `undefined` #### Implementation of ```ts FileCapableSource.readFile ``` *** ### readVars() ```ts readVars(): Record; ``` Returns a copy of `process.env`. #### Returns `Record`\<`string`, `string`\> #### Implementation of ```ts FileCapableSource.readVars ``` *** ### resolvePath() ```ts resolvePath(baseDir, candidate): string; ``` Joins a relative `.env` path onto `baseDir`. Absolute paths pass through unchanged. #### Parameters | Parameter | Type | | ------ | ------ | | `baseDir` | `string` | | `candidate` | `string` | #### Returns `string` #### Implementation of ```ts FileCapableSource.resolvePath ``` *** ### writeVars() ```ts writeVars(vars): void; ``` Writes each key to `process.env`. `Envapter.syncProcessEnv` calls it. #### Parameters | Parameter | Type | | ------ | ------ | | `vars` | `Record`\<`string`, `string`\> | #### Returns `void` #### Implementation of ```ts FileCapableSource.writeVars ``` --- # PortableSource Environment source seeded from an object you provide, for every runtime without a filesystem. Pass the object straight through, such as the config your bundler injects into a browser build (Vite's `import.meta.env`, a webpack `DefinePlugin` global), the Cloudflare `env` binding, a Vercel Edge or Fastly config object, a test fixture, or any plain object. Non-string values are JSON-stringified so the converters still apply. Those values must be JSON-serializable. Without a filesystem the `.env` cascade and file APIs do not apply. ## See [https://envapt.materwelon.dev/docs/sources#the-sources-envapt-ships](https://envapt.materwelon.dev/docs/sources#the-sources-envapt-ships) ## Implements - `BareSource` ## Constructors ### Constructor ```ts new PortableSource(vars): PortableSource; ``` #### Parameters | Parameter | Type | | ------ | ------ | | `vars` | `object` | #### Returns `PortableSource` ## Properties | Property | Modifier | Type | Default value | Description | | ------ | ------ | ------ | ------ | ------ | | `supportsFiles` | `readonly` | `false` | `false` | Always `false`. | ## Methods ### readVars() ```ts readVars(): Record; ``` Returns a copy of the provided object with every value as a string. #### Returns `Record`\<`string`, `string`\> #### Implementation of ```ts BareSource.readVars ``` --- # Envapt ## Call Signature ```ts function Envapt(key, options?): EnvaptAccessorDecorator; ``` A custom converter function with a fallback (both required), or a fallback only. ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options?` | \{ `converter?`: `undefined`; `fallback`: `undefined`; \} | Configuration options | | `options.converter?` | `undefined` | - | | `options.fallback` | `undefined` | - | ### Returns `EnvaptAccessorDecorator`\<`string` \| `undefined`\> ### Example ```ts class Config { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static accessor adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static accessor logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TFallback` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TFallback` \| `null` \| `undefined`\>; `fallback`: `TFallback`; \} | Configuration options | | `options.converter` | [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TFallback` \| `null` \| `undefined`\> | - | | `options.fallback` | `TFallback` | - | ### Returns `EnvaptAccessorDecorator`\<`TFallback`\> ### Example ```ts class Config { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static accessor adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static accessor logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TFallback` *extends* `FallbackOnlyValue` \| `undefined` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter?`: `undefined`; `fallback`: `TFallback`; \} | Configuration options | | `options.converter?` | `undefined` | - | | `options.fallback` | `TFallback` | - | ### Returns `EnvaptAccessorDecorator`\<`FallbackOnlyReturn`\<`TFallback`\>\> ### Example ```ts class Config { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static accessor adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static accessor logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator | undefined>; ``` A custom converter function without a fallback. Omit `required` to get `undefined` for a missing value, or pass `required: true` to throw `MissingEnvValue` on missing or empty values. ### Type Parameters | Type Parameter | | ------ | | `TReturnType` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TReturnType`\>; `required?`: `false`; \} | Configuration options | | `options.converter` | [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TReturnType`\> | - | | `options.required?` | `false` | - | ### Returns `EnvaptAccessorDecorator`\<`NonNullable`\<`TReturnType`\> \| `undefined`\> ### Example ```ts class Config { \@Envapt('MAINTENANCE_UNTIL', { converter: (raw) => new Date(raw) }) static accessor maintenanceUntil: Date | undefined; \@Envapt('JWT_SECRET', { converter: (raw) => Buffer.from(raw, 'base64'), required: true }) static accessor jwtSecret: Buffer; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TReturnType` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TReturnType`\>; `required`: `true`; \} | Configuration options | | `options.converter` | [`ConverterFunction`](../type-aliases/ConverterFunction.mdx)\<`TReturnType`\> | - | | `options.required` | `true` | - | ### Returns `EnvaptAccessorDecorator`\<`NonNullable`\<`TReturnType`\>\> ### Example ```ts class Config { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static accessor adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static accessor logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator>; ``` A built-in or array converter with a fallback, or `required: true`. The converter sets the fallback type, for example a number or time-string for `Converters.Time` and a `URL` instance for `Converters.Url`. ### Type Parameters | Type Parameter | | ------ | | `TConverter` *extends* `ConverterToken` \| `ArrayOf`\<`ArrayElement`\> | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \| \{ `converter`: `TConverter`; `fallback`: `InferConverterFallbackType`\<`TConverter`\>; `required?`: `false`; \} \| \{ `converter`: `TConverter`; `required`: `true`; \} | Configuration options | ### Returns `EnvaptAccessorDecorator`\<`InferConverterReturnType`\<`TConverter`\>\> ### Example ```ts import { Converters } from 'envapt'; class Config { \@Envapt('APP_PORT', { converter: Converters.Port, fallback: 3000 }) static accessor port: number; \@Envapt('APP_URL', { converter: Converters.Url, fallback: new URL('http://localhost:3000') }) static accessor url: URL; // reads CANARY_URL when present, otherwise APP_URL \@Envapt(['CANARY_URL', 'APP_URL'], { converter: Converters.Url }) static accessor canaryUrl: URL | undefined; // Time takes a number (milliseconds) or a time-string fallback (``) \@Envapt('REQUEST_TIMEOUT', { converter: Converters.Time, fallback: '10s' }) static accessor requestTimeout: number; \@Envapt('ALLOWED_ORIGINS', { converter: Converters.array({ of: Converters.String }), fallback: ['https://example.com'] }) static accessor allowedOrigins: string[]; \@Envapt('DATABASE_URL', { converter: Converters.Url, required: true }) static accessor databaseUrl: URL; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator | undefined>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TConverter` *extends* `ConverterToken` \| `ArrayOf`\<`ArrayElement`\> | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: `TConverter`; `fallback?`: `undefined`; `required?`: `false`; \} | Configuration options | | `options.converter` | `TConverter` | - | | `options.fallback?` | `undefined` | - | | `options.required?` | `false` | - | ### Returns `EnvaptAccessorDecorator`\<`InferConverterReturnType`\<`TConverter`\> \| `undefined`\> ### Example ```ts class Config { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static accessor adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static accessor logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator; ``` Required, no converter (raw string). Throws `MissingEnvValue` on first access when the env value is missing or empty. Pairing `required: true` with `fallback` matches no overload at compile time. A call that bypasses the types throws `InvalidUserDefinedConfig` at runtime. ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `required`: `true`; \} | `{ required: true }` | | `options.required` | `true` | - | ### Returns `EnvaptAccessorDecorator`\<`string`\> ### Example ```ts class Config { \@Envapt('API_KEY', { required: true }) static accessor apiKey: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator | undefined>; ``` A Standard Schema v1 schema (zod, valibot, arktype, or hand-written). Synchronous schemas only. A `validate` that returns a Promise throws `InvalidUserDefinedConfig` at runtime. Pairing `schema` with `converter` matches no overload at compile time. A call that bypasses the types throws `InvalidUserDefinedConfig` at runtime. ### Type Parameters | Type Parameter | | ------ | | `Schema` *extends* [`StandardSchemaV1`](../interfaces/StandardSchemaV1.mdx)\<`unknown`, `unknown`\> | ### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `options` | \{ `fallback?`: `undefined`; `required?`: `false`; `schema`: `SchemaConstraint`\<`Schema`\>; \} | | `options.fallback?` | `undefined` | | `options.required?` | `false` | | `options.schema` | `SchemaConstraint`\<`Schema`\> | ### Returns `EnvaptAccessorDecorator`\<`InferSchemaOutput`\<`Schema`\> \| `undefined`\> ## Call Signature ```ts function Envapt(key, options): EnvaptAccessorDecorator>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `Schema` *extends* [`StandardSchemaV1`](../interfaces/StandardSchemaV1.mdx)\<`unknown`, `unknown`\> | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \| \{ `fallback`: `InferSchemaOutput`\<`Schema`\>; `required?`: `false`; `schema`: `SchemaConstraint`\<`Schema`\>; \} \| \{ `required`: `true`; `schema`: `SchemaConstraint`\<`Schema`\>; \} | Configuration options | ### Returns `EnvaptAccessorDecorator`\<`InferSchemaOutput`\<`Schema`\>\> ### Example ```ts class Config { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static accessor adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static accessor logFile: string; } ``` --- # merge ```ts function merge(...members): Source; ``` Combine several sources into one. A later member's `readVars()` value overrides an earlier member's for the same key. A member may be a `(key) => string | undefined` reader for a source that reads one key at a time, and it fills a key missing from every snapshot. Bind the result with `Envapter.useSource`. Throws [EnvaptErrorCodes.InvalidMergedSource](../variables/EnvaptErrorCodes.mdx#property-invalidmergedsource) with no members or with more than one file-backed member. ## Parameters | Parameter | Type | | ------ | ------ | | ...`members` | ( \| [`Source`](../type-aliases/Source.mdx) \| ((`key`) => `string` \| `undefined`))[] | ## Returns [`Source`](../type-aliases/Source.mdx) ## See [https://envapt.materwelon.dev/docs/sources#combining-sources](https://envapt.materwelon.dev/docs/sources#combining-sources) --- # envapt ## Namespaces - [StandardSchemaV1](./namespaces/StandardSchemaV1/index.mdx) ## Classes - [Envapter](./classes/Envapter.mdx) - [EnvaptError](./classes/EnvaptError.mdx) - [FileSource](./classes/FileSource.mdx) - [PortableSource](./classes/PortableSource.mdx) ## Interfaces - [EnvaptIssue](./interfaces/EnvaptIssue.mdx) - [EnvFileOptions](./interfaces/EnvFileOptions.mdx) - [EnvProfile](./interfaces/EnvProfile.mdx) - [StandardSchemaV1](./interfaces/StandardSchemaV1.mdx) ## Type Aliases - [ConverterFunction](./type-aliases/ConverterFunction.mdx) - [DebugLevel](./type-aliases/DebugLevel.mdx) - [EnvaptErrorCodes](./type-aliases/EnvaptErrorCodes.mdx) - [Environment](./type-aliases/Environment.mdx) - [FileApiMode](./type-aliases/FileApiMode.mdx) - [JsonValue](./type-aliases/JsonValue.mdx) - [ProfilesConfig](./type-aliases/ProfilesConfig.mdx) - [Source](./type-aliases/Source.mdx) - [TimeFallback](./type-aliases/TimeFallback.mdx) ## Variables - [Converters](./variables/Converters.mdx) - [EnvaptErrorCodes](./variables/EnvaptErrorCodes.mdx) - [Environment](./variables/Environment.mdx) - [ENVIRONMENT\_KEYS](./variables/ENVIRONMENT_KEYS.mdx) ## Functions - [Envapt](./functions/Envapt.mdx) - [merge](./functions/merge.mdx) --- # EnvFileOptions Options for the `.env` loader, matching the dotenv `config()` options of the same name. For debug output, use `Envapter.debug` (or the `ENVAPT_DEBUG` env var). ## See [https://envapt.materwelon.dev/docs/env-files#file-options](https://envapt.materwelon.dev/docs/env-files#file-options) ## Properties | Property | Type | Description | | ------ | ------ | ------ | | `encoding?` | `string` | Encoding for reading .env files. Defaults to 'utf8'. | | `override?` | `boolean` | When `true`, each file's values replace earlier files' values and values already in `process.env` for envapt's reads. `process.env` itself changes only with `syncProcessEnv`. With the default `false`, the first value for each key stays. | --- # EnvProfile Per-environment profile entry passed to `Envapter.configureProfiles`. ## See [https://envapt.materwelon.dev/docs/env-files#profiles](https://envapt.materwelon.dev/docs/env-files#profiles) ## Properties | Property | Type | Description | | ------ | ------ | ------ | | `paths` | `string` \| `string`[] | One or more `.env` paths to load for this environment. Earlier paths take precedence unless `envFileOptions.override` is `true`. | --- # EnvaptIssue ## Extends - [`Issue`](../namespaces/StandardSchemaV1/interfaces/Issue.mdx) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------ | ------ | ------ | ------ | ------ | | `cause?` | `readonly` | `unknown` | The value a custom parser or schema threw. Unset when the value failed without a throw. | - | | `message` | `readonly` | `string` | - | [`Issue`](../namespaces/StandardSchemaV1/interfaces/Issue.mdx).[`message`](../namespaces/StandardSchemaV1/interfaces/Issue.mdx#property-message) | | `path?` | `readonly` | readonly ( \| `PropertyKey` \| [`PathSegment`](../namespaces/StandardSchemaV1/interfaces/PathSegment.mdx))[] | - | [`Issue`](../namespaces/StandardSchemaV1/interfaces/Issue.mdx).[`path`](../namespaces/StandardSchemaV1/interfaces/Issue.mdx#property-path) | --- # StandardSchemaV1 Standard Schema V1 interface. Inlined verbatim from the spec at https://standardschema.dev so envapt has zero runtime peer dependencies on any specific schema library (zod / valibot / arktype / etc). envapt accepts synchronous schemas only. A `validate` that returns a Promise fails to type-check and throws `InvalidUserDefinedConfig` at runtime. ## See [https://envapt.materwelon.dev/docs/standard-schema#any-conformant-validator-or-none](https://envapt.materwelon.dev/docs/standard-schema#any-conformant-validator-or-none) ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `Input` | `unknown` | | `Output` | `Input` | ## Properties | Property | Modifier | Type | Description | | ------ | ------ | ------ | ------ | | `~standard` | `readonly` | [`Props`](../namespaces/StandardSchemaV1/interfaces/Props.mdx)\<`Input`, `Output`\> | The Standard Schema entry point holding the validator and the inferred input/output types. | --- # StandardSchemaV1 ## Interfaces - [FailureResult](./interfaces/FailureResult.mdx) - [Issue](./interfaces/Issue.mdx) - [PathSegment](./interfaces/PathSegment.mdx) - [Props](./interfaces/Props.mdx) - [SuccessResult](./interfaces/SuccessResult.mdx) - [Types](./interfaces/Types.mdx) ## Type Aliases - [InferInput](./type-aliases/InferInput.mdx) - [InferOutput](./type-aliases/InferOutput.mdx) - [Result](./type-aliases/Result.mdx) --- # FailureResult ## Properties | Property | Modifier | Type | | ------ | ------ | ------ | | `issues` | `readonly` | readonly [`Issue`](./Issue.mdx)[] | --- # Issue ## Extended by - [`EnvaptIssue`](../../../interfaces/EnvaptIssue.mdx) ## Properties | Property | Modifier | Type | | ------ | ------ | ------ | | `message` | `readonly` | `string` | | `path?` | `readonly` | readonly (`PropertyKey` \| [`PathSegment`](./PathSegment.mdx))[] | --- # PathSegment ## Properties | Property | Modifier | Type | | ------ | ------ | ------ | | `key` | `readonly` | `PropertyKey` | --- # Props ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `Input` | `unknown` | | `Output` | `Input` | ## Properties | Property | Modifier | Type | | ------ | ------ | ------ | | `types?` | `readonly` | [`Types`](./Types.mdx)\<`Input`, `Output`\> | | `validate` | `readonly` | (`value`) => \| [`Result`](../type-aliases/Result.mdx)\<`Output`\> \| `Promise`\<[`Result`](../type-aliases/Result.mdx)\<`Output`\>\> | | `vendor` | `readonly` | `string` | | `version` | `readonly` | `1` | --- # SuccessResult ## Type Parameters | Type Parameter | | ------ | | `Output` | ## Properties | Property | Modifier | Type | | ------ | ------ | ------ | | `issues?` | `readonly` | `undefined` | | `value` | `readonly` | `Output` | --- # Types ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `Input` | `unknown` | | `Output` | `Input` | ## Properties | Property | Modifier | Type | | ------ | ------ | ------ | | `input` | `readonly` | `Input` | | `output` | `readonly` | `Output` | --- # InferInput ```ts type InferInput = NonNullable["input"]; ``` ## Type Parameters | Type Parameter | | ------ | | `Schema` *extends* [`StandardSchemaV1`](../../../interfaces/StandardSchemaV1.mdx) | --- # InferOutput ```ts type InferOutput = NonNullable["output"]; ``` ## Type Parameters | Type Parameter | | ------ | | `Schema` *extends* [`StandardSchemaV1`](../../../interfaces/StandardSchemaV1.mdx) | --- # Result ```ts type Result = | SuccessResult | FailureResult; ``` ## Type Parameters | Type Parameter | | ------ | | `Output` | --- # ConverterFunction ```ts type ConverterFunction = (raw) => TReturn & object; ``` Custom converter for an environment variable or an array element. It receives a present value with templates already expanded. envapt applies the fallback itself. A return of `undefined` or `null` means the value could not be converted. From `getAs` and an optional `@Envapt`, an error the converter throws propagates to the caller unchanged. A required read reports it as an `InvalidEnvValue` issue carrying its message. ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `TReturn` | `unknown` | ## Type Declaration | Name | Type | | ------ | ------ | | `~standard?` | `never` | | `prototype?` | `undefined` | ## See [https://envapt.materwelon.dev/docs/converters#custom-converters](https://envapt.materwelon.dev/docs/converters#custom-converters) --- # DebugLevel ```ts type DebugLevel = "silent" | "warn" | "verbose"; ``` Debug log levels for [Envapter.debug](../classes/Envapter.mdx#debug). `silent` (default) logs nothing. `warn` logs failed file reads, unresolved templates outside strict mode, an environment that falls back to development, a schema that returned a Promise, and a missing or empty variable read through `get`, a typed getter, `getAs`, or a decorated field without `required`. `verbose` also logs each loaded file and key, the effective `.env` paths, cache rebuilds, keys mirrored to `process.env`, and each key whose value failed to convert. ## See [https://envapt.materwelon.dev/docs/errors#debug-logging](https://envapt.materwelon.dev/docs/errors#debug-logging) --- # EnvaptErrorCodes ```ts type EnvaptErrorCodes = typeof EnvaptErrorCodes[keyof typeof EnvaptErrorCodes]; ``` One of the numeric [EnvaptErrorCodes](../variables/EnvaptErrorCodes.mdx) values, such as `210`. --- # Environment ```ts type Environment = typeof Environment[keyof typeof Environment]; ``` One of the [Environment](../variables/Environment.mdx) values: `'development'`, `'staging'`, `'production'`, or `'test'`. --- # FileApiMode ```ts type FileApiMode = "warn" | "throw"; ``` How the portable build's filesystem-only config APIs behave when called. `'warn'` (the default) warns once and no-ops. `'throw'` throws `FileApiUnsupported`. The node build runs these APIs normally and is unaffected by this value. ## See [https://envapt.materwelon.dev/docs/compatibility#binding-by-runtime](https://envapt.materwelon.dev/docs/compatibility#binding-by-runtime) --- # JsonValue ```ts type JsonValue = JsonPrimitive | JsonArray | JsonObject; ``` The value `Converters.Json` returns. ## See [https://envapt.materwelon.dev/docs/converters#json](https://envapt.materwelon.dev/docs/converters#json) --- # ProfilesConfig ```ts type ProfilesConfig = Partial> & object; ``` Configuration object for `Envapter.configureProfiles`. Maps each `Environment` to an optional profile override. Unspecified environments use the default cascade (`.env.${env}.local`, `.env.${env}`, `.env.local`, `.env`). ## Type Declaration | Name | Type | Description | | ------ | ------ | ------ | | `useDefaults?` | `boolean` | Set `false` to load only the configured paths. Defaults to `true`, where the configured paths load ahead of the dotenv-flow cascade. | ## See [https://envapt.materwelon.dev/docs/env-files#profiles](https://envapt.materwelon.dev/docs/env-files#profiles) --- # Source ```ts type Source = BareSource | FileCapableSource; ``` A pluggable source of environment variables. The default [FileSource](../classes/FileSource.mdx) reads `process.env` plus the `.env` cascade. A [PortableSource](../classes/PortableSource.mdx) reads from an object you pass, such as the config a bundler injects into a browser build or the Cloudflare `env` binding on Workers. Bind one with `Envapter.useSource`. ## See [https://envapt.materwelon.dev/docs/sources#any-object-can-be-a-source](https://envapt.materwelon.dev/docs/sources#any-object-can-be-a-source) --- # TimeFallback ```ts type TimeFallback = number | `${number}${TimeUnit}`; ``` A fallback for `Converters.Time`: milliseconds as a number, or a `` string such as `'15m'`. ## See [https://envapt.materwelon.dev/docs/converters#time-and-durations](https://envapt.materwelon.dev/docs/converters#time-and-durations) --- # Converters ```ts const Converters: object; ``` Built-in converters for environment variables. A scalar token such as `Converters.Number` converts a single value. The [Converters.array](#property-array) builder converts a delimited list. ## Type Declaration | Name | Type | | ------ | ------ | | `array` | *typeof* `buildArrayConverter` | | `BigInt` | `"bigint"` | | `Boolean` | `"boolean"` | | `Date` | `"date"` | | `Email` | `"email"` | | `Float` | `"float"` | | `Integer` | `"integer"` | | `Json` | `"json"` | | `Number` | `"number"` | | `Port` | `"port"` | | `RegExp` | `"regexp"` | | `String` | `"string"` | | `Time` | `"time"` | | `Url` | `"url"` | ## Example ```ts \@Envapt('PORT', { converter: Converters.Port, fallback: 3000 }) static accessor port: number; \@Envapt('TAGS', { converter: Converters.array({ of: Converters.String, delimiter: ' ' }) }) static accessor tags: string[]; ``` ## See [https://envapt.materwelon.dev/docs/converters#built-in-tokens](https://envapt.materwelon.dev/docs/converters#built-in-tokens) --- # ENVIRONMENT_KEYS ```ts const ENVIRONMENT_KEYS: readonly ["ENVIRONMENT", "ENV", "NODE_ENV", "MODE"]; ``` The variables envapt reads to detect the [Environment](./Environment.mdx), highest precedence first. The first one with a value sets it. ## See [https://envapt.materwelon.dev/docs/environment#detecting-the-environment](https://envapt.materwelon.dev/docs/environment#detecting-the-environment) --- # EnvaptErrorCodes ```ts const EnvaptErrorCodes: object; ``` Numeric codes carried by [EnvaptError.code](../classes/EnvaptError.mdx#property-code). The hundreds digit groups them by failure: 1xx for fallbacks, 2xx for conversion and validation, 3xx for configuration and lookup. ## Type Declaration | Name | Type | Description | | ------ | ------ | ------ | | `ArrayElementConversionFailed` | `206` | Thrown when an array element fails to convert to the array's element type. | | `ArrayFallbackElementTypeMismatch` | `103` | Thrown when an array fallback mixes element types, or its elements do not match the array's element converter. | | `EmptyArrayElement` | `207` | Thrown in strict mode when an array element is empty or whitespace only. | | `EnvFilesNotFound` | `303` | Thrown when a listed env file does not exist. | | `FallbackConverterTypeMismatch` | `104` | Thrown when a fallback does not match the type its built-in converter returns. | | `FileApiUnsupported` | `306` | Thrown when a file API such as `envPaths` or `baseDir` runs on a source without files, or on the portable build with `fileApiMode = 'throw'`. | | `InvalidArrayConverterType` | `201` | Thrown when an array converter is not a `Converters.array()` token, or its `of` is neither a built-in scalar nor a function. | | `InvalidBuiltInConverter` | `202` | Thrown when a converter string is not one of the built-in tokens. | | `InvalidCustomConverter` | `203` | Thrown when a converter is neither a built-in token nor a function. | | `InvalidEnvValue` | `210` | Thrown when a required read gets a value it can't convert, a bad array element, or an error from a custom parser or schema. `getRequiredAll` throws it once for all bad values, schema fields included. | | `InvalidFallback` | `101` | Thrown when an array converter gets a fallback that is not an array. | | `InvalidKeyInput` | `304` | Thrown when the key list is empty, or a key is not a string or is blank. | | `InvalidMergedSource` | `308` | Thrown when `merge` is called with no members, or with more than one filesystem-backed source. | | `InvalidUserDefinedConfig` | `302` | Thrown when an option or setting has an invalid value, or combines options that cannot go together. | | `MalformedTimeFallback` | `105` | Thrown when a time-string fallback does not match the `` format. | | `MissingDelimiter` | `301` | Thrown when an array converter's delimiter is missing or an empty string. | | `MissingEnvValue` | `305` | Thrown when a required value is missing or empty, or in strict mode when a `${VAR}` can't resolve. | | `NoSourceBound` | `307` | Thrown when an environment value is read before a source is bound with `Envapter.useSource`. | | `SchemaThrew` | `209` | Thrown when a Standard Schema's `validate` throws in `getAs` or `@Envapt({ schema })` without `required`. A required read reports it as an `InvalidEnvValue` issue. | | `SchemaValidationFailed` | `208` | Thrown when `getAs` or `@Envapt({ schema })` without `required` reads a value its Standard Schema rejects. | ## See [https://envapt.materwelon.dev/docs/errors#codes](https://envapt.materwelon.dev/docs/errors#codes) --- # Environment ```ts const Environment: object; ``` The environment names envapt detects. The first of [ENVIRONMENT\_KEYS](./ENVIRONMENT_KEYS.mdx) with a value sets the environment, matched case-insensitively. A missing or unknown name selects `'development'`. ## Type Declaration | Name | Type | Description | | ------ | ------ | ------ | | `Development` | `"development"` | The default when no environment variable is set, or when the first one set holds an unknown name. | | `Production` | `"production"` | Selected by the value `production`. | | `Staging` | `"staging"` | Selected by the value `staging`. | | `Test` | `"test"` | Selected by the value `test`. | ## See [https://envapt.materwelon.dev/docs/environment#detecting-the-environment](https://envapt.materwelon.dev/docs/environment#detecting-the-environment) --- # Envapt ## Call Signature ```ts function Envapt(key, options?): EnvaptFieldDecorator; ``` A custom converter function with a fallback (both required), or a fallback only. ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options?` | \{ `converter?`: `undefined`; `fallback`: `undefined`; \} | Configuration options | | `options.converter?` | `undefined` | - | | `options.fallback` | `undefined` | - | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`string` \| `undefined`\> ### Example ```ts class Config extends Envapter { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static readonly adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static readonly logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TFallback` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: [`ConverterFunction`](../../envapt/type-aliases/ConverterFunction.mdx)\<`TFallback` \| `null` \| `undefined`\>; `fallback`: `TFallback`; \} | Configuration options | | `options.converter` | [`ConverterFunction`](../../envapt/type-aliases/ConverterFunction.mdx)\<`TFallback` \| `null` \| `undefined`\> | - | | `options.fallback` | `TFallback` | - | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`TFallback`\> ### Example ```ts class Config extends Envapter { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static readonly adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static readonly logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TFallback` *extends* `FallbackOnlyValue` \| `undefined` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter?`: `undefined`; `fallback`: `TFallback`; \} | Configuration options | | `options.converter?` | `undefined` | - | | `options.fallback` | `TFallback` | - | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`FallbackOnlyReturn`\<`TFallback`\>\> ### Example ```ts class Config extends Envapter { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static readonly adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static readonly logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator | undefined>; ``` A custom converter function without a fallback. Omit `required` to get `undefined` for a missing value, or pass `required: true` to throw `MissingEnvValue` on missing or empty values. ### Type Parameters | Type Parameter | | ------ | | `TReturnType` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: [`ConverterFunction`](../../envapt/type-aliases/ConverterFunction.mdx)\<`TReturnType`\>; `required?`: `false`; \} | Configuration options | | `options.converter` | [`ConverterFunction`](../../envapt/type-aliases/ConverterFunction.mdx)\<`TReturnType`\> | - | | `options.required?` | `false` | - | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`NonNullable`\<`TReturnType`\> \| `undefined`\> ### Example ```ts class Config extends Envapter { \@Envapt('MAINTENANCE_UNTIL', { converter: (raw) => new Date(raw) }) static readonly maintenanceUntil: Date | undefined; \@Envapt('JWT_SECRET', { converter: (raw) => Buffer.from(raw, 'base64'), required: true }) static readonly jwtSecret: Buffer; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TReturnType` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: [`ConverterFunction`](../../envapt/type-aliases/ConverterFunction.mdx)\<`TReturnType`\>; `required`: `true`; \} | Configuration options | | `options.converter` | [`ConverterFunction`](../../envapt/type-aliases/ConverterFunction.mdx)\<`TReturnType`\> | - | | `options.required` | `true` | - | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`NonNullable`\<`TReturnType`\>\> ### Example ```ts class Config extends Envapter { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static readonly adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static readonly logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator>; ``` A built-in or array converter with a fallback, or `required: true`. The converter sets the fallback type, for example a number or time-string for `Converters.Time` and a `URL` instance for `Converters.Url`. ### Type Parameters | Type Parameter | | ------ | | `TConverter` *extends* `ConverterToken` \| `ArrayOf`\<`ArrayElement`\> | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \| \{ `converter`: `TConverter`; `fallback`: `InferConverterFallbackType`\<`TConverter`\>; `required?`: `false`; \} \| \{ `converter`: `TConverter`; `required`: `true`; \} | Configuration options | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`InferConverterReturnType`\<`TConverter`\>\> ### Example ```ts import { Converters } from 'envapt'; class Config extends Envapter { \@Envapt('APP_PORT', { converter: Converters.Port, fallback: 3000 }) static readonly port: number; \@Envapt('APP_URL', { converter: Converters.Url, fallback: new URL('http://localhost:3000') }) static readonly url: URL; // reads CANARY_URL when present, otherwise APP_URL \@Envapt(['CANARY_URL', 'APP_URL'], { converter: Converters.Url }) static readonly canaryUrl: URL | undefined; // Time takes a number (milliseconds) or a time-string fallback (``) \@Envapt('REQUEST_TIMEOUT', { converter: Converters.Time, fallback: '10s' }) static readonly requestTimeout: number; \@Envapt('ALLOWED_ORIGINS', { converter: Converters.array({ of: Converters.String }), fallback: ['https://example.com'] }) static readonly allowedOrigins: string[]; \@Envapt('DATABASE_URL', { converter: Converters.Url, required: true }) static readonly databaseUrl: URL; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator | undefined>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `TConverter` *extends* `ConverterToken` \| `ArrayOf`\<`ArrayElement`\> | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `converter`: `TConverter`; `fallback?`: `undefined`; `required?`: `false`; \} | Configuration options | | `options.converter` | `TConverter` | - | | `options.fallback?` | `undefined` | - | | `options.required?` | `false` | - | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`InferConverterReturnType`\<`TConverter`\> \| `undefined`\> ### Example ```ts class Config extends Envapter { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static readonly adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static readonly logFile: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator; ``` Required, no converter (raw string). Throws `MissingEnvValue` on first access when the env value is missing or empty. Pairing `required: true` with `fallback` matches no overload at compile time. A call that bypasses the types throws `InvalidUserDefinedConfig` at runtime. ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \{ `required`: `true`; \} | `{ required: true }` | | `options.required` | `true` | - | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`string`\> ### Example ```ts class Config extends Envapter { \@Envapt('API_KEY', { required: true }) static readonly apiKey: string; } ``` ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator | undefined>; ``` A Standard Schema v1 schema (zod, valibot, arktype, or hand-written). Synchronous schemas only. A `validate` that returns a Promise throws `InvalidUserDefinedConfig` at runtime. Pairing `schema` with `converter` matches no overload at compile time. A call that bypasses the types throws `InvalidUserDefinedConfig` at runtime. ### Type Parameters | Type Parameter | | ------ | | `Schema` *extends* [`StandardSchemaV1`](../../envapt/interfaces/StandardSchemaV1.mdx)\<`unknown`, `unknown`\> | ### Parameters | Parameter | Type | | ------ | ------ | | `key` | `EnvKeyInput` | | `options` | \{ `fallback?`: `undefined`; `required?`: `false`; `schema`: `SchemaConstraint`\<`Schema`\>; \} | | `options.fallback?` | `undefined` | | `options.required?` | `false` | | `options.schema` | `SchemaConstraint`\<`Schema`\> | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`InferSchemaOutput`\<`Schema`\> \| `undefined`\> ## Call Signature ```ts function Envapt(key, options): EnvaptFieldDecorator>; ``` A custom converter function with a fallback (both required), or a fallback only. ### Type Parameters | Type Parameter | | ------ | | `Schema` *extends* [`StandardSchemaV1`](../../envapt/interfaces/StandardSchemaV1.mdx)\<`unknown`, `unknown`\> | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `key` | `EnvKeyInput` | Environment variable name(s) to load | | `options` | \| \{ `fallback`: `InferSchemaOutput`\<`Schema`\>; `required?`: `false`; `schema`: `SchemaConstraint`\<`Schema`\>; \} \| \{ `required`: `true`; `schema`: `SchemaConstraint`\<`Schema`\>; \} | Configuration options | ### Returns [`EnvaptFieldDecorator`](../type-aliases/EnvaptFieldDecorator.mdx)\<`InferSchemaOutput`\<`Schema`\>\> ### Example ```ts class Config extends Envapter { \@Envapt('ADMIN_EMAILS', { fallback: new Set(), converter: (raw) => new Set(raw.split(',')) }) static readonly adminEmails: Set; \@Envapt('LOG_FILE', { fallback: '/var/log/app.log' }) static readonly logFile: string; } ``` --- # legacy ## Type Aliases - [EnvaptFieldDecorator](./type-aliases/EnvaptFieldDecorator.mdx) ## Functions - [Envapt](./functions/Envapt.mdx) --- # EnvaptFieldDecorator ```ts type EnvaptFieldDecorator = (target, key) => void; ``` ## Type Parameters | Type Parameter | | ------ | | `Output` | ## Type Parameters | Type Parameter | | ------ | | `Target` | | `Key` *extends* keyof `Target` | ## Parameters | Parameter | Type | | ------ | ------ | | `target` | `Target` | | `key` | \[`Output`\] *extends* \[`Target`\[`Key`\]\] ? `Key` : `object` | ## Returns `void`