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.
db — relational with vector search
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()returnsnullinstead of throwing.kv.set()swallows the error and logs.dbqueries 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
- SDK → Storage access — the API your module sees.
- Modules — what uses storage and how.
- Local mode → Docker — what files exist on disk.