Read this first — what's shipped vs. planned

TopGun does NOT replace Y.js's CRDT-as-data-structure model. TopGun provides whole-document/whole-map sync (LWW-Map and OR-Map keyed by HLC), not character-level operational transform / Y.Text. Y.Awareness (cursor presence, user metadata broadcast) has no direct TopGun equivalent — single-node `release_on_disconnect` is a partial primitive only. Y.UndoManager (per-document undo/redo with origin tracking) has no direct TopGun equivalent. Rich-text editor bindings (ProseMirror, Quill, Monaco, TipTap) are not in scope. See /docs/roadmap for the full maturity matrix.

DocsGuidesMigrating from Y.js

Migrating from Y.js

This guide is for Y.js users who want a server-backed, offline-first data layer with durable Postgres storage and server-side queries. The mapping is one-to-one for whole-document and whole-map use cases (Y.Map / Y.Doc), but Y.js features built on character-level operational transform (Y.Text, RelativePosition) and presence (Y.Awareness, UndoManager) do not have direct TopGun equivalents — cross-check the Concept Mapping table and the roadmap before porting.

Concept Mapping

Y.js ConceptTopGun EquivalentNotes
Y.DocTopGunClient instanceA single client owns many maps; there is no per-document boundary as in Y.js
Y.Mapclient.getMap(name) (LWW-Map)Same key/value semantics; conflict resolution by HLC + node-id tiebreak instead of Y.js’s per-key state vectors
Y.ArrayNo direct equivalentUse OR-Map keyed by stable id, or store a JSON array as a single LWW-Map value; ordered-list CRDT semantics are not provided
Y.TextNot supportedTopGun is whole-document sync, not character-level OT; rich-text editor bindings (ProseMirror, Quill, Monaco, TipTap) are out of scope
Y.Awarenessrelease_on_disconnect (locks/topics/counters; single-node only)Different semantics: Y.Awareness broadcasts presence/cursor metadata; release_on_disconnect is a lock-cleanup primitive. Cross-client cursor presence is on the roadmap — see /docs/roadmap
doc.observe() / map.observe()client.getMap(name).onChange(callback) or client.query(name, filter).subscribe(callback)Use onChange for raw local notifications; use query().subscribe() for filter-aware server-pushed deltas
Y.transact(() => {...})Implicit batching by SyncEngine outboxMultiple set() calls coalesce automatically into a single OpBatch over WebSocket; no explicit transact API
IndexeddbPersistence(name, doc)IDBAdapter (passed to TopGunClient constructor)Always-on; reads/writes never wait for network
WebsocketProvider(url, name, doc)TopGunClient serverUrl configSingle WebSocket connection per client multiplexes all maps and queries
Y.UndoManagerNot supportedNo per-document undo/redo with origin tracking; build at the application layer if needed
Y.RelativePositionNot supportedNo character-level OT cursors — TopGun has no character-position concept
encodeStateAsUpdate / applyUpdateMerkleTree-based delta syncAutomatic on reconnect; not a public API — clients exchange Merkle hashes to determine deltas
AuthenticationJWT via client.setAuthToken()Y.js providers do not include auth — bring-your-own-issuer in both projects. See Authentication
Server-side querySQL-style queries via client.query() + full-text search (tantivy)Y.js has no server-side query layer — clients download the full document and filter locally
Self-hosted, OSS licenseApache-2.0; self-host the Rust serverBoth projects ship under permissive OSS licenses (Apache-2.0). Framing is “server backend included” rather than “OSS replacement”

Side-by-Side Code Patterns

Pattern A — Initialize and connect

Y.js
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { IndexeddbPersistence } from 'y-indexeddb';

const doc = new Y.Doc();

// Offline persistence
const idb = new IndexeddbPersistence('my-room', doc);

// Realtime sync
const provider = new WebsocketProvider(
  'wss://yjs.example.com',
  'my-room',
  doc,
);
TopGun
import { TopGunClient } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';

const client = new TopGunClient({
  serverUrl: 'wss://topgun.example.com',
  storage: new IDBAdapter(),
});

// JWT issuance is handled by your own auth server.
// See /docs/guides/authentication for integration options.
const jwt = await myAuthServer.signIn(email, password);
client.setAuthToken(jwt);

In Y.js, a Y.Doc is the unit of sync — each room/document gets its own provider and persistence. In TopGun, a single TopGunClient owns many maps and queries over one WebSocket connection; the per-document boundary disappears.

Pattern B — Read, write, and observe

