Skip to Content
prxy.monster v1 is live. Start with Pro or Team →
ConceptsStorage adapters

Storage adapters

Modules access persistent state through a single StorageAdapter interface. The interface is implemented twice — once for cloud, once for local. Modules don’t know which one they’re using.

The interface

export interface StorageAdapter { kind: 'cloud' | 'local'; kv: KvStore; db: Database; blob: BlobStore; health(): Promise<HealthStatus>; }

Three sub-interfaces:

kv — key-value with TTL

interface KvStore { get(key: string): Promise<string | null>; set(key: string, value: string, ttlSeconds?: number): Promise<void>; setNx(key: string, value: string, ttlSeconds: number): Promise<boolean>; del(key: string): Promise<void>; ttl(key: string): Promise<number>; }

Used for: rate limits, hot caches, distributed locks (setNx), cost counters.

Cloud uses a managed low-latency KV store. Local mode uses an in-process KV store with the same TTL behavior.

interface Database { from(table: string): QueryBuilder; raw(sql: string, params?: unknown[]): Promise<unknown[]>; }

The QueryBuilder exposes a chainable API — .select().eq().order().limit() etc. — and adds one extension: vectorSearch(col, embedding, opts) for nearest-neighbor lookups.

Cloud uses managed relational storage with vector search. Local mode uses a local database file with a compatibility fallback when native vector search is unavailable.

blob — large content

interface BlobStore { put(key: string, content: Buffer | string): Promise<void>; get(key: string): Promise<Buffer | null>; delete(key: string): Promise<void>; list(prefix: string): Promise<string[]>; }

Used for: compressed conversation archives, evicted message bodies, attachments.

Cloud uses managed object storage. Local mode writes blobs under the configured local data directory. Both implementations share the same BlobStore contract so modules don’t change.

Vector search compatibility

The vector dimensionality is fixed by the configured embedding model. When a local install cannot use native vector search, the adapter falls back to a linear cosine scan. Quality is identical; latency is worse on large stores.

Falling back gracefully

When the storage backend is unavailable:

  • kv.get() returns null instead of throwing.
  • kv.set() swallows the error and logs.
  • db queries return { data: null, error } and the calling module treats it as a no-op.

This is by design — a cache miss is acceptable. A 503 to the client is not.

Lifecycle

// At gateway boot: const storage = await initStorage(); // → CloudAdapter if LOCAL_MODE !== 'true', else LocalAdapter. // Runs migrations, connects to storage, validates health. // During a request: const ctx = { storage, request, ... }; await module.pre(ctx); // Module reads/writes through ctx.storage. // At gateway shutdown: await storage.shutdown(); // Closes connections and flushes local writes.

init() and shutdown() are concrete-class methods, not part of the interface. This stops modules from accidentally calling them.

See also

Last updated on