AbsoluteJS

Sync

A reactive data layer for your own database. Push row-level diffs over a WebSocket, run server-authored mutations with optimistic client edits, declare CRDT fields for conflict-free collaboration, and run scheduled functions — all without adopting a hosted backend.

For the operator surface (point-in-time replay, tenant migration primitives, hash-chain audit, OTel across the substrate), see Substrate complete (G1–G7).

#Installation

BASH
bun add @absolutejs/sync elysia

#Quick Start

Runnable server · partial client
The server declares its in-memory Task store, registers the engine, and listens on port 3000. The client block is a React component snippet; mount it in an existing React application.

Define a collection on your engine, expose it over syncSocket, and let the engine push diffs. Reads and writes both flow through one WebSocket.

TS
import { Elysia } from 'elysia';
import { syncSocket } from '@absolutejs/sync';
import {
  createSyncEngine,
  defineMutation,
  defineReactiveQuery,
} from '@absolutejs/sync/engine';

type Task = { id: string; title: string };
const store = new Map<string, Task>();
const engine = createSyncEngine();

// Teach the engine your table once — it powers read-set-tracked queries.
engine.registerReader('tasks', { all: () => [...store.values()] });
engine.registerWriter('tasks', {
  insert: (data) => { store.set(data.id, data); return data; },
  update: (data) => { store.set(data.id, data); return data; },
  delete: (row) => { store.delete(row.id); },
});

// A live collection: re-runs and re-pushes whenever the table changes.
engine.registerReactive(defineReactiveQuery({
  name: 'tasks',
  key: (task) => task.id,
  run: ({ db }) => db.all('tasks'),
}));

engine.registerMutation(defineMutation({
  name: 'addTask',
  handler: (args, _ctx, actions) =>
    actions.insert('tasks', { id: crypto.randomUUID(), title: args.title }),
}));

new Elysia().use(syncSocket({ engine })).listen(3000);

Proof of success: open two clients, add a task in one, and confirm the second receives the row without polling.

On the client, one hook gives you the live data + an optimistic mutate:

TSX
import { useSyncCollection } from '@absolutejs/sync/react';

const Tasks = () => {
  const { data, mutate } = useSyncCollection({
    url: 'ws://localhost:3000/sync/ws',
    collection: 'tasks',
  });

  return (
    <ul>
      {data.map((task) => <li key={task.id}>{task.title}</li>)}
      <button onClick={() => mutate({
        name: 'addTask',
        args: { title: 'New task' },
        optimistic: (draft) => draft.set({
          id: crypto.randomUUID(),
          title: 'New task',
        }),
      })}>Add</button>
    </ul>
  );
};

#How it fits

Sync is a library, not a hosted backend. It runs inside your Elysia server, talks to the database you already have (Postgres, MySQL, SQLite via Drizzle or Prisma), and ships its own transport:

  • Reactive push — kill polling. A view subscribes to topics; mutations publish them; subscribers refetch the instant data changes.
  • ORM auto-reactivity — Drizzle and Prisma adapters derive topics from a query, so reads and writes line up automatically.
  • Live collections — row-level { added, removed, changed } diffs over a WebSocket, optimistic mutations, an offline queue, and a local-first IndexedDB cache.
  • Operator graph — incremental joins, aggregations, and top-N ordering as composable operators.

#CRDT Collaboration

Declare any row field as a CRDT and the engine merges concurrent writes server-side instead of overwriting. A client hook reads/writes the field with no per-keystroke server round-trip — the local replica holds the live text and uploads only the delta ops (O(edit), not O(doc)).

TS
import { rgaText } from '@absolutejs/sync/crdt';

// Server — declare a field as a CRDT; the engine merges on write instead of
// overwriting, and auto-registers a "doc:merge" mutation for the client.
engine.registerCrdt('doc', { body: rgaText });

// Client — useCollaborativeText reads/writes that field. Open the same row
// in two tabs and type at once: edits merge with no clobbering.
import { useCollaborativeText } from '@absolutejs/sync/react';

const Editor = () => {
  const doc = useCollaborativeText({
    url: 'ws://localhost:3000/sync/ws',
    collection: 'doc',
    field: 'body',
    id: 'shared',
  });

  return (
    <textarea
      value={doc.text}
      onChange={(event) => doc.setText(event.target.value)}
    />
  );
};

The first-party CRDT kit at @absolutejs/sync/crdt is zero-dependency and isomorphic — a PN-counter, an LWW register, an OR-Set, a key→value map, an ordered list, and the RGA text type. Caret positions can be anchored to CRDT element ids so they survive concurrent edits (collaborative cursors).

#Permissions & Schema

