Secrets
@modularityjs/secrets defines an abstract SecretsService for reading API keys, database passwords, JWT signing keys, and similar credentials from a managed vault. Drivers exist for HashiCorp Vault, AWS Secrets Manager, and GCP Secret Manager; a memory driver covers tests and local dev. The secrets-config extension plugs the contract into the config chain so apps can call config.get('app/stripe-key') and the value transparently comes from the configured vault in production.
Contract
abstract class SecretsService {
abstract get(name: string): Promise<string>; // throws NotFoundException
abstract has(name: string): Promise<boolean>;
}get throws NotFoundException when the secret is missing — that's almost always a configuration bug the caller cannot recover from. has lets callers (notably the config extension) probe without try/catch noise. The shape mirrors StorageService.read — same throws-on-missing rubric.
Drivers
Memory (@modularityjs/secrets-memory)
In-memory store seeded at boot. Useful for tests, and for apps that load secrets once at startup (e.g. from a CI-injected file).
import { SecretsModule } from '@modularityjs/secrets';
import { SecretsMemoryModule } from '@modularityjs/secrets-memory';
const modules = [
SecretsModule,
SecretsMemoryModule.forRoot({
entries: {
'stripe-api-key': process.env.STRIPE_API_KEY!,
'jwt-signing-key': await loadFromCISecret(),
},
}),
];HashiCorp Vault (@modularityjs/secrets-vault)
Reads KV v2 secrets via the Vault HTTP API. Token auth only in v0 — AppRole / Kubernetes auth is a follow-up. Zero SDK dependency (uses Node 22+ built-in fetch).
import { SecretsVaultModule } from '@modularityjs/secrets-vault';
const modules = [
SecretsModule,
SecretsVaultModule.forRoot({
addr: 'https://vault.example.com:8200',
token: process.env.VAULT_TOKEN!,
mount: 'secret', // KV v2 mount; default 'secret'
pathPrefix: 'app', // optional — prepended to every name
field: 'value', // KV v2 data field; default 'value'
}),
];SecretsService.get('stripe-key') reads https://vault.example.com:8200/v1/secret/data/app/stripe-key and returns the value field. Each secret should be stored as {"value": "..."} (the default field).
AWS Secrets Manager (@modularityjs/secrets-aws)
Wraps SecretsManagerClient from @aws-sdk/client-secrets-manager. Credentials come from the default AWS chain (env vars, IAM role, etc.).
import { SecretsAwsModule } from '@modularityjs/secrets-aws';
const modules = [
SecretsModule,
SecretsAwsModule.forRoot({
region: 'us-east-1',
namePrefix: 'prod/', // optional — environment isolation
// endpoint: 'http://localhost:4566', // for LocalStack
}),
];Returns SecretString (UTF-8). Binary-only secrets are reported as missing.
GCP Secret Manager (@modularityjs/secrets-gcp)
Wraps SecretManagerServiceClient from @google-cloud/secret-manager. Auth via Application Default Credentials by default; pass keyFilename for a service-account key file.
import { SecretsGcpModule } from '@modularityjs/secrets-gcp';
const modules = [
SecretsModule,
SecretsGcpModule.forRoot({
projectId: 'my-project',
version: 'latest', // or a numeric version string ('1', '7')
// keyFilename: '/etc/gcp/sa.json',
}),
];GCP secret names allow only [A-Za-z0-9_-], so the driver translates slashes to underscores: app/stripe-key → projects/my-project/secrets/app_stripe-key/versions/latest.
Config Extension
@modularityjs/secrets-config registers a ConfigSource (priority 1500, above EnvConfigSource's 1000) that pre-loads declared secret paths at boot and serves them synchronously to ConfigService.get() / .resolve().
import { ConfigModule } from '@modularityjs/config';
import { SecretsModule } from '@modularityjs/secrets';
import { SecretsAwsModule } from '@modularityjs/secrets-aws';
import { SecretsConfigModule } from '@modularityjs/secrets-config';
const modules = [
ConfigModule,
SecretsModule,
SecretsAwsModule.forRoot({ region: 'us-east-1' }),
SecretsConfigModule.forRoot({
paths: ['app/stripe-key', 'app/db-password', 'app/jwt-signing-key'],
}),
];Then in any service:
@Injectable()
class StripeService {
constructor(@Inject(ConfigService) private readonly config: ConfigService) {}
initialise() {
const apiKey = this.config.get<string>('app/stripe-key');
// …
}
}Why pre-load?
ConfigSource.get(path) is synchronous (apps call config.get() from constructors and other hot paths) but SecretsService.get() is async (network calls to Vault/AWS/GCP). The extension resolves this by fetching all declared paths once during the module's onInit and serving subsequent reads from an in-memory cache. Boot fails fast if any declared path is missing — exactly what you want for misconfiguration.
Cached values live until process exit; rotation requires an app restart in v0.
Path-name interplay
The extension passes the config path string straight to SecretsService.get(name). Each driver translates internally:
| Driver | Path app/stripe-key becomes… |
|---|---|
| memory | Lookup key app/stripe-key in entries |
| vault | https://addr/v1/{mount}/data/{pathPrefix?}/app/stripe-key |
| aws | GetSecretValue({ SecretId: '{namePrefix?}app/stripe-key' }) |
| gcp | projects/{projectId}/secrets/app_stripe-key/versions/{version} |
If your secret naming convention doesn't match, configure pathPrefix (vault), namePrefix (aws), or rename the path to match (gcp).
Failure Modes and Tuning
| Concern | Mitigation |
|---|---|
| Vault / AWS / GCP unreachable at boot | SecretsConfigModule.onInit rejects → createApp() throws — app never starts with broken config. |
| A declared secret was deleted or renamed | Same: boot fails on the next deploy with a clear NotFoundException naming the path. |
| Rotated secret in the vault | The cached value is stale until process restart. v0 has no hot-reload — restart the app on rotation. |
| Network call latency at boot | Each declared path is one round trip. 30 secrets × 50ms = 1.5s added to boot — usually fine; parallelise if not. |
| Missing IAM permissions (AWS) / role (GCP) | The SDK throws on get, propagated as-is; boot fails with the SDK's error message. |
Choosing a Driver
| Production target | Driver |
|---|---|
| AWS | secrets-aws |
| GCP | secrets-gcp |
| On-prem / hybrid | secrets-vault |
| Tests / local dev | secrets-memory |
Apps can register multiple modules — the last one in preferences: wins per the framework's standard last-wins rule. In practice you pick one driver per environment.