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