v9.1.0

Compatibility

Look up the Node, Bun, and Deno versions envapt needs, which build your runtime loads, and the TypeScript settings that let you import it.

envapt is ESM only and has two builds. The node build imports node:* modules, binds a FileSource 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.

Engine versions

RuntimeMinimumTested in CI
Node22.23.322, 24, and 26 on Linux. 22 on macOS and Windows
Bun1.3.111.4.x
Deno2.52.9.x
Cloudflare Workersnone setworkerd
Browsernone setChromium

Install

Quick Start has the command for npm, pnpm, yarn, and bun. Deno can install from either registry.

RegistryDeno commandEntries
npmdeno add npm:envaptenvapt, envapt/legacy, envapt/config
JSRdeno 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, so the first read works with no setup.

RuntimeConditionReadsNote
Node and Bunnodeprocess.env and the .env cascade
Denodenoprocess.env and the .env cascade
Deno Deploydenodashboard variables, which Deno also puts on process.envno .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 (307), whatever fileApiMode is.

RuntimeConditionConfig comes fromNote
Cloudflare Workersworkerdthe env objectbind once at module load
Vercel Edgeedge-lightprocess.env, which holds your variables at runtime thereVercel's Node.js runtime uses the node build
Fastly Computefastlyenv(), one key at a timepass env to useSource, and add fastly to your bundler's conditions
Expo and React Nativereact-nativeprocess.env.EXPO_PUBLIC_* references Expo replaces at build time, or Constants.expoConfig.extraat runtime process.env holds only NODE_ENV. Metro must have package exports on, which it does by default from Metro 0.82
Browserbrowserthe object your bundler injects, such as import.meta.envthe values ship in the client bundle, so bind only public ones
Web workersworkeran object you pass to new PortableSource(...)no process.env to read

Which condition selects which build

Conditions your tool setsBuild
any of workerd, edge-light, fastly, worker, browser, react-nativeportable
deno, node, or none of the abovenode
  • 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's envPaths, baseDir, envFileOptions, configureProfiles, and resetPaths read the filesystem (.env files on Node). On the portable build they have nothing to read. Envapter.fileApiMode takes a FileApiMode and sets what each call does there.

fileApiModeOn the portable buildOn 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 (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.

. = 'throw';

On the node build this line has no effect. If you bind a PortableSource 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.

export async function (): <number> {
    const { ,  } = await import('envapt');
    return .('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 optionsenvaptenvapt/legacy
module: "nodenext" in a package with "type": "module"resolvesresolves
module: "esnext" or "preserve" with moduleResolution: "bundler"resolvesresolves
module: "node16" or "nodenext" in a CommonJS packagefails on import, resolves on import()fails on import, resolves on import()
module: "commonjs" with moduleResolution: "bundler"failsfails
moduleResolution: "node10", which needs ignoreDeprecations: "6.0" in TypeScript 6resolves through the top-level types fieldfails, 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 explains both forms.

Where the code runsModern @EnvaptLegacy @Envapt
Nodecompile first, because node app.ts fails with a SyntaxErrorcompile first, with experimentalDecorators in tsconfig.json
Bunruns a .ts file directly, bun test includedexperimentalDecorators in tsconfig.json
Denoruns a .ts file directlyexperimentalDecorators in deno.json
vitestfails with the same SyntaxError, see Testingloads with experimentalDecorators on
Workers, browser, edgeyour bundler has to compile them. Wrangler does by default, see Workers and Browseryour bundler, with experimentalDecorators on
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).

On this page