Skip to content

Secrets & Vault

The Secrets helper family reads configuration and credentials from a backend, behind one ISecretsHelper interface. Swap the provider, and the calling code stays the same.

In an application, you rarely build a provider by hand

An IGNIS app enables secrets by overriding registerSecrets() on its application class. The framework then builds the provider, hydrates static secrets into Envs, and wires rotation for you. See the Secrets & Vault deep dive and the guide. This page documents the provider family itself.

In one example

typescript
import { createSecretsHelper, SecretProviders } from '@venizia/ignis-helpers';

const secrets = await createSecretsHelper({
  provider: SecretProviders.SYSTEM_ENVS,
});
await secrets.configure();

const value = await secrets.get({ path: 'ignored', key: 'APP_ENV_DB_PASSWORD' });

Swap provider for SecretProviders.HASHICORP_VAULT (with a config: { endpoint, auth }) or SecretProviders.DOTENV_VAULT to change the backend.

The three providers

ProviderSecretProviders valueKindOptional peer
SystemEnvsHelpersystem-envsStatic (process.env)none (default)
HashiCorpVaultHelperhashicorp-vaultKV + dynamic + rotationnode-vault (@venizia/ignis-helpers/hashicorp-vault)
DotenvVaultHelperdotenv-vaultStatic (encrypted .env.vault)@dotenvx/dotenvx (@venizia/ignis-helpers/dotenv-vault)

Only HashiCorp mints dynamic, lease-bearing credentials. The other two are read-only snapshots.

How it works

  • One base class, three providers. AbstractSecretsHelper owns the provider-agnostic machinery: a TTL cache, a lease registry, a renewal scheduler, and rotation dispatch. Each concrete provider implements only the raw fetch/renew/revoke calls.
  • Static reads are TTL-cached. get() and getBundle() cache by path, for cacheTtlSeconds (default 300).
  • The cache evicts lazily, not on a timer. An expired entry is only cleared the next time you call get() or getBundle() for that path - there's no background sweep.
  • Dynamic secrets renew themselves. lease() opens a lease-bearing secret, and schedules its own renewal at ttlSeconds * renewBeforeRatio (default ratio 0.66). Only HashiCorp supports lease() - the static providers throw.
  • A failed renewal retries with backoff, then rotates. If Vault is unreachable, the scheduler retries with capped exponential backoff. Once a renewal genuinely can't extend the lease, the helper fetches a fresh one and dispatches rotation.
  • createSecretsHelper({ provider }) picks the concrete class for you.
  • The peer packages stay invisible to bundlers. node-vault and @dotenvx/dotenvx load only through a dynamic import (ModuleUtility.load). Importing @venizia/ignis-helpers never requires either package.
  • This holds under Bun.build too. A literal dynamic import resolves at bundle time, so a compiled binary needs no external entry for either peer.

The ISecretsHelper interface

MethodPurpose
configure()Authenticate / prepare the backend (run before use)
get({ path, key?, defaultValue? })Read one static value (TTL-cached)
getBundle({ path })Read a whole key-value bundle at a path
lease({ path, key })Open a dynamic, lease-bearing secret (HashiCorp only)
onRotate(handler)Subscribe to rotation events
registerRotatable({ key, target })Connect a live consumer (a pool) to a lease key
shutdown()Stop renewal timers and revoke outstanding leases

Common tasks

Read a static value or a bundle

typescript
const password = await secrets.get({ path: 'secret/data/app', key: 'DB_PASSWORD' });
const bundle = await secrets.getBundle({ path: 'secret/data/app' });

Connect to HashiCorp Vault

Install the peer first: bun add node-vault. Vault supports three auth methods.

auth.methodFields
tokentoken
app-roleroleId, secretId, mountPath?
kubernetesrole, jwtPath?, mountPath?
typescript
const secrets = await createSecretsHelper({
  provider: SecretProviders.HASHICORP_VAULT,
  config: {
    endpoint: 'https://vault.internal:8200',
    auth: { method: 'app-role', roleId, secretId },
  },
});
await secrets.configure();

Open a dynamic, rotating credential

typescript
const lease = await secrets.lease({
  path: 'database/creds/app-role',
  key: 'datasources.PostgresDataSource',
});

secrets.onRotate(({ key, lease }) => {
  logger.for('secrets').info('Rotated | key: %s | ttl: %d', key, lease.ttlSeconds);
});

Rebuild a live consumer when its secret rotates

registerRotatable connects a pool - or any ISecretRotatable - directly to a lease key. It rebuilds on rotation without going through onRotate.

typescript
secrets.registerRotatable({ key: 'datasources.PostgresDataSource', target: pool });

Read a static .env.vault

typescript
const secrets = await createSecretsHelper({
  provider: SecretProviders.DOTENV_VAULT,
  config: { path: '.env.vault', dotenvKey: process.env.DOTENV_KEY },
});

Shut down cleanly

shutdown() clears every renewal timer and revokes every outstanding lease. Call it on application stop.

typescript
await secrets.shutdown();

See also

Files: packages/helpers/src/modules/secrets