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.

DocsGuidesMigrating from Replicache

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 ConceptTopGun EquivalentNotes
ReplicacheClientTopGunClientSingle 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 / pushURLWebSocket serverUrlSingle bidirectional WebSocket — no separate pull/push HTTP endpoints to host
ClientGroupIDClientID + JWT subjectClient identity is per-connection ClientID; user identity comes from the JWT sub claim
Pluggable indexesPlannedPluggable secondary indexes are on the roadmap — see /docs/roadmap
Replicache license feeOSS Apache-2.0No per-MAU pricing; cost = your infra
useSubscribe React hookuseQuery() React hookWraps client.query().subscribe() and re-renders on delta
SaaS server hostingSelf-hosted Rust serverNo SaaS invoice — run the Rust server alongside your own Postgres
Optimistic mutationsCRDT-merge auto-resolves conflictsLocal writes apply immediately; HLC-ordered merge converges deterministically — no custom optimistic logic required
Per-mutator validationPer-map ACL booleansCoarser than per-mutator hooks today; planned: Entry Processor for fine-grained server-side hooks (see /docs/roadmap)
IndexedDB persistenceIDBAdapter (passed to TopGunClient)Always-on local persistence; reads/writes never wait for network
AuthenticationJWT 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

Replicache
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,
});
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 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

Replicache
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()),
  [],
);
TopGun (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(), { 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

Replicache
// 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'],
});
TopGun
// 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

DeploymentStatusNotes
Single-node (recommended for migration)StableAll Replicache mappings 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 — plannedMulti-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

  1. Audit Replicache features in use (mutators, pull/push, subscribe, indexes, ClientGroupID, etc.).
  2. Cross-reference each against the Concept Mapping table; flag mappings vs gaps.
  3. Flag any feature mapping to “Not supported” / “Roadmap” cells — explicit decision needed (drop, work around, wait for roadmap).
  4. 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).
  5. Port reads/writes/subscribes one collection at a time: replace tx.set / tx.scan / useSubscribe with getMap().set / client.query() / useQuery().
  6. 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.