Key-value backends

The KvStore port and its backends — database (durable, _voltro_kv), redis (shared), and memory — plus the KV_BACKEND selector, the @voltro/kv package, and custom backends.

ctx.kv runs on a KvStore — a small Effect-native port with three built-in backends. Which one you get is a configuration choice; the handler code is identical, exactly like the cache.

Selecting the backend

KV_BACKEND picks it (the kv: field in app.config.ts seeds the env when unset):

// app.config.ts
export default { type: 'api', name: 'myApi', store: 'postgres', kv: 'database' }
Backend Durable? What it is Use
database (default, sql apps) yes — persists in _voltro_kv rows in your own SQL store the default; survives restarts, shared across replicas
redis no keys on a RESP server shared/cross-instance but deliberately ephemeral state
memory no an in-process map dev / tests / single-process

Unknown values fall back to database. On an app with no SQL store, use memory or redis.

database — the durable default

The database backend stores each entry as a row in _voltro_kv, a framework-managed table created for every SQL app (empty until ctx.kv is used). The value is a JSON string in a text column — exact round-trip for any value — with an optional expiresAt (lazy eviction on read) and a UNIQUE(key) that makes set an atomic upsert. Because it lives in your store, it survives restarts and every replica sees the same data.

redis — shared, non-durable

The redis backend keys entries under a prefix (default voltro:kv) on any RESP server (Redis / Valkey / KeyDB / Dragonfly / Upstash). It draws its client from the shared connection registry — one socket per named connection, reused across the cache, KV, and rate limiter. Redis is a cache, not a durable store, so pick it only for state you can afford to lose on a flush.

memory — dev / tests

An in-process map, eviction-free (entries live until deleted or TTL-expired). Lost on restart and invisible to other replicas — for a single process, tests, or throwaway state. Under the test harness (makeTestContext) ctx.kv is a memory KV whose TTLs honour ctx.clock.

The @voltro/kv package

The primitive ships in @voltro/kv (a leaf package — effect is the only runtime dependency; the redis drivers are lazily-imported optional deps). Wire the layers by hand only when embedding the KV outside the framework path; inside a Voltro app you just use ctx.kv / Kv.

import { Kv, KvStore, layerMemory, layerRedis } from '@voltro/kv'
import { Layer } from 'effect'

// memory-backed Kv service
const MemoryKv = Kv.Default.pipe(Layer.provide(layerMemory()))

Key exports: the Kv service, the KvStore port (+ KvStoreShape, KvSetOptions), KvError (the single typed failure), and the backend layers layerMemory / layerRedis (plus makeRedisKvStore over an existing client). The database backend + the ctx.kv facade live in @voltro/runtime (they need @voltro/database).

The KvStore port

KvStore is a Context.Tag of the KvStoreShape interface (get / set / delete / has / list / clear, all Effect-returning). Both built-in backends are just a Layer<KvStore>; a custom backend plugs into the same slot:

import { Kv, KvStore, type KvStoreShape } from '@voltro/kv'
import { Layer } from 'effect'

const myBackend: KvStoreShape = {
  // get / set / delete / has / list / clear — each returns an Effect<…, KvError>
} as KvStoreShape

const MyKv = Kv.Default.pipe(Layer.provide(Layer.succeed(KvStore, KvStore.of(myBackend))))

KvError

Every KV operation fails with one typed error — a Schema.TaggedError with { operation, key, cause }, mirroring the cache's CacheError. Declare it in a handler's error: schema and the rpc encoder marshals it across the wire; Effect.catchTag('KvError', …) catches it on the Effect path.