Storage Interfaces
RecordStore, SearchStore, AssetStore, storage(), and in-memory Storage Beta implementations.
import {
inMemoryAssetStore,
inMemoryRecordStore,
inMemoryStorage,
inMemorySearchStore,
mutateRecord,
storage,
StorageError,
} from "@use-crux/core/storage";
import type {
AssetStore,
RecordStore,
SearchStore,
Storage,
} from "@use-crux/core/storage";Overview
Crux storage is split into explicit capabilities:
| Interface | Purpose |
|---|---|
RecordStore | JSON records with reads, writes, linearizable single-key mutation, listing, TTL, filters, and watches |
SearchStore | Dense, sparse, and lexical retrieval indexes with explicit capability claims |
AssetStore | Binary and oversized payload storage with put, get, and delete |
Storage | A capability bundle: { records, search?, 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,
search,
assets,
});Primitives can consume the whole bundle or the specific capability they need:
workspace({ id: "files", namespace, storage: appStorage });
retriever({ id: "docs", records, search, 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 search 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;
mutate?(
key: string,
fn: (current: T | null) =>
| RecordMutation<T>
| Promise<RecordMutation<T>>,
): Promise<T | null>;
getVersioned?(
key: string,
): Promise<{ value: T | null; version: string | null }>;
putVersioned?(
key: string,
value: T | null,
expectedVersion: string | null,
): Promise<boolean>;
capabilities(): RecordStoreCapabilities;
}put() accepts { ttlMs }, not { ttl }. create() is the atomic insert primitive and returns false when an active record already exists.
Use mutateRecord() for portable atomic read-modify-write behavior; it selects
the adapter's native transaction or bounded versioned-CAS seam:
await mutateRecord(records, "counter", (current) => ({
type: "put",
value: { count: (current?.count ?? 0) + 1 },
}));Capability levels are explicit:
type RecordStoreCapabilities = {
ttl: "native" | "lazy" | false;
filter: "native" | "scan" | false;
watch: boolean;
batch: boolean;
mutate: "native" | "cas" | false;
};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.
SearchStore
interface SearchStore {
readonly _tag?: "SearchStore";
upsert(records: readonly SearchRecord[]): Promise<void>;
delete(keys: readonly string[]): Promise<void>;
search(query: SearchQuery): Promise<readonly SearchHit[]>;
capabilities(): SearchStoreCapabilities;
}Search records can include lexical content, dense vectors, sparse vectors, metadata, or any supported combination:
type SearchRecord = {
key: string;
content?: string;
dense?: readonly number[];
sparse?: SparseVector;
metadata?: ExactFilter;
};search() composes one or more legs:
await search.search({
legs: [
{ kind: "dense", vector: [0.12, 0.4, 0.9], candidates: 120 },
{ kind: "lexical", query: "refund policy", candidates: 80 },
],
fusion: { strategy: "rrf", k: 60 },
limit: 10,
filter: { namespace: "docs" },
});Capability levels are part of correctness:
type SearchStoreCapabilities = {
legs: Readonly<Record<"dense" | "sparse" | "lexical", boolean>>;
fusion: readonly "rrf"[];
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.
SearchStore.upsert() is full-record replacement: omitted content, dense, or sparse clears that stored payload. Multi-leg searches use deterministic normalized RRF when two or more legs are requested. Single-leg searches ignore fusion.
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 search = inMemorySearchStore();
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, search pre-filters 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.
SearchStore guide
Dense, sparse, lexical search, capability checks, and adapter expectations.
AssetStore guide
Binary files, generated outputs, and custom object storage.
Postgres RecordStore
A complete cookbook implementation for SQL-backed record storage.