Stores — Data Persistence
Secret Store
Safely store and access passwords and API keys
The secret store is PURISTA’s abstraction for sensitive data. Passwords, API keys, tokens, and certificates belong here — never in code, never in config files, never in environment variables. The store interface lets you swap providers without changing business logic.
Secret store vs. config store vs. state store
PURISTA ships three distinct store abstractions. The decision rule is simple:
| Secret store | Config store | State store | |
|---|---|---|---|
| Purpose | Sensitive credentials | Non-sensitive configuration | Runtime business state |
| Examples | Passwords, tokens, certificates | Feature flags, API URLs | Sessions, counters, job status |
| Confidential | Yes | No | Sometimes |
| Changed by handlers | Rarely | Yes | Yes |
| Logged / visible in dashboards | Never | Sometimes | Sometimes |
If the data must never appear in logs → use a secret store.
If the data is non-sensitive configuration that operators tune → use a config store.
If the data is business state your handlers read and write → use a state store.
Why a secret store
Secrets have a short lifespan and should only be accessible when needed. The secret store ensures:
- Secrets are not included in general service configurations
- Secrets are not logged or exposed in dashboards
- Different vendor solutions can be used without vendor-specific code in business logic
Usage
Pass a secret store when creating the service instance:
import { AWSSecretStore } from '@purista/aws-secret-store'
const secretStore = new AWSSecretStore({ region: 'us-east-1' })
const userService = await userServiceV1Service.getInstance(eventBridge, {
secretStore,
})
await userService.start()
Access from commands and subscriptions:
.setCommandFunction(async function (context, payload) {
// Get one or more secrets
const secrets = await context.secrets.getSecret('paymentApiKey', 'dbPassword')
// secrets.paymentApiKey — string
// secrets.dbPassword — string
// Set a secret (if enabled)
await context.secrets.setSecret('paymentApiKey', 'new-key-value')
// Remove a secret (if enabled)
await context.secrets.removeSecret('oldApiKey')
})
Official adapters
| Package | Backend | Best for |
|---|---|---|
@purista/core | In-memory (dev only) | Local development |
@purista/aws-secret-store | AWS Secrets Manager | AWS deployments |
@purista/azure-secret-store | Azure Key Vault | Azure deployments |
@purista/gcloud-secret-store | Google Cloud Secret Manager | GCP deployments |
@purista/infisical-secret-store | Infisical | Multi-cloud, team secrets |
@purista/vault-secret-store | HashiCorp Vault | Enterprise, on-premise |
@purista/dapr-sdk | Dapr secret store | Polyglot, service-mesh |
HashiCorp Vault example
import { VaultSecretStore } from '@purista/vault-secret-store'
const secretStore = new VaultSecretStore({
endpoint: 'http://vault:8200',
token: process.env.VAULT_TOKEN,
mount: 'secret',
})
Default secret store
For local development only — never in production:
import { DefaultSecretStore } from '@purista/core'
const secretStore = new DefaultSecretStore({
enableGet: true,
enableSet: true,
enableRemove: true,
config: {
paymentApiKey: 'dev-key-only',
},
})
Custom secret store
Extend SecretStoreBaseClass to build your own:
import { SecretStoreBaseClass, StoreBaseConfig } from '@purista/core'
type MySecretConfig = { url: string }
export class MySecretStore extends SecretStoreBaseClass<MySecretConfig> {
private client
constructor(config: StoreBaseConfig<MySecretConfig>) {
super('MySecretStore', config)
this.client = customClient.connect(this.config.config.url)
}
protected async getSecretImpl<Names extends string[]>(...names: Names) {
const result: Record<string, string> = {}
for (const name of names) {
result[name] = await this.client.get(name)
}
return result as any
}
protected async setSecretImpl(name: string, value: string) {
await this.client.set(name, value)
}
protected async removeSecretImpl(name: string) {
await this.client.del(name)
}
async destroy() {
await this.client.disconnect()
super.destroy()
}
}
Security best practices
Secret store values should never appear in logs. Use context.logger with redaction rules, or explicitly exclude secret fields from log payloads.
- Separate secrets from config. Configs may be logged; secrets must not be.
- Enable only needed operations. By default, only
getis enabled. Enablesetandremoveonly for services that need them. - Rotate secrets regularly. Most providers support automatic rotation. Use it.
- Scope secrets per tenant. Use key prefixes like
tenant:acme:stripeApiKeyfor multi-tenant isolation.
When to use secret stores
- API keys for third-party services
- Database passwords and connection strings
- JWT signing certificates
- OAuth client secrets
- Any credential that would be a security incident if leaked
Common pitfalls
- Storing secrets in code or env vars. The secret store abstraction exists to prevent exactly this.
- Using the default store in production.
DefaultSecretStoreis in-memory and not persistent. Use a real provider. - Logging secret values. Always redact secrets from logs.
- Not scoping by tenant. In multi-tenant systems, unscoped secrets create cross-tenant leakage risk.
- Enabling set/remove unnecessarily. Few services need to write secrets. Default to read-only.
Checklist
- No secrets in code, config files, or environment variables
- Secret store provider matches the deployment environment
- Only
getis enabled unlessset/removeare explicitly needed - Secret values are redacted from all logs
- Multi-tenant systems scope secrets by tenant
- Secret rotation is configured and tested
- Integration tests verify the concrete provider behavior