Crux
GuidesStorage

DataStore

Store Crux JSON records for memory, cache, flows, evals, and workspace metadata.

DataStore is Crux's durable JSON record interface.

Use it when Crux needs to read, write, list, or subscribe to structured state:

import { inMemoryDataStore } from '@crux/core/storage'

const data = inMemoryDataStore()

await data.set('notes:1', { title: 'First note' })
const note = await data.get('notes:1')

It is not a vector index and it is not a blob store. If a feature needs similarity search, give it a VectorStore. If a workspace needs binary files, give it a BlobStore.

Interface

import type { DataStore, JsonObject } from '@crux/core/storage'

const data: DataStore = {
  async get(key) {
    return null
  },

  async set(key, value, options) {
    // persist JsonObject
  },

  async delete(key) {
    // delete record
  },

  async list(prefix, options) {
    return { entries: [] }
  },
}

Core features use namespaced keys internally. Preserve keys exactly and make prefix listing deterministic.

Common Uses

Memory uses a data store for block state:

memory({
  id: 'assistant',
  store: data,
  namespace: `user:${userId}`,
  blocks,
})

Workspace metadata uses a data store for paths, MIME types, sizes, timestamps, previews, and blob URIs:

workspace({
  id: 'thread-files',
  namespace: threadId,
  data,
  blobs,
})

Retrieval systems usually pair data with vectors:

retriever({
  id: 'docs',
  data,
  vectors,
  dense,
})

TTL

set() accepts an optional TTL in milliseconds:

await data.set('cache:brand:abc', { text: 'Brand voice' }, { ttl: 300_000 })

Stores that support TTL should expose supportsTtl():

if (data.supportsTtl?.()) {
  await data.set(key, value, { ttl: 300_000 })
}

Bundled TTL behavior:

StoreTTL behavior
inMemoryDataStore()Lazy expiry on get() and list()
cruxConvexStore()Stores _expiresAt, lazy expiry on read
cruxRedisStore()Native Redis PX key expiration

TTL is used by context caching and can also be used for short-lived app data.

Bundled Data Stores

Use inMemoryDataStore() for tests and examples:

import { inMemoryDataStore } from '@crux/core/storage'

const data = inMemoryDataStore()

Use cruxConvexStore() when your app already runs on Convex. Its component list query returns { docs, cursor } pages from the by_key index while the store adapter owns TTL suppression and decoded-value filters. See the Convex guide for setup and boundary rules.

Use Upstash Redis for a simple durable key-value store:

import { Redis } from '@upstash/redis'
import { cruxRedisStore } from '@crux/upstash/redis'

const data = cruxRedisStore({
  redis: new Redis({
    url: process.env.UPSTASH_REDIS_REST_URL!,
    token: process.env.UPSTASH_REDIS_REST_TOKEN!,
  }),
})

Subscriptions And Reactive UI

subscribe() is optional. Implement it when your backend can push changes and you want reactive UI without polling.

data.subscribe?.((event) => {
  if (event.type === 'set') {
    console.log('Updated', event.key, event.value)
  } else {
    console.log('Deleted', event.key)
  }
})

Reactive UI can use:

  • createConvexTransport() when using Convex. It consumes the component's { docs, cursor } page shape and filters decoded values locally.
  • createSSETransport() with cruxSSEHandler() when the store implements subscribe().
  • createPollingTransport() with any DataStore.

See React reference and Server reference for exact APIs.

Custom DataStore

A custom data store is straightforward if your database can read/write JSON records and list by key prefix.

import type { DataStore, JsonObject } from '@crux/core/storage'

export function myDataStore(): DataStore {
  return {
    async get(key) {
      const row = await db.records.findUnique({ where: { key } })
      return row ? (row.value as JsonObject) : null
    },

    async set(key, value) {
      await db.records.upsert({
        where: { key },
        create: { key, value },
        update: { value },
      })
    },

    async delete(key) {
      await db.records.deleteMany({ where: { key } })
    },

    async list(prefix = '') {
      const rows = await db.records.findMany({
        where: { key: { startsWith: prefix } },
        orderBy: { key: 'asc' },
      })

      return {
        entries: rows.map((row) => ({
          key: row.key,
          value: row.value as JsonObject,
        })),
      }
    },
  }
}

If your database also supports vectors or blobs, implement VectorStore or BlobStore as separate capabilities and bundle them with storage().

For a complete SQL implementation, see the Postgres DataStore cookbook.

On this page