bunderstackbunderstack/ docs

Sync & Realtime

Typed TanStack DB collections over the oRPC Publisher transport

bunderstack/sync layers TanStack DB collections over the same generated oRPC procedures. It provides local live queries, optimistic mutations, scoped windows, and reliable realtime updates inferred from your server app.

Create a sync client

bun add bunderstack @tanstack/react-query @tanstack/db @tanstack/query-db-collection
import { createSyncClient } from 'bunderstack/sync'
import type { App } from './bunderstack'

const api = createSyncClient<App>({ queryClient })

Each exposed table receives:

api.posts.collection
api.posts.table
api.posts.scopedCollection(options)
api.posts.collectionByIds(ids)

Collections are stable by configuration. Calling scopedCollection() or collectionByIds() with equivalent options returns the existing collection; do not cast a plain array into a collection and do not rebuild collections on every render.

Optimistic mutations and reconciliation

api.posts.collection.insert({ id, title, userId })
api.posts.collection.update(id, (draft) => {
  draft.title = 'renamed'
})
api.posts.collection.delete(id)

The server mutation returns the canonical row after defaults, ownership, and database transforms are applied. The sync layer reconciles that row into every materialized view without a follow-up list request. Several overlapping local edits to the same row are coalesced so a stale completion cannot overwrite a newer optimistic state.

Scoped collections

Use a growing window for feeds and other ordered datasets:

const feed = api.posts.scopedCollection({
  filters: { replyToId: null },
  sort: 'createdAt',
  order: 'desc',
  initialCount: 20,
})

const result = useLiveQuery((q) =>
  q
    .from({ post: feed.collection })
    .orderBy(({ post }) => post.createdAt, 'desc'),
)

await feed.loadMore()
feed.hasMore()

Use collectionByIds(ids) for exact relation lookups rather than searching a capped base collection:

const authorIds = [...new Set(posts.map((post) => post.userId))].sort()
const authors = api.user.collectionByIds(authorIds)

One realtime path

On the server, generated writes and context.realtime.publish() emit typed changes through oRPC Publisher. The built-in realtime.changes procedure returns an event iterator. On the client, Bunderstack applies each change to query caches and all materialized collection views.

database write
  → oRPC Publisher
  → realtime.changes iterator
  → query cache + TanStack DB collections

Realtime starts automatically in browser sync clients and stays disabled during SSR. Applications do not poll a list endpoint and do not create a second event client.

Reliability behavior

The library owns the transport lifecycle:

  • an internal heartbeat keeps quiet streams observable without producing application events or cache work;
  • a stream that stops delivering is detected and replaced. The server sends a heartbeat on a fixed interval and advertises that interval on the event itself. The client tears the connection down after 2.5 intervals of silence and reconnects. Without this a connection killed by a proxy idle-timeout, a sleeping laptop, or a network change would hang open and deliver nothing;
  • failed connections retry with exponential backoff and jitter rather than a fixed request loop;
  • Publisher event IDs allow resume while retained events are available;
  • after reconnect, subscribed tables are refetched once so an expired replay window cannot leave the cache stale;
  • changes are applied in one batch per animation frame, so a burst of writes costs one cache write and one invalidation per query key rather than one of each per event;
  • a heartbeat is filtered inside the transport and never appears in a live query.

Publisher replay is an optimization. The database and the reconnect refetch remain the source of cache correctness.

Custom publications

Publish a canonical row from a procedure or job when a write happens outside generated CRUD:

await context.realtime.publish(schema.posts, 'update', updatedPost)

Use Redis realtime configuration when web and worker processes are separate; the memory publisher is process-local.

For TanStack Query without collections, use syncRealtime from bunderstack/query. Framework adapters are covered in Framework Portability.

On this page