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 |
| Portable build | Workers, edge runtimes, the 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 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.
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 (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 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.
declare const : <string, unknown>; // the object the platform hands you
.(new ());
const upstream = .('UPSTREAM_URL', .);const cacheTtl = .('CACHE_TTL', ., '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.
declare const : <string, string>; // fetched from your secrets manager
const : = { : () => };
.();
const = .('API_BASE_URL', .);
const = .('DB_POOL_SIZE', ., 10);Prop
Type
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.
declare const : { (: string): string | undefined }; // a keyed config store
.(() => .());
const = .('REGION', .);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).
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.
declare const : <string, string>; // fetched and awaited
.((., new ()));
const = .('DATABASE_URL', .);Here Envapter.source keeps the .env cascade. With the default 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:
declare const : <string, unknown>; // the Worker env
declare const : <string, string>; // fetched and awaited
.((new (), new ()));
const = .('API_KEY', .);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.
declare const : { (: string): string | undefined };
.((new (), () => .()));
const = .('REDIS_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:
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 fallbackThese rules decide what the cache holds.
- The cache builds when you bind.
useSourcebuilds it when you call it, and on the node build importing envapt does the same. The.envfiles are read then. source.readVars()runs once per cache build.FileSourcecopiesprocess.envat that point, so if code setsprocess.env.FEATURE_FLAGS = 'on'afterwards, the cache keeps the old value. To make envapt read such a change, bind again withEnvapter.useSource(new FileSource()).- Some calls rebuild the cache. The rebuild runs inside the call, so a failing rebuild throws at the assignment.
Compatibility 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, 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.