# Secret Store

Safely store and access passwords, API keys, and tokens — never in code, never in environment variables, always behind an abstraction.

---
Canonical: /handbook/stores/secret-store/
Source: web/src/content/handbook-cards/stores/secret-store.mdx
Format: Markdown for agents
---

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](/handbook/stores/config-store/).  
If the data is business state your handlers read and write → use a [state store](/handbook/stores/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:

```typescript [main.ts]
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:

```typescript [command.ts]
.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

```typescript [vault.ts]
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:

```typescript [default.ts]
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:

```typescript [custom.ts]
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

<div class="callout callout--warning">
  <div class="callout__title">Never log secrets</div>
  <p>Secret store values should never appear in logs. Use <code>context.logger</code> with redaction rules, or explicitly exclude secret fields from log payloads.</p>
</div>

- **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