Row-level reads and writes are gated declaratively: the read rule filters every diff the engine emits, and the write rule runs against the committed row before the mutation touches your store (a deny rolls the transaction back).

TS
import { definePermissions } from '@absolutejs/sync/engine';

const engine = createSyncEngine({
  permissions: definePermissions({
    tasks: {
      // Row-level read filter applied to every diff the engine emits.
      read: (row, ctx) => row.userId === ctx.userId,
      // Write gate runs before insert/update/delete; deny rolls the txn back.
      write: (ctx) => ctx.role !== 'viewer',
    },
  }),
});

The companion defineSchema + the field kit validate every write (a bad write throws SchemaError), and migrate lazily upcasts rows on read — no database migration step required.

A search collection keeps a server-side BM25 (or vector) index current from the same change feed. The subscription's params are the query — the ranked top-K streams back as a normal collection, re-ranked as rows change.

TS
import { createTextIndex, defineSearchCollection } from '@absolutejs/sync/engine';

// Live full-text search (BM25) kept current from the same change feed.
engine.registerSearch(defineSearchCollection({
  name: 'taskSearch',
  table: 'tasks',
  source: () => [...store.values()],
  key: (task) => task.id,
  index: () => createTextIndex({
    fields: ['title', 'body'],
    key: (task) => task.id,
  }),
}));

// Client subscribes; the params ARE the query string. Top-K rows stream back
// re-ranked as rows change, each tagged with _score.
const hits = useSyncCollection({
  collection: 'taskSearch',
  params: 'urgent',
  url,
});

#Scheduled Functions

Cron-pattern server jobs whose writes go live through the change feed — no polling, no separate scheduler service. Wired through the scheduled Elysia plugin (an optional subpath so consumers without it pull no cron dependency).

TS
import { defineSchedule } from '@absolutejs/sync/engine';
import { scheduled } from '@absolutejs/sync/scheduled';

engine.registerSchedule(defineSchedule({
  name: 'nightly-summary',
  pattern: '0 0 3 * * *',          // 6-field cron, seconds first
  run: ({ actions }) => actions.insert('reports', { /* ... */ }),
}));

new Elysia().use(syncSocket({ engine })).use(scheduled({ engine }));

#CRDT Backends — first-party adapters

The in-package CRDT is great for offline-merge and moderate collaboration. For production-scale collaborative text, swap in a battle-tested backend from the sync-adapters repo — @absolutejs/sync-yjs, @absolutejs/sync-automerge, or @absolutejs/sync-loro — all behind the same TextCrdtAdapter contract:

TS
// Swap the in-package RGA for a production CRDT — same call sites:
import { yjsText } from '@absolutejs/sync-yjs';
// or
import { automergeText } from '@absolutejs/sync-automerge';
import { loroText } from '@absolutejs/sync-loro';

engine.registerCrdt('doc', { body: yjsText });

const doc = useCollaborativeText({
  url, collection: 'doc', field: 'body', id: 'shared',
  create: createYjsText,  // <- the only client change
});

#Framework Hooks

Idiomatic bindings live at @absolutejs/sync/{react, vue, svelte, angular}. Each ships useSyncCollection / createSyncCollectionStore / SyncCollectionService.connect for live collections, plus a matching useCollaborativeText / createCollaborativeTextStore / SyncCollectionService.collaborativeText for CRDT fields.

All four hook surfaces are SSR-safe and return the same shape — so swapping frameworks in a multi-stack app doesn't change your data layer.

Continue toward an outcome

These playbooks show where this package fits, how to verify the combined system, and what changes before production.

Current package surface

What ships today

@absolutejs/syncv2.13.0 · stableData & SyncnpmSource
22entry points284symbols

Import surface · click to copy

31 symbols
createWriteBehindCacheexportPermalinkSource
TS
createWriteBehindCache
Exported from @absolutejs/sync
Use this API in an outcome:Add realtime collaboration

Outcomes

What you can build

Reactive application data

Push database changes into live collections and framework bindings without polling or manually naming every invalidation topic.

A complete synchronization engine

Compose CRDT collaboration, write-behind caching, PostgreSQL/MySQL/SQLite stores, ORM integrations, and one brokered upstream pool for many tenants.

Hardening checklist

Production guidance

Design for reconnects and concurrencyUse durable cursors, reconnect and replay behavior, bounded upstream pools, explicit conflict semantics, transaction-aware change publication, and adapter-specific operational metrics.

Follow in order

Troubleshooting path

1
A client stopped updating
Inspect the collection query, mutation commit, change publication, topic derivation, broker delivery, client cursor, reconnect state, and local projection in that order.