Skip to main content
Key0 requires two stores to operate and supports an optional third for auditing:
StoreInterfacePurpose
Challenge storeIChallengeStoreTracks challenge records through their lifecycle (PENDING, PAID, DELIVERED, etc.)
Seen TX storeISeenTxStorePrevents double-spend by deduplicating transaction hashes
Audit store (optional)IAuditStoreAppend-only log of every state transition with actor, reason, and timestamp
Both Redis and Postgres backends ship with @key0ai/key0. They provide identical atomic guarantees — Redis via Lua scripts, Postgres via serializable transactions.

Choosing a backend

Use Redis when you want:
  • Sub-millisecond latency on reads and writes
  • Simple infrastructure (single Redis instance or cluster)
  • Automatic key expiration via TTLs
Redis stores challenge records as hashes, uses SET NX for transaction dedup, and runs Lua scripts for atomic compare-and-swap state transitions.

Setup

import Redis from "ioredis";
import {
  RedisChallengeStore,
  RedisSeenTxStore,
  RedisAuditStore,
} from "@key0ai/key0";

const redis = new Redis(process.env.REDIS_URL!);

const store = new RedisChallengeStore({ redis });
const seenTxStore = new RedisSeenTxStore({ redis });
const auditStore = new RedisAuditStore({ redis }); // optional

Configuration options

RedisChallengeStore accepts a RedisStoreConfig object:
OptionTypeDefaultDescription
redisRedis (ioredis)requiredRedis client instance
keyPrefixstring"key0"Prefix for all Redis keys
challengeTTLSecondsnumber900 (15 min)TTL for the request-index key
recordTTLSecondsnumber604800 (7 days)TTL for challenge hash keys
deliveredTTLSecondsnumber43200 (12 hours)TTL applied when a record reaches DELIVERED
RedisSeenTxStore and RedisAuditStore accept only redis and keyPrefix.

Key naming

All keys are prefixed with keyPrefix (default key0):
Key patternTypePurpose
key0:challenge:{challengeId}HashFull challenge record
key0:request:{requestId}StringMaps requestId to challengeId for lookups
key0:seentx:{txHash}StringDouble-spend prevention (SET NX)
key0:paidSorted setPAID records scored by paidAt timestamp (for refund queries)
key0:audit:{challengeId}ListAppend-only audit log per challenge

TTL behavior

KeyDefault TTLApplied when
Challenge hash7 days (recordTTLSeconds)On create
Request index900s (challengeTTLSeconds)On create
Seen TX7 daysOn markUsed
Audit listMatches challenge hashOn create and each transition
Delivered records12h (deliveredTTLSeconds)On PAID to DELIVERED transition

Atomic transitions

State transitions use a Lua script that runs entirely within Redis. The script:
  1. Reads the current state (compare)
  2. Writes the new state and field updates (swap)
  3. Maintains the key0:paid sorted set (add on PAID, remove on exit from PAID)
  4. Appends an audit entry to the challenge’s audit list
All four steps execute atomically — no other command can interleave.

Health check

Call healthCheck() at startup to fail fast on misconfiguration:
await store.healthCheck(); // throws if Redis is unreachable

IAuditStore

The audit store is optional but recommended for production. Every state transition is logged with:
  • challengeId and requestId for correlation
  • fromState and toState for the transition
  • actor (e.g., "engine", "cron", "admin")
  • reason (optional, e.g., "challenge_created", "payment_verified")
  • updates (the field changes applied in the transition)
  • createdAt timestamp
Both RedisAuditStore and PostgresAuditStore implement IAuditStore. In addition, RedisChallengeStore and PostgresChallengeStore automatically write audit entries during create() and transition() calls — even without a standalone audit store configured.
// Retrieve the full audit trail for a challenge
const history = await auditStore.getHistory(challengeId);

Standalone (Docker) storage

When running Key0 as a standalone Docker container, set the STORAGE_BACKEND environment variable to select your backend:
STORAGE_BACKEND=redis   # Use Redis for all stores
STORAGE_BACKEND=postgres # Use Postgres for challenge and seen-tx stores
Redis is always required in standalone mode, even when using Postgres as the primary storage backend. The BullMQ-based refund cron job uses Redis as its queue broker.

Connection health

Always verify your storage connection at startup. A misconfigured or unreachable store causes silent failures — challenges are created but never persisted, and payments cannot be verified.
For Redis, call store.healthCheck() before accepting traffic. For Postgres, the auto-migration step serves as an implicit health check — if it fails, the store constructor’s internal ready promise rejects, and subsequent operations throw.