Version history for @goenvless/env, the runtime package your application imports. The envless CLI ships separately as @goenvless/cli. Pin it in your projects for the latest features, security patches, and performance improvements.
This is not the SDK. The API client is @goenvless/sdk, a separate package with its own changelog, and its Python and Ruby twins, both envless-sdk, and its PHP twin, envless/sdk, have their own: Python, Ruby and PHP. The Python and Ruby runtimes, both envless-env, have their own too: Python and Ruby, and so does the PHP runtime, envless/env.
v0.0.4
The runtime tells you when a newer release is out. The first time a process loads its environment, @goenvless/env asks the npm registry for the latest published version in the background. When that is newer than the installed one, it prints a single line and carries on:
ℹ @goenvless/env 0.0.5 is available, you are on 0.0.4. https://envless.cloud/docs/reference/changelogs-runtime.- It never delays or fails a load. The request runs alongside it, gives up after 2 seconds, and any failure, being offline included, is ignored
- It only runs when stdout is a terminal, so CI, containers, production logs and edge runtimes never make the request
- It runs once per process, even when several entrypoints load, for example
@goenvless/env/registerpreloaded andload()from@goenvless/env/serverawaited later - The request is a plain
GETtoregistry.npmjs.organd carries nothing about your project, your variables or your machine key - Set
ENVLESS_DISABLE_UPDATE_NOTICEto any non-empty value to turn it off
v0.0.3
Object plumbing names are reserved on the env proxy. Every name Object.prototype owns (toString, valueOf, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toLocaleString, __proto__, the __defineGetter__ family), plus then and toJSON, is now treated as plumbing rather than as a variable, and all four traps agree on it: get, has, ownKeys and getOwnPropertyDescriptor.
The env proxy
- A variable named
toJSONcan no longer stand in for the serialisation guard. The guard used to fire only when the store held no name calledtoJSON, so a variable actually namedtoJSONmadeenv.toJSONreturn that variable's value instead of throwing. A string is not callable, soJSON.stringify(env)fell back to enumerating the proxy and copied every decrypted secret into the output.toJSONis now answered before the store is consulted, so the root and/serverproxies always throw and/clientalways returns its filtered snapshot - Spreading an env that holds such a name no longer throws, and on
/clientit no longer copies the snapshot function onto the plain object.'toJSON' in envisfalse, andObject.keys(env), spread,Object.entriesand the/clientsnapshot all skip these names - The store still keeps the value. The plain record
load()resolves to still carries it, and it is still written intoprocess.envlike any other variable. Only reading it off the proxy is unavailable, so rename the variable if your code needsenv.<NAME> - Previously only
then,toString,valueOfandconstructorwere treated as plumbing, and only when the store did not hold a variable of that name
Freezing the env is refused like every other mutation
Object.freeze(env)andObject.seal(env)now throwEnvless env is read-only.through a newpreventExtensionstrap- Without that trap,
Object.freezemade the underlying target non-extensible first, and only then did theownKeysinvariant reject the variable names the trap reports. TheTypeErrorarrived after the damage was done, and because the target belongs to the exported singleton, enumeration of thatenvstayed broken for the rest of the process. Refusing the operation up front leaves the proxy intact
Unsafe names are matched without regard to case
- The loader and the injected seed now compare
UNSAFE_PROCESS_ENV_NAMESin lower case, since the Windows process environment ignores case, sopath,Node_Optionsandld_preloadare refused exactly likePATH,NODE_OPTIONSandLD_PRELOAD - As before, a refused name is still returned by
load()and still readable throughenv, it is only kept out ofprocess.env. In an injected environment the refused name is left out of the seed entirely
Client names that are not JavaScript identifiers are skipped when inlining
@goenvless/env/viteand@goenvless/env/nextboth inline a variable by writing its name into your source (import.meta.env.<NAME>and Next'sconfig.env), so a name that is not a valid identifier produces broken code. Client-visibility names now have to match^[A-Za-z_][A-Za-z0-9_]*$to be inlined, and the rest are skipped with a warning listing them- The
ENVLESS_CLIENT_ENVhydration payload still carries the skipped names, soenvfrom@goenvless/env/clientreads them by name. It isimport.meta.envandprocess.envin the browser that will not see them. Rename them using letters, numbers and underscores, and do not start with a number
v0.0.2
A wrapped process adopts the environment it already holds instead of fetching it again. When envless run or envless build starts your process, the CLI writes an ENVLESS_INJECTED manifest alongside the variables. The runtime now seeds its store from that manifest the first time anything touches it, so env reads work with no load() and no token, and load() resolves to those values rather than going to the network. Before this, a wrapped process threw the not-loaded error and load() fetched with a machine token it should not have needed.
Adopting an injected environment
- The manifest is a JSON object of
{ "<NAME>": { "t": type, "v": visibility } }. The values themselves come fromprocess.env, so the manifest carries metadata only and never a secret. Each variable keeps its configured type and visibility, which is what makes the server and client split enforceable at build time: a wrapped Vite or Next build can use the plugin with no token and no second round trip, and only variables Envless marks client reach the bundle - An entry whose name is not present in
process.envis skipped, and an entry with a missing or unrecognisedt/vfalls back tostringandservervisibility - Names that change how the process executes code are refused even when the manifest lists them, on the same list the loader uses
ENVLESS_INJECTEDitself joins that list, which grows from 14 names to 15, so a stored variable can never forge a manifest by being written intoprocess.envunder that name- A malformed manifest (not JSON, not an object, an array, or one that yields no usable entry) leaves the store empty rather than throwing, and loading falls back to fetching as usual
load({ revalidate: true })still bypasses the adopted values and fetches
New optional view
@goenvless/env/serverand@goenvless/env/clientboth exportoptionalalongsideenv.optional.Xreturnsundefinedwhen the name is not set, whereenv.XthrowsMissing env var: X. Useenvfor something the app cannot run without, andoptionalfor code that tests whether an integration is configured, the readingprocess.envgave for free- It is the same proxy with the same rules otherwise: values are still coerced by their configured type, and the
/clientview still exposes client-visibility variables only, so a server-only name still throws there - It still throws the not-loaded error before anything is loaded, so a misconfigured process fails loudly instead of reading
undefinedeverywhere - It is typed as
Partial<EnvlessServerEnv>andPartial<EnvlessClientEnv>, so your generated names come back with| undefined. The root@goenvless/enventry does not export it, andenvthere is unchanged
v0.0.1
Initial public release. @goenvless/env ships the application runtime, and the envless CLI ships separately as @goenvless/cli, a binary-only package with no importable module. The runtime fetches your ciphertext from the API and decrypts it locally (zero-knowledge, the server never sees your key), then populates process.env without clobbering existing ambient variables and without placing names that would change how code executes.
Nine entrypoints ship: ., /server, /client, /next, /vite, /eslint, /crypto, /edge and /register.
Root: @goenvless/env (six exports)
env: a proxy over your loaded variables, unfiltered and not narrowed by the generated types (those views live on/serverand/client)load(opts?): asynchronously fetch + decrypt + populateprocess.env. Takes{ revalidate? }and resolves to a plain recordencrypt(plaintext, passphrase, workspaceUniqueId)/decrypt(ciphertext, passphrase, workspaceUniqueId): Web Crypto helpers.encryptwrites thev1:<base64(iv || ciphertext)>envelope,decryptreads that and the cost-pinnedv2:<iterations>:<base64(iv || ciphertext)>formpassphraseStrength(passphrase): returns{ valid, error }against the same floor the dashboard and CLI enforce (16 characters minimum with 3 character classes, or 24 characters with 2, no run of 4 repeated or sequential characters, no dominant weak fragment)ENCRYPTION_PARAMETERS: the cryptographic constants (PBKDF2 200000 / SHA-256 → AES-GCM-256, salt namespace, version prefix) for byte-for-byte interop with non-JS implementations- This entry reaches
node:fsthrough the loader and cache, so it needs a filesystem. For crypto alone use/crypto, for a runtime with no filesystem use/edge
Subpath entrypoints
@goenvless/env/server:env(all variables, coerced by their configured type),load({ revalidate? }), and__override(values)(returns ausing-disposable that restores the previous values, handy in tests)@goenvless/env/client:envexposing only client-visibility variables. Accessing a server-only variable throws. It hydrates synchronously from the payload the Next or Vite plugin injects (ENVLESS_CLIENT_ENV/__ENVLESS_CLIENT_ENV__), so there is noload()to await in the browser@goenvless/env/register: side-effect module that eagerly runs the loader via top-level await. Usenode --import @goenvless/env/register app.jsor a Bunpreload@goenvless/env/next:withEnvless(nextConfig?)returns an async Next config function that awaitsload()(fillingprocess.envfor server/build) and inlines client-visibility variables intoconfig.env. Before it hands the config back it removes everyNEXT_PUBLIC_name Envless marks server visibility fromprocess.envand warns with the list, because Next inlines anyNEXT_PUBLIC_name it finds into client chunks whatever Envless says about it. Flag those client in Envless if they belong in the browser, or rename them@goenvless/env/vite:envless()returns a Vite plugin that awaitsload()anddefinesimport.meta.env.<NAME>for client-visibility variables only. It runs the same strip and the same warning against the project'senvPrefix, defaulting toVITE_when none is declared@goenvless/env/eslint: ESLint plugin named@goenvless/env, with the@goenvless/env/no-server-import-in-clientrule (errors when a client file imports@goenvless/env/server) and aconfigs.recommendedpreset.clientGlobsare matched against both the absolute path and the path relative to the working directory, so ordinary project-relative globs likesrc/client/**work@goenvless/env/crypto: the crypto surface on its own, pure Web Crypto with no Node built-ins and noBuffer, so it runs in a browser, a service worker or an edge runtime. Exportsencrypt,decrypt,encryptWithKey(key, plaintext),decryptWithKey(key, ciphertext),workspaceKey(passphrase, workspaceUniqueId),importWorkspaceKey(base64urlKey),passphraseStrengthandENCRYPTION_PARAMETERS@goenvless/env/edge:load(options)for runtimes with no filesystem, also the default export. Takes{ token, project, environment, product?, passphrase? | key?, workspaceId?, endpoint?, fetch? }and returns a plain record. It never writesprocess.env, keeps no cache and reads no.envlessfile, so everything it needs is passed in. With a passphrase and noworkspaceIdit resolves the workspace over the API first, since the salt is derived from it
Loading model
- Reads
ENVLESS_TOKEN(anev_sk_API key) plusENVLESS_KEY(base64url raw AES-256 key) orENVLESS_PASSPHRASE, plusENVLESS_PRODUCT/ENVLESS_PROJECT/ENVLESS_ENVslugs (falling back to a local.envlessfile for product/project/env).ENVLESS_PRODUCTis optional and only needed when the project slug exists in more than one product of the workspace - Loading is async, so use
/register(eager preload) orawait load()before readingenv./registeris the only entrypoint that loads on its own, importing the root or/serverdoes not - The on-disk ciphertext cache lives under
~/.envless/cache(directories0700, files0600) and is keyed by workspace + product + project + environment, so two projects sharing a slug in different products never read each other's entry. A cached entry is reused for 15 minutes, andload({ revalidate: true })skips it load()is memoised for the life of the process. A second call returns the same promise, so nothing is fetched or decrypted twice, and onlyload({ revalidate: true })starts a fresh one. A load that throws drops the memo, so the next call retries rather than replaying the failure- Loaded values populate
process.envwithout overwriting variables already present in the ambient environment. The loader remembers the names it wrote, so a revalidating load updates its own and deletes the ones that have since disappeared, while an ambient name it never owned is left exactly as it was - Names that change how a process or its children execute code are decrypted but never written into
process.env. All 14 are refused with a warning that lists them:NODE_OPTIONS,NODE_PATH,NODE_REPL_EXTERNAL_MODULE,ELECTRON_RUN_AS_NODE,LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT,DYLD_INSERT_LIBRARIES,DYLD_LIBRARY_PATH,DYLD_FRAMEWORK_PATH,PATH,BUN_INSPECT,BUN_INSPECT_CONNECT_TOand theENVLESS_CLIENT_ENVhydration channel. They are still returned byload()and still readable throughenv, andenvless run/envless buildrefuse the same 14 when they build a child environment
The env proxy
- Serialising a server env throws.
JSON.stringify(env)on the root or/serveris refused, because it would copy every decrypted secret straight into the output. Read the names you need, or use the plain recordload()already resolved to. The/clientproxy still serialises, since it is filtered to client-visibility variables and those are inlined into the bundle anyway - The proxy is read-only. Assigning to it, deleting a name off it, redefining a property or reprototyping it all throw.
__override(values)from/serveris the sanctioned way to layer temporary values, and it is scoped and disposable rather than a mutation - Enumeration follows the same rules as reading, so
Object.keys(env)andinon the/clientproxy show client-visibility names only
Typed codegen
- Run
envless types(from@goenvless/cli) to emitenvless-env.d.ts, augmentingNodeJS.ProcessEnv,@goenvless/env/server'sEnvlessServerEnv, and@goenvless/env/client'sEnvlessClientEnvsoenvis fully typed per variable EnvlessServerEnv/EnvlessClientEnvship empty, so a name that is not in your generated types is a compile error rather than silently typed. Runtime access is unaffected, this is a type-level check only, and it meansenvless typesis what turns on typing
Runtime
- Pure Web Crypto API, so no native dependencies, no WASM and no polyfill on Node 20+ / Bun / Deno / modern browsers
/client,/cryptoand/edgeare the browser-safe entries, asserted at build time to carry nonode:import and noBuffer. The root,/server,/next,/viteand/registerall reachnode:fsthrough the loader and cache, so they cannot run on an edge runtimerequire('@goenvless/env/register')throws by design, because a CJS require cannot block and would hand control back before a single variable is loaded. Preload the ESM entry withnode --import @goenvless/env/register, orawait load()from the root- Same encryption recipe used by the CLI and dashboard, so values encrypted here decrypt identically everywhere else