Crux
API Reference@use-crux/core

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:

InterfacePurpose
RecordStoreJSON records with reads, writes, linearizable single-key mutation, listing, TTL, filters, and watches
SearchStoreDense, sparse, and lexical retrieval indexes with explicit capability claims
AssetStoreBinary and oversized payload storage with put, get, and delete
StorageA 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.

On this page