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.
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 Concept | TopGun Equivalent | Notes |
|---|---|---|
Y.Doc | TopGunClient instance | A single client owns many maps; there is no per-document boundary as in Y.js |
Y.Map | client.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.Array | No direct equivalent | Use 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.Text | Not supported | TopGun is whole-document sync, not character-level OT; rich-text editor bindings (ProseMirror, Quill, Monaco, TipTap) are out of scope |
Y.Awareness | release_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 outbox | Multiple 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 config | Single WebSocket connection per client multiplexes all maps and queries |
Y.UndoManager | Not supported | No per-document undo/redo with origin tracking; build at the application layer if needed |
Y.RelativePosition | Not supported | No character-level OT cursors — TopGun has no character-position concept |
encodeStateAsUpdate / applyUpdate | MerkleTree-based delta sync | Automatic on reconnect; not a public API — clients exchange Merkle hashes to determine deltas |
| Authentication | JWT via client.setAuthToken() | Y.js providers do not include auth — bring-your-own-issuer in both projects. See Authentication |
| Server-side query | SQL-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 license | Apache-2.0; self-host the Rust server | Both 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
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,
);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
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));
});
});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
| Deployment | Status | Notes |
|---|---|---|
| Single-node (recommended for migration) | Stable | All Y.Map/Y.Doc capabilities mapped above are production-ready in single-node mode |
| Cluster (current) | Partition-routing, no Raft | Safe when one node owns the partition; weaker guarantees under node failure — see /docs/roadmap |
| Cluster (planned) | Raft-replicated — planned | Cluster-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 withIndexeddbPersistence. - 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
- 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.).
- Cross-reference each against the Concept Mapping table above; mark mappings vs. gaps.
- 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).
- Port the provider layer: replace
Y.Doc + WebsocketProvider + IndexeddbPersistencewithnew TopGunClient({ serverUrl, storage: new IDBAdapter() })andclient.setAuthToken(jwt). - Port reads/writes/observes one map at a time: replace
yMap.set/yMap.get/yMap.observewithclient.getMap().set/.get/.onChange(andclient.query().subscribefor filter-aware server-pushed deltas). - 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.