Y.js
const yMap = doc.getMap('todos');

// Read
const todo = yMap.get('todo-1');
console.log(todo);

// Write
yMap.set('todo-1', { title: 'Write guide', done: false });

// Observe — fires on any change to this Y.Map
yMap.observe((event) => {
  event.changes.keys.forEach((change, key) => {
    console.log(key, change.action, yMap.get(key));
  });
});
TopGun
import { Predicates } from '@topgunbuild/client';

// Read — synchronous, local-first (no await)
const todo = client.getMap('todos').get('todo-1');
console.log(todo);

// Write — synchronous, local-first; syncs in background
client.getMap('todos').set('todo-1', {
  title: 'Write guide',
  done: false,
});

// Raw local-change notifications (no filter):
const unsubLocal = client.getMap('todos').onChange(() => {
  const todo = client.getMap('todos').get('todo-1');
  console.log(todo);
});

// Filter-aware server-pushed deltas:
const query = client.query('todos', {
  predicate: Predicates.equal('done', false),
});
const unsub = query.subscribe((results) => {
  console.log(results);
});

yMap.observe and client.getMap().onChange() are the closest match — both fire on any write to the local map. For filter-aware deltas (e.g. “only show open todos”), use client.query().subscribe(), which Y.js does not provide because all filtering in Y.js is client-side over the full document.

If you’re using React

import { useQuery, useMutation } from '@topgunbuild/react';
import { type Todo } from './schema';

function TodoList() {
  const { data: todos } = useQuery<Todo>('todos');
  const { create } = useMutation<Todo>('todos');
  const add = () => create(crypto.randomUUID(), { title: 'Write guide', done: false });
  return (
    <>
      <button onClick={add}>Add</button>
      <ul>{todos.map(t => <li key={t._key}>{t.title}</li>)}</ul>
    </>
  );
}

Deployment Mode Semantics

DeploymentStatusNotes
Single-node (recommended for migration)StableAll Y.Map/Y.Doc capabilities mapped above are production-ready in single-node mode
Cluster (current)Partition-routing, no RaftSafe when one node owns the partition; weaker guarantees under node failure — see /docs/roadmap
Cluster (planned)Raft-replicated — plannedCluster-safe distributed locks and split-brain protection require Raft consensus, on roadmap — see /docs/roadmap

For a first migration, start on single-node. The partition-routed cluster mode works but does not yet have Raft-backed consensus; see the roadmap for cluster-safety status.

Why migrate?

  • Local-first reads/writes: Operations never wait for network; the UI updates immediately (parity with Y.js).
  • Server-side filtering and indexing: Y.js requires you to download the full document client-side and filter locally. TopGun supports client.query(filter) so the server returns only matching rows.
  • Server-side queries: SQL-style queries and full-text search (tantivy) on the server. Y.js has no server-side query layer.
  • Plain Postgres durable store: Migrate data off TopGun via pg_dump. Y.js typically pairs with a custom server (y-websocket, y-redis, y-leveldb) — TopGun ships with the durable store.
  • Offline persistence via IDBAdapter: Always-on parity with IndexeddbPersistence.
  • CRDT conflict resolution by HLC: Deterministic across clients (parity with Y.js’s CRDT semantics, but at whole-document granularity, not character-level).
  • Performance: 483K ops/sec fire-and-forget, ~37K ops/sec fire-and-wait at 1.5ms p50 on M1 Max (200 connections). See benchmarks for methodology.

Migration checklist

  1. Audit Y.js features in use — Y.Doc, Y.Map, Y.Array, Y.Text, Y.Awareness, Y.UndoManager, and the provider stack (y-websocket, y-indexeddb, y-leveldb, y-redis, etc.).
  2. Cross-reference each against the Concept Mapping table above; mark mappings vs. gaps.
  3. Flag any feature that maps to a “Not supported” or “Roadmap” cell — those need an explicit decision (drop, work around, or wait for the roadmap item).
  4. Port the provider layer: replace Y.Doc + WebsocketProvider + IndexeddbPersistence with new TopGunClient({ serverUrl, storage: new IDBAdapter() }) and client.setAuthToken(jwt).
  5. Port reads/writes/observes one map at a time: replace yMap.set / yMap.get / yMap.observe with client.getMap().set / .get / .onChange (and client.query().subscribe for filter-aware server-pushed deltas).
  6. Backfill data: write a one-time migration script that reads from your Y.js persistence (e.g. y-leveldb / y-redis / y-mongodb) and writes to TopGun via the client SDK or directly to Postgres.