Storage Interfaces
RecordStore, VectorStore, AssetStore, storage(), and in-memory Storage Beta implementations.
import {
inMemoryAssetStore,
inMemoryRecordStore,
inMemoryStorage,
inMemoryVectorStore,
storage,
StorageError,
} from "@use-crux/core/storage";
import type {
AssetStore,
RecordStore,
Storage,
VectorStore,
} from "@use-crux/core/storage";Overview
Crux storage is split into explicit capabilities:
| Interface | Purpose |
|---|---|
RecordStore | JSON records with get, put, create, delete, paged list, optional scan, TTL, filters, and watches |
VectorStore | Dense, sparse, and hybrid vector indexes with explicit capability claims |
AssetStore | Binary and oversized payload storage with put, get, and delete |
Storage | A capability bundle: { records, vectors?, assets? } |
Use this module for application code. @use-crux/core/storage is the canonical Storage Beta entry point.
storage(config)
Normalize and shallow-freeze a capability bundle:
const appStorage = storage({
records,
vectors,
assets,
});Primitives can consume the whole bundle or the specific capability they need:
workspace({ id: "files", namespace, storage: appStorage });
retriever({ id: "docs", records, vectors, dense });Use storage.scope() for tenant, user, session, or workspace isolation:
const tenantStorage = storage.scope(appStorage, `tenant:${tenantId}`);Scoped storage preserves capability claims while prefixing record and vector keys.
RecordStore
interface RecordStore<T extends JsonObject = JsonObject> {
get(key: string): Promise<T | null>;
getMany?(keys: readonly string[]): Promise<readonly (T | null)[]>;
put(key: string, value: T, options?: RecordWriteOptions): Promise<void>;
putMany?(entries: readonly RecordWrite<T>[]): Promise<void>;
create(key: string, value: T, options?: RecordWriteOptions): Promise<boolean>;
delete(key: string): Promise<void>;
deleteMany?(keys: readonly string[]): Promise<void>;
list(prefix: string, options?: RecordListOptions): Promise<RecordPage<T>>;
scan?(
prefix: string,
options?: Omit<RecordListOptions, "cursor">,
): AsyncIterable<RecordEntry<T>>;
watch?(prefix: string, callback: (event: RecordEvent<T>) => void): () => void;
capabilities(): RecordStoreCapabilities;
}put() accepts { ttlMs }, not { ttl }. create() is the atomic insert primitive and returns false when an active record already exists.
Capability levels are explicit:
type RecordStoreCapabilities = {
ttl: "native" | "lazy" | false;
filter: "native" | "scan" | false;
watch: boolean;
batch: boolean;
};Filters are exact top-level scalar equality only:
await records.list("docs:", {
filter: { namespace: "public", active: true, deletedAt: null },
});Adapters must reject unsupported filters with StorageError code invalid_filter; they must not silently drop filter clauses.
VectorStore
interface VectorStore {
upsert(records: readonly VectorRecord[]): Promise<void>;
delete(keys: readonly string[]): Promise<void>;
search(query: VectorSearchQuery): Promise<readonly VectorHit[]>;
capabilities(): VectorStoreCapabilities;
}search() uses a discriminated query:
await vectors.search({
mode: "hybrid",
dense: [0.12, 0.4, 0.9],
sparse: { indices: [12, 98], values: [0.8, 0.3] },
fusion: "dbsf",
limit: 10,
filter: { namespace: "docs" },
});Capability levels are part of correctness:
type VectorStoreCapabilities = {
dense: boolean;
sparse: boolean;
hybrid: boolean;
fusion: readonly ("rrf" | "dbsf")[];
filter: "pre" | "post" | false;
consistency: "strong" | "eventual";
};Production filtered retrieval requires filter: 'pre'. A post filter can be useful for local tooling, but Crux primitives that rely on exact filtered top-k results reject it unless they explicitly opt into approximate behavior.
AssetStore
interface AssetStore {
put(asset: Asset, options?: AssetPutOptions): Promise<StoredAsset>;
get(ref: AssetRef): Promise<StoredAsset>;
delete(ref: AssetRef): Promise<void>;
}Asset URIs are internal application references, not automatic public URLs. Signed URLs are app-authorized and are not shown in devtools by default.
Errors
Storage contract failures throw StorageError:
try {
await records.put("cache:key", { value: "x" }, { ttlMs: 60_000 });
} catch (error) {
if (error instanceof StorageError && error.code === "ttl_unsupported") {
// choose a non-TTL path
}
}Error codes include not_found, conflict, unsupported_capability, invalid_key, invalid_value, invalid_filter, ttl_unsupported, and backend_error.
In-Memory Implementations
Use these for tests, demos, and local examples:
const records = inMemoryRecordStore();
const vectors = inMemoryVectorStore();
const assets = inMemoryAssetStore();
const all = inMemoryStorage();In-memory stores are process-local and not durable. They are contract-correct reference implementations: records use lazy TTL and scan filtering, vectors pre-filter metadata before scoring, and assets support explicit put(), get(), and delete() lifecycle behavior.
Related
RecordStore guide
JSON record storage, TTL, filters, watches, and custom database implementations.
VectorStore guide
Dense, sparse, and hybrid vector search.
AssetStore guide
Binary files, generated outputs, and custom object storage.
Postgres RecordStore
A complete cookbook implementation for SQL-backed record storage.