Storage access
Modules access persistent state through ctx.storage. The storage adapter exposes three sub-stores: KV, DB, and blob.
async pre(ctx) {
// Quick KV
await ctx.storage.kv.set('mykey', 'myvalue', 60);
// Relational
const result = await ctx.storage.db.from('users').select('*').eq('id', 'abc').maybeSingle();
// Blob
await ctx.storage.blob.put('attachments/foo.json', JSON.stringify({ ... }));
}The same code runs against Postgres+Redis+R2 (cloud) or SQLite+memory+filesystem (local).
ctx.storage.kv — key-value
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>;
}| Method | Notes |
|---|---|
get | Returns null on miss. Errors are swallowed and treated as misses. |
set | Optional TTL in seconds. No TTL = no expiry. |
setNx | Atomic set-if-not-exists. Use for distributed locks. |
del | No-op if key doesn’t exist. |
ttl | Seconds until expiry. -1 if no TTL set. -2 if key doesn’t exist. |
Best for: rate limit counters, hot caches, distributed locks (setNx), spend counters.
Backends:
- Cloud: Upstash Redis (HTTP REST).
- Local: in-memory
Map<string, { value, expiresAt }>with a 30-second sweep.
ctx.storage.db — relational + vector
interface Database {
from(table: string): QueryBuilder;
raw(sql: string, params?: unknown[]): Promise<unknown[]>;
}The QueryBuilder is Supabase-style chainable:
// SELECT
const result = await ctx.storage.db
.from('patterns')
.select('id, content, score')
.eq('user_id', ctx.apiKey.userId)
.gte('success_rate', 0.6)
.order('score', { ascending: false })
.limit(5);
if (result.error) ctx.logger.error({ err: result.error }, 'patterns lookup failed');
const patterns = result.data as Array<{ id: string; content: string; score: number }>;
// INSERT
await ctx.storage.db.from('events').insert({
id: randomUUID(),
user_id: ctx.apiKey.userId,
type: 'cache_miss',
});
// UPSERT (Postgres ON CONFLICT)
await ctx.storage.db.from('counters').upsert(
{ id: 'requests', value: 1 },
{ onConflict: 'id' },
);
// VECTOR SEARCH
const matches = await ctx.storage.db
.from('embeddings')
.vectorSearch('embedding', queryEmbedding, { limit: 10, minScore: 0.7 });
// → Array<{ score: number; data: unknown }>Best for: patterns, cached embeddings, audit logs, anything you’d put in Postgres normally.
Backends:
- Cloud: Postgres (Neon) + pgvector (
vector(1536), ivfflat index). - Local: better-sqlite3 + sqlite-vec when available. Falls back to a JSON-cosine scan when sqlite-vec isn’t compiled.
ctx.storage.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[]>;
}Best for: compressed conversation archives, attachments, anything that doesn’t belong in a DB row.
Backends:
- Cloud: Cloudflare R2.
- Local: filesystem under
~/.prxy/blob/{key}.
Schema migrations
Built-in modules ship their migrations alongside the module. Custom modules must run their own. Two patterns:
Idempotent CREATE in init()
Simple, works for additions:
async init(storage) {
await storage.db.raw(`
CREATE TABLE IF NOT EXISTS my_state (
id TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
}Versioned migrations (recommended for production modules)
Track applied migrations in your own table and run forward-only DDL. See apps/gateway/src/storage/migrations in the source for the pattern the built-ins use.
Failure handling
Storage operations are designed to fail gracefully:
- KV unavailable —
getreturnsnull,setswallows error and logs. - DB unavailable — queries return
{ data: null, error }. Calling code should treat as no-op. - Blob unavailable —
getreturnsnull,putthrows (writes are usually critical, reads are usually optional).
The principle: a storage outage shouldn’t take the whole gateway offline. A semantic cache miss is acceptable. A 503 to the client is not.
See also
- Module interface — full Module type.
- Storage adapters concept — backend implementations.
- Lifecycle — when each hook can use storage.