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
| 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 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, 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 (307), whatever fileApiMode is.
| Runtime | Condition | Config comes from | Note |
|---|---|---|---|
| Cloudflare Workers | workerd | the env object | bind once at module load |
| Vercel Edge | edge-light | process.env, which holds your variables at runtime there | Vercel's Node.js runtime uses the node build |
| Fastly Compute | fastly | env(), one key at a time | pass env to useSource, and add fastly to your bundler's conditions |
| Expo and React Native | 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 | 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
exportsmap, so the first one your tool sets wins. If your bundler sets bothbrowserandnode, you get the portable build. - Only the node build exports
FileSource. Under a portable condition, importingFileSourcefromenvaptfails. envapt/legacyfollows the same conditions.envapt/configexists 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.
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 (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 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 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 | loads with experimentalDecorators on |
| Workers, browser, edge | your bundler has to compile them. Wrangler does by default, see Workers and Browser | your bundler, with experimentalDecorators on |
{ "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).