Stores — Data Persistence

Config Store

Non-sensitive configuration that varies by environment

The config store holds non-sensitive configuration that may change during runtime. API URLs, feature flags, timeout settings, and third-party provider endpoints all belong here — not in code, not in environment variables, and never alongside secrets.

Config store vs. secret store vs. state store

PURISTA ships three distinct store abstractions. Choose based on what the data is:

Config storeSecret storeState store
PurposeNon-sensitive configurationSensitive credentialsRuntime business state
ExamplesFeature flags, API URLsPasswords, tokens, certificatesSessions, counters, job status
ConfidentialNoYesSometimes
Changed by handlersYesRarelyYes
Validated automaticallyNo (validate yourself)NoNo

If the data is a credential → use a secret store.
If the data is business state your handlers read and write → use a state store.
If the data is non-sensitive configuration that operators tune → use a config store.

Config store vs. service config

PURISTA offers two ways to provide configuration. Choose based on mutability and type safety needs:

Config storeService config
Typed out of the boxNoYes (Zod schema)
Validated out of the boxNoYes
Changes during runtimeYesNo
Distributed / sharedYesNo

Use service config for values needed at startup that never change: database connection strings, service name, port numbers.

Use config store for values that might change without a restart: third-party API URLs, feature flags, rate limits.

Usage

Pass a config store when creating the service instance:

import { DaprConfigStore } from '@purista/dapr-sdk'

const configStore = new DaprConfigStore({ configStoreName: 'app-config' })

const userService = await userServiceV1Service.getInstance(eventBridge, {
  configStore,
})
await userService.start()

Access from commands and subscriptions:

.setCommandFunction(async function (context, payload) {
  // Get one or more config values
  const config = await context.configs.getConfig('thirdPartyApiUrl', 'requestTimeout')

  // config.thirdPartyApiUrl — string
  // config.requestTimeout — number

  // Set a config value at runtime
  await context.configs.setConfig('featureFlag:newDashboard', true)

  // Remove a config value
  await context.configs.removeConfig('deprecated:oldEndpoint')
})

Validate config reads

Config stores return unknown values. Validate them in production:

import { z } from 'zod'

const apiConfigSchema = z.object({
  hostUrl: z.string().url(),
  port: z.number().int().min(1).max(65535),
})

.setCommandFunction(async function (context, payload) {
  const raw = await context.configs.getConfig('hostUrl', 'port')
  const config = apiConfigSchema.parse(raw)

  // config is now fully typed and validated
  const response = await fetch(`${config.hostUrl}:${config.port}/api/v1/data`)
})

Default config store

For local development, use the in-memory default:

import { DefaultConfigStore } from '@purista/core'

const configStore = new DefaultConfigStore({
  enableGet: true,
  enableSet: true,
  enableRemove: true,
  config: {
    thirdPartyApiUrl: 'https://api.example.com',
    requestTimeout: 5000,
  },
})

Official adapters

PackageBackendBest for
@purista/coreIn-memoryDevelopment, testing
@purista/aws-config-storeAWS Systems Manager Parameter StoreAWS deployments
@purista/nats-config-storeNATS KVNATS-first platforms
@purista/redis-config-storeRedisRedis-first platforms
@purista/dapr-sdkDapr configurationPolyglot, service-mesh

Custom config store

Extend ConfigStoreBaseClass to build your own:

import { ConfigStoreBaseClass, StoreBaseConfig } from '@purista/core'

type MyConfigConfig = { url: string }

export class MyConfigStore extends ConfigStoreBaseClass<MyConfigConfig> {
  private client

  constructor(config?: StoreBaseConfig<MyConfigConfig>) {
    super('MyConfigStore', config)
    this.client = customClient.connect(this.config.config.url)
  }

  protected async getConfigImpl<Names extends string[]>(...names: Names) {
    const result: Record<string, unknown> = {}
    for (const name of names) {
      const value = await this.client.get(name)
      result[name] = value ? JSON.parse(value) : undefined
    }
    return result as any
  }

  protected async setConfigImpl(name: string, value: unknown) {
    await this.client.set(name, JSON.stringify(value))
  }

  protected async removeConfigImpl(name: string) {
    await this.client.del(name)
  }

  async destroy() {
    await this.client.disconnect()
    super.destroy()
  }
}

When to use config stores

  • Feature flags that change without deployment
  • Third-party API URLs that vary by environment
  • Rate limits and timeout settings
  • A/B test configuration
  • Any value that operators might tune in production

Common pitfalls

  • Storing secrets in config stores. Configs may be logged or exposed in dashboards. Use secret stores for credentials.
  • Not validating reads. Config stores return unknown. Always parse and validate.
  • Assuming set/remove are enabled. By default, only get is enabled. Enable set and remove explicitly if needed.
  • Using config for startup-critical values. If a missing config prevents the service from starting, use service config instead.

Checklist

  • Config store is used for non-sensitive, runtime-mutable values
  • Config reads are validated with Zod schemas in production
  • Set/remove capabilities are enabled only when needed
  • Secrets are in secret stores, never in config stores
  • Integration tests cover the concrete provider behavior

Related

Read Next
REST API Endpoints

from Exposing Your Service