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 storeConfig storeState store
PurposeSensitive credentialsNon-sensitive configurationRuntime business state
ExamplesPasswords, tokens, certificatesFeature flags, API URLsSessions, counters, job status
ConfidentialYesNoSometimes
Changed by handlersRarelyYesYes
Logged / visible in dashboardsNeverSometimesSometimes

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

PackageBackendBest for
@purista/coreIn-memory (dev only)Local development
@purista/aws-secret-storeAWS Secrets ManagerAWS deployments
@purista/azure-secret-storeAzure Key VaultAzure deployments
@purista/gcloud-secret-storeGoogle Cloud Secret ManagerGCP deployments
@purista/infisical-secret-storeInfisicalMulti-cloud, team secrets
@purista/vault-secret-storeHashiCorp VaultEnterprise, on-premise
@purista/dapr-sdkDapr secret storePolyglot, 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

Never log secrets

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 get is enabled. Enable set and remove only 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:stripeApiKey for 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. DefaultSecretStore is 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 get is enabled unless set/remove are 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

Related

Read Next
REST API Endpoints

from Exposing Your Service