Using secret stores
Fetch secrets from 1Password, Vault, Doppler, AWS Secrets Manager, and others, then read them typed. envapt does not fetch secrets, it reads what you bind.
envapt does not fetch secrets, contact any remote API, or manage credentials. It reads typed values on top of whatever object is bound as the active source. To use a secret store, get your secrets into envapt one of two ways.
- Wrapper CLIs (
op run,doppler run,infisical run,bws run) inject secrets as real environment variables before your process starts. On Node, envapt reads them fromprocess.envwith no extra code. - SDKs and HTTP APIs hand you the secrets in your own code. You fetch them at boot, build a plain object, then bind it as a source.
A source's readVars() is synchronous, so finish the async fetch before you call useSource. Await the SDK call,
reduce the result to a Record<string, string>, then bind that object.
Wrapper CLIs, zero code on Node
A wrapper CLI resolves your secrets and execs your process with them already set as environment variables. The default Node source reads them like any other variable, so the app code is unchanged.
# references, not the secrets themselves
DB_PASSWORD=op://prod/db/password
API_BASE_URL=op://prod/api/base-urlop run --env-file=secrets.env -- node dist/server.js// op set these in process.env before the process started
const = .('DB_PASSWORD');
const = .('API_BASE_URL', .);The same shape works with doppler run -- node dist/server.js, infisical run -- ..., and bws run -- .... Nothing binds a source, because the secrets are already in process.env.
Fetch at boot, then bind
When you fetch with an SDK or HTTP call, await it, reduce the result to a flat object, then bind it. Reads after that are the normal typed API.
declare const : <string, string>; // already fetched and awaited
.({ : () => });
const = .('DATABASE_URL', .);
const = .('DB_POOL_SIZE', 10);On Node, useSource replaces the default Node source, so the .env cascade and the file-only config APIs
(envPaths, baseDir, configureProfiles) stop working after the swap. Bind a store source only if you do not
also need those. To keep both, see the bridge pattern below.
Inline object or ManualEnvSource
ManualEnvSource runs coerceToStringRecord at construction, so non-string values (numbers, booleans, objects) are JSON-stringified before they reach envapt's cache. The bare { readVars: () => secrets } form skips that step, so any non-string in the payload reaches the converters unparsed. Prefer ManualEnvSource unless you know the payload is already all strings.
declare const : <string, string>;
.(new ());Per-store examples
Each example fetches a flat Record<string, string> and binds it. Pin and verify each SDK against its own docs before shipping, the snippets reflect the API at the time of writing.
Each example reads one or two bootstrap credentials from process.env to initialize the SDK. These have to exist before envapt's source is bound, so they are the narrow exception to routing all config through envapt.
AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { Envapter, ManualEnvSource, Converters } from 'envapt';
// This should be done early in your app, before any envapt read
const client = new SecretsManagerClient();
const { SecretString } = await client.send(new GetSecretValueCommand({ SecretId: 'prod/myapp' }));
if (!SecretString) throw new Error('secret is binary, not a string');
const secrets = JSON.parse(SecretString) as Record<string, string>;
Envapter.useSource(new ManualEnvSource(secrets));
// Now use envapt anywhere in your project
const dbUrl = Envapter.getUsing('DATABASE_URL', Converters.Url);Store the secret as one flat JSON object so JSON.parse returns a Record. The client reads credentials and region from the standard AWS credential chain.
HashiCorp Vault
import vault from 'node-vault';
import { Envapter, ManualEnvSource, Converters } from 'envapt';
const vc = vault({ endpoint: process.env.VAULT_ADDR });
const { auth } = await vc.approleLogin({
role_id: process.env.VAULT_ROLE_ID,
secret_id: process.env.VAULT_SECRET_ID
});
vc.token = auth.client_token;
const { data } = await vc.read('secret/data/myapp');
const secrets = data.data as Record<string, string>;
Envapter.useSource(new ManualEnvSource(secrets));
const gcpServiceAccount = Envapter.getUsing('GCP_SA_JSON', Converters.Json);HashiCorp ships no official Node client, node-vault is community-maintained. For KV v2 the payload sits at data.data, two levels deep.
1Password
Use op run (above) to avoid an SDK dependency, or fetch with the SDK and a service-account token.
import sdk from '@1password/sdk';
import { Envapter, ManualEnvSource } from 'envapt';
const op = await sdk.createClient({
auth: process.env.OP_SERVICE_ACCOUNT_TOKEN,
integrationName: 'my-app',
integrationVersion: 'v1.0.0'
});
const refs = {
DB_PASSWORD: 'op://prod/db/password',
API_KEY: 'op://prod/api/key'
};
const secrets: Record<string, string> = {};
for (const [key, ref] of Object.entries(refs)) {
secrets[key] = await op.secrets.resolve(ref);
}
Envapter.useSource(new ManualEnvSource(secrets));
const dbPassword = Envapter.get('DB_PASSWORD');@1password/sdk is pre-1.0, pin the version. It needs a service-account token in OP_SERVICE_ACCOUNT_TOKEN.
Doppler
Use doppler run -- node dist/server.js for the zero-code path, or download the secrets over HTTP.
import { Envapter, ManualEnvSource, Converters } from 'envapt';
const url = `https://api.doppler.com/v3/configs/config/secrets/download?project=${project}&config=${config}&format=json`;
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.DOPPLER_TOKEN}` } });
const secrets = (await res.json()) as Record<string, string>;
Envapter.useSource(new ManualEnvSource(secrets));
const corsOrigins = Envapter.getUsing('ALLOWED_ORIGINS', Converters.array({ of: Converters.Url }));Confirm the auth scheme against Doppler's API reference. Older docs used HTTP Basic with the token as the username and an empty password.
Infisical
import { InfisicalSDK } from '@infisical/sdk';
import { Envapter, ManualEnvSource, Converters } from 'envapt';
const client = new InfisicalSDK();
await client.auth().universalAuth.login({
clientId: process.env.INFISICAL_CLIENT_ID!,
clientSecret: process.env.INFISICAL_CLIENT_SECRET!
});
const { secrets } = await client.secrets().listSecrets({ projectId: 'YOUR_PROJECT_ID', environment: 'prod' });
const vars = Object.fromEntries(secrets.map((s) => [s.secretKey, s.secretValue]));
Envapter.useSource(new ManualEnvSource(vars));
const cacheTtl = Envapter.getUsing('CACHE_TTL', Converters.Time, '15m');The secret shape uses secretKey and secretValue. Use @infisical/sdk (v5+), not the unmaintained infisical-node.
Bitwarden Secrets Manager
import { BitwardenClient, DeviceType, LogLevel } from '@bitwarden/sdk-napi';
import { Envapter, ManualEnvSource } from 'envapt';
const client = new BitwardenClient(
{ apiUrl: 'https://api.bitwarden.com', identityUrl: 'https://identity.bitwarden.com', deviceType: DeviceType.SDK },
LogLevel.Info
);
await client.auth().loginAccessToken(process.env.BW_ACCESS_TOKEN!);
const { data } = await client.secrets().list(process.env.BW_ORG_ID!);
const full = await client.secrets().getByIds(data.map((s) => s.id));
const vars = Object.fromEntries(full.data.map((s) => [s.key, s.value]));
Envapter.useSource(new ManualEnvSource(vars));
const smtpPort = Envapter.getNumber('SMTP_PORT', 587);list() returns identifiers only, fetch the values with getByIds. The package ships native binaries, so it runs on Node only, and it is in beta.
Bridge through process.env on Node
To keep the default Node source and the .env cascade, write the fetched secrets onto process.env instead of calling useSource. This only works if the Object.assign runs before the first envapt read, before Envapter.load(), and before importing envapt/config. The cache builds once on first access and ignores any later writes to process.env.
import { Envapter, Converters } from 'envapt';
declare const secrets: Record<string, string>; // fetched and awaited
Object.assign(process.env, secrets);
const dbUrl = Envapter.getUsing('DATABASE_URL', Converters.Url);If the fetch cannot finish before that first read, write a custom source that implements FileEnvSource with supportsFiles set to true. It keeps the .env cascade running alongside your fetched values. See Sources.