Read this first — what's shipped vs. planned

Shipped today (single-node): JWT auth, per-map ACL booleans (read/write), JWT roles claim extraction, LWW-Map and OR-Map CRDTs, IndexedDB offline storage, WebSocket realtime sync, full-text search (tantivy), HLC-ordered pub/sub topics. Planned (not yet available): First-class RBAC role abstraction, map-pattern security policies (users:*, public:*), field-level allow-lists, cluster mTLS, Raft-replicated multi-region. See /docs/roadmap for the full maturity matrix.

DocsGuidesMigrating from Firebase

Migrating from Firebase

This guide is for Firestore users, Firebase Realtime Database users, and anyone considering leaving Firebase due to vendor lock-in, per-MAU cost surprises, or the inability to write data offline without waiting for a round-trip. TopGun maps most Firebase data primitives directly — with local-first reads and writes, CRDT conflict resolution, and a self-hosted OSS alternative to Firebase’s proprietary platform. Before diving in, cross-check any Firebase feature you rely on against the Concept Mapping table below and the roadmap to confirm it is available today.

Concept Mapping

Firebase conceptTopGun equivalentNotes
Firestore Documentclient.getMap(‘users’).get(userId)Local-first; no network round-trip on read
Firestore Collectionclient.getMap(‘users’)Server-side full-text and SQL-style queries available
onSnapshot() realtime listenerclient.query(name, filter).subscribe(callback) (vanilla JS) / useQuery() React hookServer-pushed deltas via WebSocket; filter-aware — mirrors Firebase onSnapshot query semantics
serverTimestamp()Hybrid Logical Clock (automatic, server-sanitized)HLCs are causality-aware; no clock-skew bugs
Batched write / runTransactionImplicit batching in SyncEngine outboxWrites coalesce automatically into a single OpBatch over WebSocket; no explicit batch API needed
Last-write-wins conflictLWW-Map merge by HLC + node-id tiebreakDeterministic across clients; no “lost update” surprises
Set-add / set-removeOR-Map (Observed-Remove)Concurrent adds/removes via ORMap never lose data
Realtime Database ref.on(‘value’)client.getMap(name).onChange(callback)Local LWWMap change notification — fires on any write to the map (local or synced); callback receives no arguments, caller reads getMap().get(key) for current value
Presence (onDisconnect)Server release_on_disconnect API for locks/topics/countersSingle-node only today; cluster presence wiring is planned (see /docs/roadmap)
Offline persistence (enableIndexedDbPersistence)First-class via IDBAdapterAlways-on; reads/writes never wait for network
Firestore Security RulesJWT auth + per-map ACL read/write booleansPattern-based rules (users/{userId}) on roadmap — see /docs/roadmap
Cloud Functions onWrite triggerNot yet shipped — Entry Processor is design-only (see /docs/roadmap)No current server-side equivalent; see /docs/roadmap for planned delivery
Per-MAU billingNone — self-hosted, OSS (Apache-2.0)Cost = your infra; no usage-based pricing
Vendor lock-inNone — open source, MsgPack wire protocol, plain Postgres durable storeMigrate off TopGun via pg_dump

Side-by-Side Code Patterns

Pattern A — Initialize and authenticate

Firebase
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';

const app = initializeApp({
  apiKey: '...',
  authDomain: 'my-app.firebaseapp.com',
  projectId: 'my-app',
});

const auth = getAuth(app);
await signInWithEmailAndPassword(
  auth,
  'user@example.com',
  'password'
);
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);

TopGun does not issue JWTs itself. Bring your own auth provider (BetterAuth, Clerk, Firebase Auth, or any OIDC-compatible issuer) and pass the resulting JWT via client.setAuthToken(). See the Authentication guide for step-by-step setup.

Pattern B — Read, write, and subscribe to realtime updates

Firebase (Firestore)
import {
  getFirestore,
  doc,
  getDoc,
  setDoc,
  onSnapshot,
} from 'firebase/firestore';

const db = getFirestore(app);
const ref = doc(db, 'users', userId);

// Read — requires a network round-trip on cache miss
const snap = await getDoc(ref);
console.log(snap.data());

