Read this first — what's shipped vs. planned
TopGun does NOT replicate Replicache's server-mutator + client-side optimistic-mutation model. CRDT-merge semantics (LWW-Map / OR-Map by HLC) remove the need for explicit mutator functions, but they also mean there is no server-authoritative custom mutation logic. Replicache's Pull/Push protocol and per-mutator validation logic must be re-architected, not ported: validation in TopGun runs as per-map ACL booleans plus (planned) Entry Processor — not as per-mutation server functions. Replicache useSubscribe with query.scan and pluggable indexes maps to client.query() with predicates, but pluggable secondary indexes are still on the roadmap. Reference: /docs/roadmap.
Migrating from Replicache
This guide is for Replicache users (including those on the deprecated upstream) and anyone evaluating Replicache as an architecture pattern. TopGun replaces Replicache’s mutator + Pull/Push round-trip with CRDT-merge sync, but the architectural shift is real: validate every Replicache feature you depend on against the Concept Mapping table and the roadmap before committing to a port.
Concept Mapping
| Replicache Concept | TopGun Equivalent | Notes |
|---|---|---|
ReplicacheClient | TopGunClient | Single client entry point; constructed with serverUrl and a storage adapter |
mutators (custom server-side functions) | CRDT writes via getMap().set() | No equivalent for custom server-side mutator logic — CRDT-merge replaces explicit mutators; rewrite, do not port |
replicache.query(tx => tx.scan().toArray()) | client.query(name, predicate) | Server-side filter via predicate; results are local-first when cached |
replicache.subscribe(tx => tx.scan(), { onData }) | client.query().subscribe() | Server-pushed deltas via WebSocket; predicate-aware subscription |
pullURL / pushURL | WebSocket serverUrl | Single bidirectional WebSocket — no separate pull/push HTTP endpoints to host |
ClientGroupID | ClientID + JWT subject | Client identity is per-connection ClientID; user identity comes from the JWT sub claim |
| Pluggable indexes | Planned | Pluggable secondary indexes are on the roadmap — see /docs/roadmap |
| Replicache license fee | OSS Apache-2.0 | No per-MAU pricing; cost = your infra |
useSubscribe React hook | useQuery() React hook | Wraps client.query().subscribe() and re-renders on delta |
| SaaS server hosting | Self-hosted Rust server | No SaaS invoice — run the Rust server alongside your own Postgres |
| Optimistic mutations | CRDT-merge auto-resolves conflicts | Local writes apply immediately; HLC-ordered merge converges deterministically — no custom optimistic logic required |
| Per-mutator validation | Per-map ACL booleans | Coarser than per-mutator hooks today; planned: Entry Processor for fine-grained server-side hooks (see /docs/roadmap) |
| IndexedDB persistence | IDBAdapter (passed to TopGunClient) | Always-on local persistence; reads/writes never wait for network |
| Authentication | JWT via client.setAuthToken() | BYO JWT issuer (BetterAuth, Clerk, OIDC, etc.) — TopGun does not issue JWTs itself |
Side-by-Side Code Patterns
Pattern A — Initialize and authenticate
import { Replicache } from 'replicache';
const rep = new Replicache({
name: 'user-123',
mutators: {
createTodo: async (tx, { id, text }) => {
await tx.set(`todo/${id}`, { id, text });
},
},
pushURL: '/api/replicache-push',
pullURL: '/api/replicache-pull',
auth: jwt,
});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 Replicache, the client is configured with a mutators map and separate pushURL/pullURL HTTP endpoints that you implement on your own server. In TopGun, there are no mutator functions and no push/pull endpoints to host: the client opens a single WebSocket to serverUrl and writes go directly through CRDT-merge. JWT issuance is your responsibility in both systems.
Pattern B — Read, write, subscribe
import { useSubscribe } from 'replicache-react';
// Write — via mutator
await rep.mutate.createTodo({
id: '1',
text: 'Buy milk',
});
// Or, inside a mutator function:
// await tx.set('todo/1', { id: '1', text: 'Buy milk' });
// Subscribe — re-runs on any matching change
const todos = useSubscribe(
rep,
async (tx) =>
(await tx.scan({ prefix: 'todo/' }).toArray()),
[],
);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(), { text: 'Buy milk', done: false });
return (
<>
<button onClick={add}>Add</button>
<ul>{todos.map(t => <li key={t._key}>{t.text}</li>)}</ul>
</>
);
}Replicache writes route through a named mutator that calls tx.set server-side; reads use tx.scan and useSubscribe re-runs the scan whenever data changes. TopGun writes go directly through useMutation — no mutator wrapper — and useQuery() delivers server-pushed deltas filtered server-side. Because Replicache’s typical usage is React-component-centric, the hook-first API is the natural replacement.
Pattern C — Multi-key writes
// Multi-key writes happen inside one mutator
const rep = new Replicache({
name: 'user-123',
mutators: {
createTodoWithTags: async (tx, { id, text, tags }) => {
await tx.set(`todo/${id}`, { id, text });
for (const tag of tags) {
await tx.set(`tag/${tag}/${id}`, true);
}
await tx.set(`stats/todoCount`, (await tx.get('stats/todoCount') ?? 0) + 1);
},
},
pushURL: '/api/replicache-push',
pullURL: '/api/replicache-pull',
});
await rep.mutate.createTodoWithTags({
id: '1',
text: 'Buy milk',
tags: ['shopping', 'urgent'],
});// Multiple .set() calls are automatically coalesced
// by the SyncEngine outbox into a single OpBatch
// sent over WebSocket. No mutator wrapper needed.
client.getMap('todos').set('1', { id: '1', text: 'Buy milk' });
client.getMap('tags').set('shopping/1', true);
client.getMap('tags').set('urgent/1', true);
// Counter increment uses an OR-Map or a dedicated
// counter primitive — see /docs/guides/counters.
// All writes are batched automatically into one
// OpBatch and delivered to the server in order.Replicache groups multi-key writes inside a single mutator function so they apply atomically on the server. TopGun has no mutator wrapper: separate getMap().set calls are coalesced by the SyncEngine outbox into a single OpBatch. CRDT semantics make per-key writes commutative, so atomicity is replaced by deterministic per-key convergence.
Deployment Mode Semantics
| Deployment | Status | Notes |
|---|---|---|
| Single-node (recommended for migration) | Stable | All Replicache mappings 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 | Multi-region with QUORUM/STRONG consistency 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: Synchronous local reads and writes — parity with Replicache optimistic mutations, without the mutator wrapper.
- CRDT-merge auto-resolves conflicts: LWW-Map and OR-Map merge deterministically via HLC; no need to design custom mutators for conflict-free operations.
- OSS Apache-2.0: No per-MAU pricing, no SaaS invoice — run the Rust server on your own infra.
- Self-hosted on your own Postgres: Plain Postgres durable store; migrate data off TopGun with
pg_dump. - 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 Replicache features in use (mutators, pull/push, subscribe, indexes, ClientGroupID, etc.).
- Cross-reference each against the Concept Mapping table; flag mappings vs gaps.
- Flag any feature mapping to “Not supported” / “Roadmap” cells — explicit decision needed (drop, work around, wait for roadmap).
- Rewrite mutator-based logic as CRDT writes (
getMap().set), or move custom validation into per-map ACL hooks (planned: Entry Processor for fine-grained server-side hooks). - Port reads/writes/subscribes one collection at a time: replace
tx.set/tx.scan/useSubscribewithgetMap().set/client.query()/useQuery(). - Backfill data via a one-time migration script that reads from Replicache’s IndexedDB (or your server-side Replicache cache store) and writes to TopGun via the client SDK or direct Postgres insert.