Replace references already in process.env with their resolved values at
application startup. Your existing loader, such as Nx, chooses the dotenv
files. resenv resolves the resulting environment before application code runs.
Doppler is the only built-in cloud provider. A value such as
doppler://my-project/dev/API_TOKEN tells resenv which secret to fetch. The
application then reads the actual value from process.env.API_TOKEN.
Nx or another loader sets process.env
|
resenv fetches the referenced values and replaces them in memory
|
Application starts with ordinary environment values
Resolution happens once at startup. It does not watch the provider for changes; restart the application to fetch updated values. No filename is passed to resenv, and dotenv files are never rewritten.
Requires Node.js 20 or later. This is an experimental beta, currently distributed as a package archive; it has not been published to npm.
Install the archive supplied by the maintainer in your application project:
npm install --save-exact /path/to/ciqol-resenv-0.1.0-beta.1.tgzThis records resenv in your project's manifest and lockfile. Keep the archive
available wherever that project installs dependencies. Once the beta is
published to npm, you will be able to install @ciqol/resenv@beta instead.
The CLI examples below use npx --no-install to run the copy installed in your
project. In an npm script, use resenv directly.
After installing the package, run:
npx --no-install resenv login dopplerEach developer authenticates with their own Doppler access. Secret reads use
the HTTPS API. Credentials come from DOPPLER_TOKEN, the saved OS credential,
or an existing Doppler CLI login, in that order. Startup reports missing access
and stops; it does not open a login prompt.
Native login uses the browser flow observed in Doppler's official MCP client
and saves the token in macOS Keychain or Linux Secret Service. The CLI is a
fallback; the browser protocol is not a public OAuth contract and fresh native
login is not yet verified end to end. Windows needs DOPPLER_TOKEN because
native login/storage is not implemented there. See
Doppler login and platform details.
For example, in an Nx application's .env.serve:
API_TOKEN=doppler://my-project/dev/API_TOKEN
PORT=3000The reference contains the Doppler project, config and secret name. This is
resenv's reference syntax. Plain values such as PORT pass through unchanged.
The reference itself grants no access: the authenticated identity needs
permission to read that secret. Doppler references need no dotenvx private key.
Once the environment has been loaded:
node --require @ciqol/resenv/resolve app.cjs
node --import @ciqol/resenv/resolve app.mjsThe preload replaces process.env.API_TOKEN before the application and its
imports execute. It infers Doppler from the value, so no provider flag is needed.
Alternatively, wrap a command whose environment is already populated:
npx --no-install resenv run -- node app.mjsThe wrapper supplies resolved values to the child process. It inherits stdin/stdout/stderr, preserves exit status and forwards SIGINT, SIGTERM and SIGHUP to that child.
Put the preload on the application's target, after Nx loads its dotenv files:
{
"name": "app",
"targets": {
"serve": {
"executor": "nx:run-commands",
"cache": false,
"continuous": true,
"options": {
"command": "node --require=@ciqol/resenv/resolve app.mjs",
"cwd": "{projectRoot}"
}
}
}
}Run nx serve app. Nx selects .env.serve and any applicable overrides,
then starts the application with resenv. Use your application's existing path
and target options. For an existing @nx/js:node target, add:
"runtimeArgs": ["--require", "@ciqol/resenv/resolve"]Preloading the outer nx process runs too early. A shell-wide NODE_OPTIONS
setting also affects unrelated processes. Keep the preload on the application
target. If a build needs secrets, its process needs its own integration after
its environment is loaded; values embedded in build output remain in that output.
- Only the enabled reference forms are resolved. Ordinary values pass through.
- Doppler fetches only the named secrets; repeated references share one request within a resolution pass. Values are not cached on disk.
- All lookups must succeed before the environment is changed or a child starts. Missing access, malformed references and provider failures stop startup.
- Fetched values stay literal, even if they contain
$()or another reference. - The canonical preload leaves shell expressions literal. Executing commands requires the separate opt-in described below.
- Application code can read its resolved environment. Access to the same credentials also permits other code or sessions to retrieve those secrets.
See Doppler limits and security guidance.
Call the library after your loader has populated the environment, before importing modules that read configuration:
import { resolve } from '@ciqol/resenv'
const childEnv = resolve()
// Also replace values in process.env; the returned copy can be ignored.
resolve(process.env, { replace: true })resolve(env = process.env, options = {}) always returns a new resolved
environment. replace defaults to false, leaving the input unchanged. With
replace: true, it also replaces values in the input after every lookup succeeds;
the return value is still an independent copy.
Providers are inferred from each original value, just as with /resolve and
run. Doppler is the only supported cloud provider today. No options are
required: without references, the result is an unchanged copy and no credentials
are accessed. An environment can contain several references, including different
Doppler projects/configs. Each value selects its own source; a command can supply
a Doppler token, and Doppler can supply a decryption key. Commands still require
commands: true. Returned secret values are never interpreted again.
CommonJS uses the same API through
require('@ciqol/resenv'). Both formats share the ResenvError class, whose
code identifies the failure. CLI and preload failures exit with a safe message.
Using replace: true requires process.env or writable data properties. A read-only
property, setter or Proxy target fails before any replacement is committed.
Keep the default copy mode for inputs that should remain unchanged. The Node
preloads enable in-place replacement so application imports see resolved values.
NODE_DEBUG=resenv node --require @ciqol/resenv/resolve app.mjsFor the Nx target above, use NODE_DEBUG=resenv nx serve app. Set NODE_DEBUG
when launching Node, before dotenv loading. Diagnostics use node:util's
debuglog and are off by default:
RESENV 12345: Replaced secret value for "API_TOKEN".
RESENV 12345: Done: replaced 1 secret value(s).
With no matching references, the only message is Nothing to handle. Failures
include a safe error and completion summary. Logs omit values, tokens, command
text and provider responses; no log files are created.
NODE_DEBUG=child_process (including NODE_DEBUG=*) makes Node print spawn
options containing credentials. resenv blocks its subprocess operations with
UNSAFE_DEBUG_LOGGING in that mode. Use NODE_DEBUG=resenv instead.
- Opt-in shell commands:
/resolve-commandsresolves complete$(command)values through/bin/sh. The canonical/resolvepreload leaves these values literal for both--requireand--import, including whenRESENV_COMMANDSis set in the environment.resenv run --commands -- ...enables commands alongside inferred Doppler references, as does the command preload. Use only trusted configuration; commands run with your permissions. A timeout limits execution time, but cannot undo file changes, network requests or other effects. Leave this option disabled when the environment can contain untrusted input. Commands have closed stdin, a 30-second timeout and a 64 KiB combined output limit. Trailing LF characters are removed; fetched output stays literal. Embedded forms such asprefix$(command)suffixare unsupported. - Dotenvx decryption: original
encrypted:values still trigger decryption in the resolver, including/resolve. They need a matchingDOTENV_PRIVATE_KEYorDOTENV_PRIVATE_KEY_<name>in the environment; missing or invalid keys stop startup. Doppler references do not need a dotenvx private key.
/resolve-doppler is an equivalent name for /resolve; run --doppler remains
accepted. New integrations should use the canonical entry points above.
Use one preload mode per process. Combining /resolve and /resolve-commands
or combining a preload with resenv run fails with CONFLICTING_PRELOADS to
prevent reinterpreting fetched values. Do not configure a second resolver in
the child process through NODE_OPTIONS or other launch hooks.
For both modes together, use resenv run --commands -- ... or one library call
with { commands: true }.
For checkout setup, building, testing, trying local changes in another project and submitting fixes, see CONTRIBUTING.md.
MIT. Copyright (c) 2026 ciqol contributors.