Skip to main content

KV store

A Redis-compatible key-value abstraction underpins much of Covara: cross-isolate subscriptions, sessions, the task queue, rate limits, the changelog, billing idempotency, and the search outbox. Initialize it once and the framework wires everything to it.

Backends

BackendtypeUse
MemorymemoryDevelopment, single process. Per-isolate — not shared.
RedisredisProduction on Node / multi-instance.
Durable Objectdurable-objectCloudflare Workers — shared state without Redis.

Initialize

The recommended entry point is initializeKV, which also wires cross-process subscription fan-out for distributed stores:

import { initializeKV } from "covara/kv";

// Development
await initializeKV({ type: "memory", prefix: "my-app" });

// Production (Node)
await initializeKV({ type: "redis", redis: { url: env.REDIS_URL } });

For any distributed (non-memory) store, initializeKV calls initializeEventSubscription() for you, so a mutation on one instance reaches subscribers on another. If you instead set the global KV directly with setGlobalKV(...), call initializeEventSubscription() yourself.

import { getGlobalKV, setGlobalKV } from "covara";

const kv = getGlobalKV(); // throws if not initialized

Durable Object KV (Workers)

The in-memory KV is per-isolate, and Cloudflare runs many isolates — so a mutation handled by one isolate wouldn't reach subscribers on another without a shared store. The Durable Object KV solves this:

import { createDurableObjectKV, setGlobalKV, initializeEventSubscription } from "covara";
export { CovaraKVDurableObject } from "covara";

setGlobalKV(createDurableObjectKV(env.COVARA_KV));
void initializeEventSubscription();
[durable_objects]
bindings = [{ name = "COVARA_KV", class_name = "CovaraKVDurableObject" }]

[[migrations]]
tag = "v1"
new_sqlite_classes = ["CovaraKVDurableObject"]

Key properties:

  • All operations (strings, hashes, sets, lists, sorted sets, TTLs, transactions) run inside a single, single-threaded Durable Object — strongly consistent, and a multi() batch is atomic.
  • Collections store one entry per member, avoiding the 128 KB single-value cap.
  • Pub/sub uses hibernatable WebSockets — one WebSocket per isolate, idle connections don't accrue duration charges, automatic reconnect with backoff.
  • Zero Cloudflare imports (structural types), so it's Node-testable.

createDurableObjectKV(namespace, { name?, prefix? }) is the direct form (name selects the DO instance, default "covara-kv"); createKV({ type: "durable-object", durableObject: { namespace: env.COVARA_KV } }) is the config-style equivalent. Full setup in Durable Object KV deployment.

What uses the KV

FeatureWhat it stores
Subscriptions / changelogSharded subscriptions, changelog window, cross-process events
Aggregate subscriptionsThe covara:aggregate pub/sub channel
SessionsSession records (Redis store)
TasksQueue, locks, idempotency, results, DLQ
Rate limiting / login throttleCounters
OIDCClients, codes, refresh tokens, consents (when KV present)
BillingWebhook dedupe, credits ledger
Search outboxIndex queue, in-flight ops, dead set