// Write
await setDoc(ref, { name: 'Alice', age: 30 });

// Realtime subscription
const unsub = onSnapshot(ref, (snap) => {
  console.log(snap.data());
});
TopGun
import { TopGunClient, Predicates } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';

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

// Read — synchronous, local-first (no await)
const record = client.getMap('users').get(userId);
console.log(record);

// Write — synchronous, local-first; syncs in background
client.getMap('users').set(userId, { name: 'Alice', age: 30 });

// Realtime subscription (vanilla JS):
// QueryHandle.subscribe receives server-pushed deltas,
// filtered server-side — mirrors onSnapshot query semantics.
const query = client.query('users', {
  predicate: Predicates.equal('id', userId),
});
const unsub = query.subscribe((results) => {
  console.log(results);
});

// React shortcut — wraps the same QueryHandle:
// const results = useQuery('users', { where: ... });

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

TopGun reads and writes are synchronous because data lives in local memory first. The SyncEngine syncs changes to the server in the background. The client.query().subscribe() path delivers server-pushed, filter-aware deltas — the closest equivalent to Firestore’s onSnapshot query listener. client.getMap().onChange() is a lower-level notification that fires on any write to the local LWWMap (local or synced), with no filter; the callback receives no arguments and the caller reads the current value by calling get().

If you’re using React

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

function UserCard({ userId }: { userId: string }) {
  const { data: users } = useQuery<UserProfile>('users');
  const { update } = useMutation<UserProfile>('users');
  const profile = users.find(u => u._key === userId);
  const setOnline = () => update(userId, { name: profile?.name ?? '', status: 'online' });
  return <button onClick={setOnline}>{profile?.name}</button>;
}

Pattern C — Batched writes

Firebase (Firestore)
import { getFirestore, writeBatch, doc } from 'firebase/firestore';

const db = getFirestore(app);
const batch = writeBatch(db);

batch.set(doc(db, 'users', 'alice'), { name: 'Alice' });
batch.set(doc(db, 'users', 'bob'), { name: 'Bob' });
batch.set(doc(db, 'posts', 'p1'), { title: 'Hello' });

// Explicit commit required
await batch.commit();
TopGun
// Multiple .set() calls are automatically coalesced
// by the SyncEngine outbox into a single OpBatch
// sent over WebSocket. No explicit batch API needed.
// CRDT semantics make per-key writes commutative.

client.getMap('users').set('alice', { name: 'Alice' });
client.getMap('users').set('bob', { name: 'Bob' });
client.getMap('posts').set('p1', { title: 'Hello' });

// All three writes are batched automatically
// and delivered as one OpBatch to the server.

The same auto-batching applies to multiple useMutation .create() / .update() calls inside a React render or event handler.

Deployment Mode Semantics

DeploymentStatusNotes
Single-node (recommended for migration)StableAll Firestore/RTDB 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 — 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: Operations never wait for network; the UI updates immediately. Compare to Firestore’s getDoc, which requires a network round-trip on a cache miss.
  • CRDT conflict resolution: LWW-Map and OR-Map merge deterministically via HLC. No “lost update” surprises when two clients write the same key offline.
  • Self-hosted, OSS (Apache-2.0): Run TopGun on your own cloud, your own Postgres. No proprietary platform, no vendor agreement required.
  • Plain Postgres durable store: Migrate data off TopGun with pg_dump. Your data is always in a table you can query directly.
  • 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 which Firebase features you use — Firestore, Realtime Database, Authentication, Cloud Functions, Storage, etc.
  2. Cross-reference each feature against the Concept Mapping table above.
  3. Flag any planned-only items (Cloud Functions trigger, map-pattern security rules, cluster mTLS) against /docs/roadmap and decide whether to wait or work around.
  4. Port auth: replace Firebase Auth with a JWT issuer of your choice and configure client.setAuthToken(). See Authentication.
  5. Port reads/writes one collection at a time: replace doc() / getDoc() / setDoc() with getMap().get() / getMap().set(), and replace onSnapshot with client.query().subscribe() (vanilla JS) or useQuery() (React).
  6. Backfill existing data via a one-time import script that reads from Firestore and writes to TopGun via the client SDK or direct Postgres insert.