# TopGun — Full Documentation > Generated by apps/docs-astro/scripts/build-llms-full.mjs from a curated allowlist > of canonical pages. For the short index see /llms.txt. > Source: https://github.com/TopGunBuild/topgun --- # Welcome > Build real-time apps that work offline — instant local reads/writes, automatic sync when online, no conflict handling required. Docs Get Started Welcome # Welcome Build real-time apps that work offline. Local writes are instant and survive disconnects — reconnecting clients sync automatically without you writing conflict-handling code. Try the [live demo](https://demo.topgun.build/) to see it in action, or jump straight to the [Quickstart](/docs/quickstart) to scaffold a working app in under 5 minutes. ## What you can build TopGun is well-suited for collaborative tools, offline-first mobile and desktop web apps, local-first developer tools, and any app where users expect data to survive a flaky connection. You scaffold a React project, drop in a few hooks, and get instant local reads/writes with background sync — no custom sync logic, no conflict resolution ceremony. ## When to use TopGun - **Real-time collaboration** — shared docs, whiteboards, live editing - **Offline-first web and mobile apps** — todos, notes, forms that keep working without a signal - **Local-first developer tools** — IDEs, CLI tools, dashboards that feel instant - **Multi-tab / multi-device sync** — changes in one tab appear in another automatically - **AI-assisted apps** — agents can read and mutate live data via the built-in MCP server ## When not to use TopGun - Pure server-side analytical or reporting workloads where all reads/writes happen server-side - Single-writer admin tools where local-first overhead and offline persistence add no value - Scenarios requiring strong cross-document ACID transactions across many maps - Apps where all clients are always online and eventual consistency is an unacceptable trade-off ## Features Next Page Quickstart --- # Quickstart > Build a local-first React app in under 5 minutes — works offline by default. Docs Get Started Quickstart # Quickstart Build your first local-first application with TopGun in under 5 minutes. No server required to start — the app works offline by default and syncs when you add one. Want to see it running first? Try the [live demo](https://demo.topgun.build/). ## 1. Installation

Install the core client, adapters, the React hooks package, and Zod for schema definitions.

## 2. The Canonical App Hook-first is the simple way to read and write data with React. One read hook (`useQuery`) gives you the list and re-renders when data changes. One write hook (`useMutation`) adds new items. One type-safe `Todo` shape ties them together. The whole loop fits in 24 lines, and your list shows up instantly then keeps itself in sync as data changes. ## 3. How It Works Here is a step-by-step breakdown of the canonical snippet above: **Lines 1–4: Imports.** Three packages installed in step 1 (`@topgunbuild/react`, `@topgunbuild/client`, `@topgunbuild/adapters`) plus the `Todo` type derived from your schema. **Lines 6–9: Client init.** `TopGunClient` takes a `storage` adapter for local persistence. `IDBAdapter` writes to IndexedDB so your data survives page reloads. No explicit `start()` call is needed in module-level code — the adapter initializes lazily in the background. Uncomment `serverUrl` to connect to a sync server. **Line 12: Read hook.** `useQuery('todos')` subscribes to the `todos` map. The component re-renders automatically whenever data changes locally or arrives from the server. **Line 13: Write hook.** `useMutation('todos')` returns `{ create, update, remove }`. Call `create(key, value)` to add a new item. The write lands in memory immediately (zero-latency) and syncs to the server in the background when a server is configured. **Line 14: Add function.** A plain function that calls `create` with a timestamp-based key and the new todo shape. No loading state, no async/await in the component — the write is always instant. **Lines 15–20: Render.** Standard React JSX. `todos.map(t => ...)` iterates the live list. Use `t._key` as the React key — this is the string key you passed to `create`, exposed on every query result item. **Line 23: Provider.** `TopGunProvider` makes the client available to all hooks in the tree via React context. ## 4. End-to-End Types No manual `interface` needed — the type flows from the Zod schema through the hook generic: ```ts // schema.ts export type Todo = z.infer; ``` ```tsx // app.tsx const { data: todos = [] } = useQuery('todos'); ``` `z.infer` produces `{ text: string; done: boolean }`. Pass that as the generic to `useQuery` and TypeScript knows the shape of every item in `todos` — autocomplete works, typos are caught at compile time, and you never have to keep a manual interface in sync with your schema. ## 5. Imperative API (advanced) Use when outside React — service workers, Node scripts, non-component utilities, or when you need direct map access without hooks. `client.getMap('todos')` returns the map directly. `.set(key, value)` writes to local memory immediately and queues the change for sync. This is not the recommended path for UI code — prefer `useQuery` and `useMutation` in React components.

Non-Blocking Initialization

IndexedDB can take 50-500ms to initialize. TopGun's IDBAdapter initializes lazily in the background and queues reads and writes until the store is ready — your UI renders instantly with zero blocking time and no await ceremony. If you need a hard signal that persistence is ready (e.g., before a critical migration step), keep a reference to the adapter and call await adapter.waitForReady().

Persistence

By using IDBAdapter, your data is automatically saved to IndexedDB. Even if the user refreshes the page or closes the browser, the state is preserved locally.

Optional: Encrypt Local Storage

For sensitive data, wrap your adapter with EncryptedStorageAdapter to encrypt data at rest. See the Client-Side Encryption section in the Security Guide.

Building with an AI Agent?

See the AI Builder guide for prompt templates, agent setup, and live database access via MCP. ## 6. Add real-time sync (optional) By default the app runs in local-only mode — data persists in IndexedDB and works offline without a server. To add real-time sync across browsers and devices: 1. Uncomment the `serverUrl` line in the client init above. 2. Start the TopGun server in a second terminal: ```bash pnpm start:server ``` The server boots in seconds with embedded storage (`./topgun.redb`) — no Postgres, no Docker required. 3. Open your app in two browser tabs and watch changes sync in real time. In production, always use `wss://` instead of `ws://` to encrypt data in transit. See the [Security Guide](/docs/guides/security) for TLS configuration. Previous Welcome Next Section Tutorial: Real-time Todo --- # Installation > Install the TopGun client SDK and necessary adapters to get started. Docs Get Started Installation # Installation

Package Manager

Install the TopGun client SDK and necessary adapters to get started.

Core Initialization

The most robust way to initialize TopGun is by creating the client and passing your storage adapter explicitly.

Note: TopGunClient provides full control over configuration and lifecycle. Use this for production apps.

Server Development

Boot the server from the repository root. The default backend is embedded redb (durable, on-disk, no Postgres required); the only prerequisite is a Rust toolchain to compile the binary.

No external database required. The server writes to ./topgun.redb by default; data survives restarts. Set TOPGUN_NO_AUTH=1 for auth-free local exploration. See the Server & CLI reference for flags and env vars. Previous Comparison Next Section Quickstart --- # Core Concepts > TopGun is built on a foundation of distributed systems theory. Understanding these concepts will help you design better local-first applications. Docs Concepts # Core Concepts TopGun is built on a foundation of distributed systems theory. Understanding these concepts will help you design better local-first applications. --- # Local-First Architecture > TopGun is designed for the modern "Local-First" web. It fundamentally changes how applications interact with data, inverting the traditional client-server relationship. Docs Concepts Local-First # Local-First Architecture TopGun is designed for the modern "Local-First" web. It fundamentally changes how applications interact with data, inverting the traditional client-server relationship. ## The Philosophy In a Local-First app, the primary data source is the **local database** on the user's device. The network is treated as an optional enhancement, rather than a critical dependency. ## Why it matters Traditional architectures degrade user experience by introducing network latency into every interaction. TopGun ensures your app feels native and responsive, regardless of network conditions—whether in a tunnel, an elevator, or on a flaky 3G connection. Previous Concepts Next Concept CRDTs & Time --- # CRDTs & Hybrid Logical Clocks > How TopGun manages state consistency and time across distributed, offline-first clients without a central coordinator. Docs Concepts CRDTs & Time # CRDTs & Hybrid Logical Clocks How TopGun manages state consistency and time across distributed, offline-first clients without a central coordinator. ## Optimistic UI & Consistency TopGun uses **Optimistic Updates** by default. When a user performs an action, it is applied immediately to the local state. - **Immediate Feedback:** The UI updates instantly (0ms latency). - **Background Sync:** Behind the scenes, an Operation Log (OpLog) queues the mutation. - **Convergence:** The Sync Engine uploads changes when the network is available.

Data Flow

  1. User clicks "Save"
  2. map.set(key, val) → Local State Updated
  3. UI Refreshes (Frame 1)
  4. OpLog entry created
  5. Sync Engine pushes to Server (Async)
## Hybrid Logical Clocks (HLC) In distributed systems, you cannot rely on physical time (wall clock) because device clocks drift or can be manually changed. TopGun solves this with **Hybrid Logical Clocks**. ### How it works Every event in TopGun is tagged with an HLC timestamp. This timestamp combines: - **Physical Time:** The wall clock time (e.g., from `Date.now()`). - **Logical Counter:** A counter that increments if multiple events happen within the same millisecond or if the physical clock regresses. - **Node ID:** A unique identifier for the device/client, used as a tie-breaker when timestamps are equal.

Why is this important?

It allows TopGun to correctly order events across devices even if their clocks are slightly off. "Last-Write-Wins" is determined by comparing HLCs, not unreliable system time.

Clock Drift Tolerance

TopGun tolerates clock drift of up to 60 seconds between devices. If a remote timestamp is more than 60 seconds ahead of local time, the event is still accepted (to maintain availability), but a warning is logged. This ensures the system remains functional even with misconfigured device clocks.

## Conflict-Free Replicated Data Types (CRDTs) TopGun guarantees that all clients eventually converge to the same state without manual conflict resolution code. It uses a **Last-Write-Wins (LWW) Map** CRDT structure. Previous Local-First Next Concept Sync Protocol --- # Sync Protocol > Understanding how TopGun moves data between clients and servers efficiently using Merkle Trees and WebSockets. Docs Concepts Sync Protocol # Synchronization Protocol Understanding how TopGun moves data between clients and servers efficiently using Merkle Trees and WebSockets. ## Sync vs. Request Instead of writing thousands of API endpoints (`GET /users`, `POST /todos`), TopGun synchronizes state.

Traditional REST/GraphQL

  • • Fetch data explicitly
  • • Send requests to mutate
  • • Handle loading/error states
  • • Manual retry logic

TopGun Sync

  • • Subscribe to datasets
  • • Mutate local objects directly
  • • Automatic retries & conflict resolution
  • • Real-time updates pushed to you
## Merkle Tree Synchronization To keep bandwidth usage extremely low, TopGun doesn't re-download the whole dataset every time you reconnect. It uses **Merkle Trees** (hash trees) to efficiently detect differences.

The Exchange Process

  1. 1

    Handshake: Client and Server exchange the Root Hash of their respective Merkle Trees.

  2. 2

    Comparison: If hashes match, data is identical. Sync complete (0 bytes transferred).

  3. 3

    Drill Down: If hashes differ, they request hashes of child nodes (buckets) to pinpoint the exact difference.

  4. 4

    Patch: Only the modified records (leaves) are transmitted over the wire.

## HTTP Sync For serverless environments where persistent WebSocket connections are unavailable, TopGun provides a stateless HTTP sync transport via `POST /sync`. Unlike WebSocket sync, which maintains a persistent connection for real-time push updates and Merkle tree delta exchange, HTTP sync uses a polling-based request-response model. The client accumulates operations locally and periodically sends them to the server along with sync timestamps. The server processes the operations, computes deltas from its in-memory LWWMap for records newer than the client's `lastSyncTimestamp` (using HLC comparison), and returns acknowledgments and delta records in a single response. Each request carries full client context -- `clientId`, `clientHlc`, and `syncMaps` with per-map timestamps -- so any server node behind a load balancer can handle any request without session affinity. Authentication is performed via an `Authorization: Bearer ` header on every request.

The HTTP Sync Exchange

  1. 1

    Accumulate: Client writes operations to local state (LWWMap + OpLog) and queues them for sync.

  2. 2

    Poll: At the configured poll interval (default 5 seconds), client sends POST /sync with queued operations and syncMap timestamps indicating the last known server state per map.

  3. 3

    Process: Server applies client operations using CRDT merge semantics, then computes deltas by iterating in-memory maps and filtering records newer than the client's lastSyncTimestamp.

  4. 4

    Respond: Server returns acknowledgments for processed operations, delta records for requested maps, and any one-shot query results -- all in a single response.

  5. 5

    Apply: Client applies delta records to local state and updates lastSyncTimestamp per map for the next poll cycle.

## When to Use Which Transport
Criterion WebSocket HTTP Sync AutoConnectionProvider
Real-time updates Pushed instantly Polled (configurable interval) WS when available, HTTP fallback
Serverless compatible No (needs persistent connection) Yes (stateless requests) Yes (auto-detects)
Live query subscriptions Yes No (one-shot queries only) Depends on active transport
Bandwidth efficiency Merkle tree delta sync Timestamp-based delta sync Best available
Connection cost Per-connection billing Per-request billing Adapts to environment
Recommended for Real-time apps, VPS/container deployments Serverless functions, edge functions Unknown deployment target
## Server Architecture While clients are "Local-First", TopGun is backed by a Rust server (single-node stable today; cluster mode in development).

1. Gateway Node

Handles WebSocket connections and routes traffic.

2. Partition Engine

In-memory sharding logic that distributes data across the cluster.

3. Persistence Layer

Async write-behind to the configured backend (redb embedded default, PostgreSQL optional for production).

4. Pub/Sub Bus

Broadcasts updates to other connected clients in real-time.

Previous CRDTs & Time Next Data Structures --- # Data Structures > TopGun provides two primary CRDT-based data structures - LWW-Map (Last-Write-Wins Map) and OR-Map (Observed-Remove Map). Understanding the difference between them is critical for preventing data loss in distributed applications. # Data Structures TopGun provides two primary CRDT-based data structures: **LWW-Map** (Last-Write-Wins Map) and **OR-Map** (Observed-Remove Map). Understanding the difference between them is critical for preventing data loss in distributed applications. ## LWW-Map (Last-Write-Wins) The **LWW-Map** is the default map structure in TopGun. As the name suggests, it resolves conflicts by accepting the update with the highest timestamp (Last-Write-Wins).

Use when: Each item in the collection is a unique entity identified by a stable key (e.g., a user profile by UUID, a document by ID). The LWW-Map is collection-oriented — `useQuery(mapName)` returns an array of all items, each with a `_key` property.

### How it works - Each key holds a single value and a timestamp. - When two nodes update the same key, the one with the later timestamp wins. - The collection pattern uses stable UUID keys (e.g., one profile per user) so concurrent updates to different profiles never collide. ### Usage ## OR-Map (Observed-Remove) The **OR-Map** is designed for Sets and Collections. It allows you to add multiple items to a key without them overwriting each other, even if they have the same value (depending on implementation) or if they are added concurrently. In TopGun, the OR-Map is implemented as a map where values are "observed" and can be removed without race conditions affecting re-additions.

Use when: Collections, Lists, Sets, and scenarios where multiple users might add/remove items concurrently (e.g., Todo Lists, Chat Messages in a channel, Tags).

### Why LWW is bad for Sets Imagine two users adding a "Todo Item" to a list. If you used LWW-Map and stored the list as a JSON array under a single key `todos`, the user who saves last would overwrite the other user's addition. With **OR-Map**, each addition is treated as a unique operation. Even if two users add the same value, or different values, both are preserved (unless one is explicitly removed). ### Usage ## Tombstones & Deletion When you delete data in TopGun, the record isn't immediately removed from storage. Instead, a **tombstone** is created. This is essential for proper synchronization with offline clients.

LWW-Map Tombstones

A tombstone is simply a record with value: null:

{`{ value: null, timestamp: {...} }`}
    

OR-Map Tombstones

Removed tags are stored in a tombstone set:

{`tombstones: Set`}
    

Garbage Collection: Tombstones are eligible for cleanup once all replicas have acknowledged the deletion. The LWWMap.prune() method removes tombstones older than a given threshold, preventing "resurrection" of deleted data.

## Summary
Feature LWW-Map OR-Map
Primary Use Case Key-Value properties (Profile, Config) Collections, Sets, Lists (Todos, Comments)
Conflict Resolution Last Write Wins (Overwrites) Union of Adds (Merge), Observed Remove
Deletion Tombstone (value: null) Tag added to tombstone set
Hook (UI) useQuery / useMutation useORMap
Previous Sync Protocol Next Section Guides --- # Schema-typed data > Add compile-time type safety to TopGun maps using the TopGunClient generic. Docs Guides Schema-typed data # Schema-typed data Schema-typed data: define your data shapes as TypeScript types (optionally derived from Zod schemas); pass them to `TopGunClient`; get autocomplete and compile-time errors on map operations. Type enforcement is compile-time only — there is no runtime validation or server-side schema enforcement in the current version. --- ## Defining a schema The recommended approach is to define your types via Zod schemas, then derive TypeScript types from them using `z.infer`. This gives you a single source of truth: Zod schemas for runtime validation in your business logic, TypeScript types for the compiler. If you prefer plain TypeScript without Zod, you can define the interface directly without the Zod step — just omit the `z.object(...)` declarations and write the interfaces by hand. Zod is not a dependency of TopGun; it is optional. --- ## Typed map access Pass your schema type as the generic parameter to `TopGunClient`. The client narrows the return types of `getMap`, `getORMap`, and `query` to your application types. Omitting `` falls back to the untyped overload — `getMap(name: string): LWWMap` — which preserves backward compatibility for callers that supply explicit type parameters manually. --- ## Typed hooks (React) The React hooks inherit the schema types when you pass a typed client to `TopGunProvider`. You can also specify the generic directly on the hook. --- ## Compile-time-only honesty caveat > **Important:** Type enforcement is compile-time only. Types are erased at runtime. If external code or another client writes data that does not match your interface, TypeScript will not catch it. Consider adding runtime validation (e.g. via Zod's `TodoSchema.parse(value)`) for untrusted data. Other limitations: | Limitation | Details | |-----------|---------| | **No server-side enforcement** | The server does not validate data against your TypeScript schema. Any valid CRDT operation is accepted regardless of the value shape. | | **Schema evolution** | If you add or rename fields in your interface, existing data in storage is not migrated. Use optional fields (`field?: type`) for new fields to handle records written before the schema change. | | **No cross-language schemas** | The TypeScript schema is only available in TypeScript clients. Other language clients must independently maintain compatible type definitions. | --- **Next steps** - [Search & live queries](/docs/guides/search-and-live-queries) — apply typed predicate queries to your typed maps - [Quickstart](/docs/quickstart) — canonical hook-first snippet with the Zod schema flow end-to-end - [Client API reference](/docs/reference/client) — full `getMap`, `getORMap`, `query` signatures Previous Live notifications Next Search & live queries --- # Offline-first apps > Build apps that work without a network and recover state automatically on reconnect. Docs Guides Offline-first apps # Offline-first apps Offline-first apps: TopGun reads and writes go to local IndexedDB first; the sync engine ships pending writes when the server reconnects; HLC timestamps make merges automatic. The result is an app that feels instant whether or not the network is present. --- ## Why offline-first Most apps treat the network as a requirement: reads block on a fetch, writes fail loudly when the server is unreachable. TopGun inverts this: - **Reads are local.** `useQuery` pulls from the in-memory CRDT map, which is backed by IndexedDB. No network round-trip required. - **Writes are local first.** `useMutation` writes immediately to the local map. The sync engine queues the write and ships it to the server when connected. - **Merges are automatic.** When two offline clients reconnect, TopGun reconciles their writes using HLC (Hybrid Logical Clock) timestamps. The most recent write per key wins (LWW semantics). No manual conflict handling. --- ## Setting up IDBAdapter Pass a `new IDBAdapter()` to the client constructor. The adapter persists the CRDT state to IndexedDB in the browser. No configuration needed — the database name is derived from the client's `nodeId`. Once the client has started, writes survive a page reload. On the next load, the in-memory state is rebuilt from IndexedDB before the server connection is attempted — so your app renders instantly from local state. --- ## Detecting connection state Use the `useSyncState` hook to get per-record sync status, or listen to the client's connection events directly for a global indicator. ### Per-record sync status (React) ### Global connection state (imperative) --- ## Reconnect and merge When the client reconnects after a period offline, TopGun uses a Merkle tree to compute the exact delta between the local state and the server state. Only the records that changed on either side are exchanged — not the full dataset. From the application's perspective, this is invisible: `useQuery` subscribers receive the merged results automatically. You do not need to trigger a manual refresh. --- ## Local-only mode TopGun works without a server at all. Omit `serverUrl` for a fully local app — reads and writes go to IndexedDB only, with no sync. Local-only mode is useful for: - **Progressive disclosure** — ship a working local app first; add sync later - **Private data** — user preferences, drafts, or cached data that should never leave the device - **Testing** — write deterministic unit tests without mocking a server --- **Next steps** - [Real-time collaboration](/docs/guides/realtime-collaboration) — build multi-user shared maps on top of the same local-first foundation - [Live notifications](/docs/guides/live-notifications) — ephemeral topic messages that complement offline-capable maps - [Schema-typed data](/docs/guides/schema-typed-data) — add compile-time type safety to your offline map operations Previous Real-time collaboration Next Live notifications --- # Building TopGun Apps with AI Coding Agents > How to build a TopGun app using Claude Code, Cursor, or Codex — prompt templates, context to seed the conversation, common pitfalls, and live database access via MCP. Docs Guides Building with AI # Building TopGun Apps with AI Coding Agents Drop the right context into Claude Code, Cursor, or Codex once, and your agent will scaffold a working TopGun app in under five minutes — no manual API discovery required. This guide walks through the prompt template, context block, common pitfalls to pre-empt, and how to give your agent live database access via MCP. ## 1. Pick your agent TopGun works with any AI coding agent. Three are well-suited for this workflow: | Agent | Suits | Install / docs | |-------|-------|---------------| | [Claude Code](https://claude.ai/code) | Agentic CLI sessions — reads your repo, runs commands, commits code | [docs.anthropic.com/claude-code](https://docs.anthropic.com/en/docs/claude-code/overview) | | [Cursor](https://cursor.sh) | IDE-integrated workflow — inline completions + Agent panel in VS Code fork | [cursor.sh/docs](https://docs.cursor.com/) | | [Codex](https://platform.openai.com/docs/guides/codex) | API-driven pipelines — scriptable, headless, OpenAI-native | [platform.openai.com/docs/guides/codex](https://platform.openai.com/docs/guides/codex) | TopGun is post-LLM pretraining cutoff, so all three agents need the context block in section 2 to produce correct code on first iteration — they will not have TopGun in their training data. ## 2. Give it context The key step: give your agent the `llms-full.txt` bundle so it has the full API surface in its context window. **Paste this block at the start of every new TopGun session:** This gives your agent three things: 1. The complete API reference loaded from [`https://topgun.build/llms-full.txt`](https://topgun.build/llms-full.txt) 2. The canonical hook-first snippet (the same shape as the [Quick Start canonical app](/docs/quickstart#the-canonical-app)) 3. A one-line mental model: **hooks for reads (`useQuery`), `useMutation` for writes, schema in Zod, `pnpm start:server` for the local backend** Without this block, agents reliably hallucinate methods like `client.subscribe()` or reach for raw map getters for reads, which won't re-render on data changes. ## 3. Recommended prompt template After giving your agent the context block above, describe what you want to build. Use this template as your starting point: **Examples:** - `Build me a todo app using TopGun. Schema: text:string, done:boolean. Permissions: open.` - `Build me a chat app using TopGun. Schema: author:string, body:string, ts:number. Permissions: per-user.` - `Build me a drawing app using TopGun. Schema: x:number, y:number, color:string. Permissions: open.` Replace the brackets with your actual requirements. The `Use the hook-first API` line is load-bearing — include it verbatim to steer the agent toward `useQuery` / `useMutation` from the first generation. ## 4. Common agent mistakes and how to fix them **Mistake 1: Used `client.getMap` instead of `useQuery`** *Symptom:* The app renders initial data but doesn't update when other users add or change records. *Why it happens:* `client.getMap('todos')` returns a raw map object — it doesn't subscribe to React state. The agent reaches for it because it looks like a standard getter. *Fix:* Swap every read to `useQuery('mapName')`. Cross-reference: [Quick Start canonical app](/docs/quickstart#the-canonical-app). ```tsx // Wrong — no re-renders when data changes const map = client.getMap('todos'); // Correct — live-updating, re-renders on every change const { data: todos = [] } = useQuery('todos'); ``` **Mistake 2: Hallucinated a method that doesn't exist** *Symptom:* The agent generates code calling `client.subscribe()`, `client.watch()`, `client.on()`, or a similar method that doesn't exist in the TopGun client API. *Why it happens:* Agents extrapolate from Firebase / Supabase patterns they were trained on. *Fix:* Connect the agent to the MCP server (see section 5). With live database access, the agent can call `topgun_list_maps` and `topgun_schema` tools to introspect the real API surface, eliminating hallucination. See the [MCP Server guide](/docs/guides/mcp-server). **Mistake 3: Forgot the server bootstrap** *Symptom:* `WebSocket connection to 'ws://localhost:8080' failed` or `ECONNREFUSED` errors in the browser console. *Why it happens:* The agent generates correct client code but doesn't know a server needs to be running locally. *Fix:* Run `pnpm start:server` in a second terminal before opening the app. See [Quick Start — Start the Server](/docs/quickstart#2-start-the-server). ```bash # In a second terminal — keep this running while you develop pnpm start:server ``` ## 5. Live database access via MCP Connect your agent to a running TopGun server so it can query your real data, introspect the schema, and avoid hallucination entirely. TopGun ships `@topgunbuild/mcp-server`, which implements the [Model Context Protocol](https://modelcontextprotocol.io/) and exposes eight tools to any MCP-compatible agent: `topgun_query`, `topgun_mutate`, `topgun_search`, `topgun_subscribe`, `topgun_schema`, `topgun_stats`, `topgun_explain`, `topgun_list_maps`. **Step 1:** Make sure `pnpm start:server` is running. **Step 2:** Add the TopGun MCP server to your agent's config. **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`): {/* Config reproduced from guides/mcp-server.mdx claudeConfigCode export — that file is the source of truth; update both if TOPGUN_URL key or @topgunbuild/mcp-server package name ever change */} **Cursor** (`.cursor/mcp.json`): {/* Config reproduced from guides/mcp-server.mdx cursorConfigCode export — that file is the source of truth; update both if TOPGUN_URL key or @topgunbuild/mcp-server package name ever change */} For the full MCP reference — all available tools, authentication, and remote server configuration — see the [MCP Server guide](/docs/guides/mcp-server). ## 6. Worked example Here is a complete session transcript: one context paste, one prompt, one correction, working app. --- **User (context block paste + prompt):** > You are building a TopGun app. TopGun is a local-first real-time sync library. > Load the full API reference from: https://topgun.build/llms-full.txt > [... seed prompt from section 2 ...] > > Build me a task list app using TopGun. Schema: text:string, done:boolean. Permissions: open. > Use the hook-first API: useQuery for reads, useMutation for writes. **Agent (first generation):** The agent reads `llms-full.txt`, sees the canonical hook-first snippet, and produces: **User (one correction):** > The checkbox isn't persisting after reload. The `update` call looks right — make sure `await client.start()` runs before the component mounts, not inside it. **Agent (correction):** The agent moves `await client.start()` to module scope (before the component function), which is already correct in the example above. With `await client.start()` at module level, the client is connected before the first render, and `update` calls go through immediately. **Result:** Working app in under five minutes. Data persists across reloads via IndexedDB and syncs across browser tabs in real time. --- **Why this works reliably:** The `llms-full.txt` bundle gives the agent ~120 KB of accurate TopGun API documentation in its context window. Agents don't need to be pre-trained on TopGun — they just need accurate docs at the start of the session. Previous MCP Server Next API Reference --- # Authentication & Security > Secure your TopGun app by integrating Clerk, BetterAuth, Firebase, or a custom JWT provider. Docs Guides Authentication # Authentication & Security Authentication and security: connect any JWT-issuing provider (Clerk, BetterAuth, Firebase, or custom) to your `TopGunClient` instance; the client sends the token to the server on every WebSocket handshake; the server validates it and enforces per-collection RBAC access control. Local-first writes happen before the network authenticates, so offline clients can write immediately and reconcile permissions on reconnect. ## Setup Create the client once and export it. The `storage` field accepts `new IDBAdapter()` (zero-arg) for IndexedDB persistence in the browser. Register a token provider after the client is created. The provider is called on every `AUTH_REQUIRED` message — which the server sends on initial connection and after any reconnect. ## How it works ## Clerk integration Clerk is a popular React-first auth provider. Its JWTs use RS256 — configure JWT_SECRET with the Clerk RSA public key. ### Clerk JWT_SECRET (production) Clerk uses RSA asymmetric signing. Set `JWT_SECRET` to the RSA public key from Clerk's JWKS endpoint: ``` https://YOUR_CLERK_DOMAIN.clerk.accounts.dev/.well-known/jwks.json ``` The server automatically detects RSA public keys (by checking for `-----BEGIN`) and uses the RS256 algorithm. --- ## BetterAuth integration BetterAuth is a framework-agnostic auth library. The @topgunbuild/adapter-better-auth package lets TopGun serve as the BetterAuth database backend. ### Bridge endpoint: BetterAuth session → TopGun JWT --- ## Custom auth provider If you manage your own JWT issuance (e.g. a custom login endpoint), call `setAuthTokenProvider` with an async function that returns the token from your session store. --- ## Firebase integration Firebase uses RS256 for JWT signing. Obtain the Firebase public key from the Google JWKS endpoint and set it as `JWT_SECRET`. | Provider | Algorithm | JWT_SECRET value | |----------|-----------|-----------------| | Custom / self-hosted | HS256 | Shared secret string | | Clerk | RS256 | RSA public key (PEM) | | Firebase | RS256 | RSA public key (PEM) | | Auth0 | RS256 | RSA public key (PEM) | The TopGun server detects RSA keys automatically (by checking for `-----BEGIN`) and switches to RS256 validation. --- ## JWT token structure TopGun reads only the `sub` and `roles` claims from the JWT payload: --- ## Token lifecycle and refresh - **Active connections are not terminated when a token expires.** Once a WebSocket connection is established, token expiry does not disconnect the client. The session continues until the connection drops. - **Token expiry only matters on reconnect.** When the WebSocket closes (page reload, network drop), the server sends `AUTH_REQUIRED`. `setAuthTokenProvider` is called at that point to obtain a fresh token. - **Returning `null` from the provider leaves the connection unauthenticated.** No data operations are permitted in that state. - **Recommendation:** your token provider should call your app's token or session refresh endpoint rather than relying on a cached token that may be stale after a long offline period. --- ## Authentication protocol --- **Next steps** - [RBAC](/docs/guides/rbac) — configure per-collection read/write access control on the server - [Security (TLS)](/docs/guides/security) — enable TLS for the WebSocket connection in production - [Offline-first apps](/docs/guides/offline-first) — understand how authentication interacts with offline writes Previous Counters & locks Next Security (TLS) --- # Client API > TopGunClient — the imperative SDK surface that React hooks call under the hood.

Client API

`TopGunClient` is the imperative SDK entry point. It manages local CRDT maps, the sync engine, and the connection lifecycle. The class is generic over an optional schema type that narrows `getMap`, `getORMap`, and `query` to your application types. ```typescript interface AppSchema { todos: { text: string; done: boolean }; users: { name: string; email: string }; } const client = new TopGunClient({ storage: new IDBAdapter(), }); // Narrowed: LWWMap const todos = client.getMap('todos'); ``` The untyped fallback overloads remain for callers that pass explicit type parameters. ## Constructor ```typescript new TopGunClient(config: TopGunClientConfig) ``` `TopGunClientConfig` accepts: | Field | Type | Description | |---|---|---| | `storage` | `IStorageAdapter` | Required. Local persistence (typically `new IDBAdapter()`). | | `serverUrl` | `string` | Optional. WebSocket URL for single-server mode. Mutually exclusive with `cluster`. | | `cluster` | `TopGunClusterConfig` | Optional. Cluster configuration. Mutually exclusive with `serverUrl`. | | `nodeId` | `string` | Optional. Auto-generated UUID if omitted. | | `backoff` | `Partial` | Optional. Reconnect backoff tuning. | | `backpressure` | `Partial` | Optional. Backpressure thresholds and strategy. | | `auth` | `AuthProvider` | Optional. Pluggable auth provider (Clerk, Firebase, BetterAuth, Custom). | The constructor branches on three mutually exclusive modes: ### Local-only mode (default) When neither `serverUrl` nor `cluster` is supplied, the client uses `NullConnectionProvider`. Operations stay in memory and IndexedDB; no socket is opened. This is the default for offline demos, tests, and prototype apps. ```typescript const client = new TopGunClient({ storage: new IDBAdapter(), }); ``` ### Single-server mode Supply `serverUrl` to connect to one TopGun server over WebSocket. ```typescript const client = new TopGunClient({ serverUrl: 'ws://localhost:8080', storage: new IDBAdapter(), }); ``` ### Cluster mode Supply `cluster.seeds` (one or more seed URLs) for partition-aware routing across multiple nodes. ```typescript const client = new TopGunClient({ cluster: { seeds: [ 'ws://node1.example.com:8080', 'ws://node2.example.com:8080', 'ws://node3.example.com:8080', ], smartRouting: true, connectionsPerNode: 2, connectionTimeoutMs: 5000, }, storage: new IDBAdapter(), }); ``` `TopGunClusterConfig` fields: `seeds` (required), `connectionsPerNode` (default 1), `smartRouting` (default true), `partitionMapRefreshMs` (default 30000), `connectionTimeoutMs` (default 5000), `retryAttempts` (default 3). Supplying both `serverUrl` and `cluster` throws at construction. ### Advanced: HTTP-sync and auto-connection providers For environments without WebSockets (Cloudflare Workers, serverless, restrictive proxies), build a custom `SyncEngine` with `HttpSyncProvider` or `AutoConnectionProvider`. These are advanced and bypass the `TopGunClient` wrapper. ```typescript const hlc = new HLC('client-1'); const provider = new HttpSyncProvider({ url: 'https://your-api.example.com', clientId: 'client-1', hlc, authToken: 'your-jwt-token', syncMaps: ['todos'], pollIntervalMs: 5000, }); const engine = new SyncEngine({ nodeId: 'client-1', connectionProvider: provider, storageAdapter: new IDBAdapter(), }); ``` `AutoConnectionProvider` attempts WebSocket first, then falls back to HTTP polling. Same setup pattern. ## Core Methods ### `start()` ```typescript public async start(): Promise ``` Initializes the storage adapter (calls `storage.initialize('topgun_offline_db')`). For local-only mode this is optional — `IDBAdapter` initializes lazily on first read/write. Calling `start()` eagerly is useful when you want to surface storage errors before the UI renders. ### `getMap(name)` ```typescript getMap(name: K): LWWMap; getMap(name: string): LWWMap; ``` Returns an `LWWMap` (last-write-wins CRDT) for the given name. Creates the map on first call and restores its state from storage asynchronously. ```typescript const users = client.getMap('users'); users.set('u1', { name: 'Alice' }); const user = users.get('u1'); ``` ### `getORMap(name)` ```typescript getORMap(name: K): ORMap; getORMap(name: string): ORMap; ``` Returns an `ORMap` (observed-remove CRDT) for multi-value-per-key data. ```typescript const tags = client.getORMap('tags'); tags.add('post:123', 'javascript'); tags.add('post:123', 'typescript'); const postTags = tags.get('post:123'); // ['javascript', 'typescript'] ``` ### `query(mapName, filter)` ```typescript query(mapName: K, filter: QueryFilter): QueryHandle; query(mapName: string, filter: QueryFilter): QueryHandle; ``` Returns a live query subscription. The handle emits an initial result and delta updates. See [Search & live queries](/docs/guides/search-and-live-queries) for the canonical use-case-driven example. ```typescript const handle = client.query('todos', { where: { completed: false }, sort: { createdAt: 'desc' }, limit: 10, }); const unsubscribe = handle.subscribe((results) => { console.log('Results:', results); }); // Cursor pagination const { nextCursor, hasMore } = handle.getPaginationInfo(); if (hasMore && nextCursor) { const nextPage = client.query('todos', { where: { completed: false }, sort: { createdAt: 'desc' }, limit: 10, cursor: nextCursor, }); } ``` `QueryHandle` methods: `subscribe(cb)`, `onDelta(listener)`, `consumeChanges()`, `getLastChange()`, `getPendingChanges()`, `clearChanges()`, `resetChangeTracker()`, `getFilter()`, `getMapName()`, `getPaginationInfo()`, `onPaginationChange(listener)`, `updatePaginationInfo(info)`, `syncState` (getter), `onSyncStateChange(listener)`. ### `topic(name)` ```typescript public topic(name: string): TopicHandle ``` Returns a pub/sub topic for ephemeral fan-out messaging (not CRDT-persisted). See [Live notifications](/docs/guides/live-notifications) for the canonical hook-first pattern. ```typescript const chat = client.topic('chat-room'); chat.publish({ text: 'Hello!' }); const unsubscribe = chat.subscribe((msg) => { console.log(msg); }); ``` ### `getLock(name)` ```typescript public getLock(name: string): DistributedLock ``` Returns a distributed lock handle. See [Counters & locks](/docs/guides/counters-and-locks) for fencing-token semantics and single-node-vs-cluster caveats. ```typescript const lock = client.getLock('resource-A'); if (await lock.lock(10000)) { // Critical section await lock.unlock(); } ``` `DistributedLock` methods: `lock(ttl?: number)`, `unlock()`, `isLocked()`. ### `getPNCounter(name)` ```typescript public getPNCounter(name: string): PNCounterHandle ``` Returns a PN-Counter (positive-negative) for offline-capable increment/decrement. See [Counters & locks](/docs/guides/counters-and-locks) for a canonical example. ```typescript const likes = client.getPNCounter('likes:post-123'); likes.increment(); likes.decrement(); likes.addAndGet(10); likes.subscribe((value) => { console.log('Current likes:', value); }); ``` ### Entry processors — planned (v2.x) Server-side atomic read-modify-write via user-defined functions (`client.executeOnKey`, `client.executeOnKeys`, `BuiltInProcessors`) requires a WASM sandbox on the v2.x roadmap. The SDK surface throws an explanatory error pre-launch — see [/docs/roadmap](/docs/roadmap). ### `close()` ```typescript public async close(): Promise ``` Tears down the client. Awaits cluster reconnect-timer cleanup so the host process does not leak `setTimeout` handles. ## Search and SQL ### `search(mapName, query, options?)` ```typescript public async search( mapName: string, query: string, options?: { limit?: number; minScore?: number; boost?: Record } ): Promise> ``` One-shot BM25 full-text search. Requires FTS enabled for the map on the server. ```typescript const results = await client.search
('articles', 'machine learning', { limit: 20, minScore: 0.5, boost: { title: 2.0, body: 1.0 }, }); ``` ### `searchSubscribe(mapName, query, options?)` ```typescript public searchSubscribe( mapName: string, query: string, options?: SearchOptions ): SearchHandle ``` Live BM25 subscription. The handle receives ENTER/UPDATE/LEAVE deltas as documents change. ```typescript const handle = client.searchSubscribe
('articles', 'machine learning', { limit: 20, minScore: 0.5, }); const unsubscribe = handle.subscribe((results) => setResults(results)); handle.dispose(); ``` ### `sql(query)` ```typescript public async sql(query: string): Promise ``` Execute a SQL query server-side via DataFusion. Map names are table names. Requires the server's DataFusion feature and registered schemas. ```typescript const result = await client.sql('SELECT name, age FROM users WHERE age > 21 ORDER BY age'); // result.columns: ['name', 'age'] // result.rows: [[...], [...]] ``` ### `vectorSearch(mapName, queryVector, options?)` ```typescript public async vectorSearch( mapName: string, queryVector: Float32Array | number[], options?: VectorSearchClientOptions ): Promise ``` ANN search against the HNSW vector index. Query vector serializes as little-endian f32 bytes. ```typescript const results = await client.vectorSearch('notes', new Float32Array([0.1, 0.2, 0.3]), { k: 5 }); ``` ### `hybridSearch(mapName, queryText, options?)` ```typescript public async hybridSearch( mapName: string, queryText: string, options?: HybridSearchClientOptions ): Promise ``` Tri-hybrid search combining exact match, full-text, and semantic vector results via Reciprocal Rank Fusion. ### `hybridSearchSubscribe(mapName, queryText, options?)` ```typescript public hybridSearchSubscribe( mapName: string, queryText: string, options?: HybridSearchSubscribeOptions ): HybridSearchHandle ``` Live tri-hybrid search subscription. ### `hybridQuery(mapName, filter?)` ```typescript public hybridQuery(mapName: string, filter: HybridQueryFilter = {}): HybridQueryHandle ``` Combine FTS predicates with traditional filter predicates in a single query. Results include `_score` for FTS ranking. ```typescript const handle = client.hybridQuery
('articles', { predicate: Predicates.and( Predicates.match('body', 'machine learning'), Predicates.equal('category', 'tech') ), sort: { _score: 'desc' }, limit: 20, }); ``` ## Connection State ### `getConnectionState()` ```typescript public getConnectionState(): SyncState ``` Returns the current state from the connection state machine (e.g., `OFFLINE`, `CONNECTING`, `AUTHENTICATING`, `READY`). ### `onConnectionStateChange(listener)` ```typescript public onConnectionStateChange(listener: (event: StateChangeEvent) => void): () => void ``` Subscribe to state transitions. Returns an unsubscribe function. ```typescript const unsubscribe = client.onConnectionStateChange((event) => { console.log(`State: ${event.from} -> ${event.to}`); }); ``` ### `getStateHistory(limit?)` ```typescript public getStateHistory(limit?: number): StateChangeEvent[] ``` Returns the ring-buffered state-change history (useful for debugging reconnect storms). ### `resetConnection()` ```typescript public resetConnection(): void ``` Reset the connection and state machine. Use after fatal errors to start fresh. ## Backpressure ### `getPendingOpsCount()` ```typescript public getPendingOpsCount(): number ``` Number of unacknowledged operations waiting for server ack. ### `getBackpressureStatus()` ```typescript public getBackpressureStatus(): BackpressureStatus ``` Snapshot: `{ pending, maxPending, strategy, paused, droppedCount }`. ### `isBackpressurePaused()` ```typescript public isBackpressurePaused(): boolean ``` True when the writer is currently paused under the pause strategy. ### `onBackpressure(event, listener)` ```typescript public onBackpressure( event: 'backpressure:high' | 'backpressure:low' | 'backpressure:paused' | 'backpressure:resumed' | 'operation:dropped', listener: (data?: BackpressureThresholdEvent | OperationDroppedEvent) => void ): () => void ``` Subscribe to backpressure transitions. ```typescript client.onBackpressure('backpressure:high', ({ pending, max }) => { console.warn(`Warning: ${pending}/${max} pending ops`); }); client.onBackpressure('backpressure:paused', () => showLoadingSpinner()); client.onBackpressure('backpressure:resumed', () => hideLoadingSpinner()); ``` ## Event Journal ### `getEventJournal()` ```typescript public getEventJournal(): EventJournalReader ``` Returns the journal reader for subscribing to and replaying map-change events (audit trail, undo history, change-feed consumers). ```typescript const journal = client.getEventJournal(); const unsubscribe = journal.subscribe((event) => { console.log(`${event.type} on ${event.mapName}:${event.key}`); }); // Filtered subscription journal.subscribe( (event) => console.log('User changed:', event.key), { mapName: 'users' } ); // Historical replay const events = await journal.readFrom(0n, 100); ``` `EventJournalReader` methods: `readFrom(sequence, limit)`, `readMapEvents(...)`, `getLatestSequence()`, `subscribe(listener, options?)`. ## Conflict Resolvers ### `getConflictResolvers()` ```typescript public getConflictResolvers(): ConflictResolverClient ``` Observe merge rejections (the built-in CRDT merge logic rejected a remote change). The returned client's `register` / `unregister` / `list` methods throw — registering custom server-side resolvers requires a WASM sandbox on the v2.x roadmap, see [/docs/roadmap](/docs/roadmap). ```typescript const resolvers = client.getConflictResolvers(); resolvers.onRejection((rejection) => { console.log(`Merge rejected: ${rejection.reason}`); }); ``` ## Per-Record Sync State ### `getRecordSyncStateTracker()` ```typescript public getRecordSyncStateTracker(): RecordSyncStateTracker ``` The tracker projects op-log mutations, connection state, and merge rejections into a four-state tag per `(mapName, key)`. Used by React hooks (`useSyncState`, the `*WithSyncState` companions) and by advanced consumers reading sync state outside a query context. ## Cluster Mode API ### `isCluster()` ```typescript public isCluster(): boolean ``` True when the client was constructed with `cluster` config. ### `getConnectedNodes()` ```typescript public getConnectedNodes(): string[] ``` List of currently connected node IDs (cluster mode); empty array otherwise. ### `getPartitionMapVersion()` ```typescript public getPartitionMapVersion(): number ``` Current partition-map version; 0 in single-server mode. ### `isRoutingActive()` ```typescript public isRoutingActive(): boolean ``` True when direct routing to partition owners is active. ### `getClusterHealth()` ```typescript public getClusterHealth(): Map ``` Per-node health map. ### `refreshPartitionMap()` ```typescript public async refreshPartitionMap(): Promise ``` Force a partition-map refresh. Useful after detecting routing errors. ### `getClusterStats()` ```typescript public getClusterStats(): { mapVersion: number; partitionCount: number; nodeCount: number; lastRefresh: number; isStale: boolean } | null ``` Router statistics snapshot. ## Authentication ### `setAuthToken(token)` ```typescript public setAuthToken(token: string): void ``` Set a static JWT for the next sync handshake. ```typescript client.setAuthToken('jwt-token-here'); ``` ### `setAuthTokenProvider(provider)` ```typescript public setAuthTokenProvider(provider: () => Promise): void ``` Provider is called on each reconnection — refresh tokens land here. ```typescript client.setAuthTokenProvider(async () => { const token = await refreshToken(); return token; }); ``` For Clerk/Firebase/BetterAuth/Custom, prefer the `auth` constructor option with a pluggable `AuthProvider` instead of calling these methods directly. --- [← Reference Overview](/docs/reference) · [Core →](/docs/reference/core) --- # React > Reference for @topgunbuild/react — hooks for building reactive TopGun apps.

React

`@topgunbuild/react` ships hooks that wrap the imperative `TopGunClient` API. Every hook below resolves the client through `` — that provider is the one required ancestor. For the imperative surface these hooks call into, see the [Client Reference](/docs/reference/client). ## Setup ### `TopGunProvider` Required React context provider. Wrap your app once at the root. ```tsx const client = new TopGunClient({ storage: new IDBAdapter(), }); export function App() { return ( ); } ``` ### `useClient` Escape hatch returning the underlying `TopGunClient`. Use this for imperative operations (calling `client.getEventJournal`, `client.topic`, etc.) inside event handlers and effects. ```tsx function ResetButton() { const client = useClient(); return ; } ``` Throws if used outside ``. ## Reactive Data Hooks ### `useQuery` Live query subscription. Returns `data`, `loading`, `error`, change-tracking fields, pagination info, and per-record sync state. See [Search & live queries](/docs/guides/search-and-live-queries) for canonical examples. ```tsx interface Todo { text: string; done: boolean } function TodoList() { const { data, loading, error } = useQuery('todos', { where: { done: false }, sort: { createdAt: 'desc' }, limit: 10, }); if (loading) return

Loading…

; if (error) return

Error: {error.message}

; return (
    {data.map(item =>
  • {item.text}
  • )}
); } ``` Signature: ```typescript function useQuery( mapName: string, query?: QueryFilter, options?: UseQueryOptions ): UseQueryResult ``` `UseQueryOptions` (all optional): | Field | Description | |---|---| | `onChange` | Called for any change event with the `ChangeEvent`. | | `onAdd` | Called when an item is added: `(key, value)`. | | `onUpdate` | Called when an item is updated: `(key, value, previous)`. | | `onRemove` | Called when an item is removed: `(key, previous)`. | | `maxChanges` | Cap on accumulated `changes[]`. Default 1000. | `UseQueryResult` fields: | Field | Description | |---|---| | `data` | `QueryResultItem[]` — current rows; each item carries `_key`. | | `loading` | True until first result arrives. | | `error` | `Error \| null`. | | `lastChange` | Most recent `ChangeEvent` (or `null`). | | `changes` | All `ChangeEvent` since the last `clearChanges()`. | | `clearChanges` | Resets `changes` and `lastChange`. | | `nextCursor` / `hasMore` / `cursorStatus` | Pagination state. | | `syncState` | `ReadonlyMap` keyed by `_key`. | Pagination example: ```tsx const [cursor, setCursor] = useState(); const { data, nextCursor, hasMore } = useQuery('todos', { sort: { createdAt: 'desc' }, limit: 20, cursor, }); // {hasMore && } ``` ### `useMutation` Imperative write helper. Returns `{ create, update, remove, map }`. ```tsx function AddTodo() { const { create } = useMutation('todos'); return ( ); } ``` Signature: ```typescript function useMutation(mapName: string): UseMutationResult ``` `UseMutationResult`: | Field | Type | Description | |---|---|---| | `create` | `(key: K, value: T) => void` | Write a new entry. | | `update` | `(key: K, value: T) => void` | Overwrite an entry. (Same semantics as `create` under LWW.) | | `remove` | `(key: K) => void` | Delete (tombstone) an entry. | | `map` | `LWWMap` | Underlying map instance for advanced consumers. | ### `useMap` Lower-level escape hatch returning the raw `LWWMap` instance for the named map. Useful when you need direct access to `merge`, `prune`, or `subscribe` — but for most UI code, `useQuery`/`useMutation` is the right choice. ```typescript function useMap(mapName: string): LWWMap ``` A companion `useMapWithSyncState` variant exposes per-record sync state alongside the map. ### `useORMap` Same shape as `useMap` but returns the `ORMap` for multi-value-per-key data. ```typescript function useORMap(mapName: string): ORMap ``` Companion: `useORMapWithSyncState`. ## Sync State ### `useSyncState` Returns the current `RecordSyncState` for a `(mapName, key)` pair. The state is one of `synced`, `pending`, `conflict`, or `offline`. Re-renders only when *that* key's state changes. See [Offline-first apps](/docs/guides/offline-first) for the canonical reconnect-and-merge pattern. ```tsx function SyncBadge({ id }: { id: string }) { const state = useSyncState('todos', id); return {state}; } ``` ```typescript function useSyncState(mapName: string, key: string): RecordSyncState ``` ## Pub/Sub ### `useTopic` Subscribe to (and publish on) an ephemeral pub/sub topic. See [Live notifications](/docs/guides/live-notifications) for canonical examples. ```tsx function ChatLog() { const [messages, setMessages] = useState([]); const topic = useTopic('chat-room', (msg) => { setMessages(prev => [...prev, msg as string]); }); return ( <>
    {messages.map((m, i) =>
  • {m}
  • )}
); } ``` ```typescript function useTopic(topicName: string, callback?: TopicCallback): TopicHandle ``` ## Counters ### `usePNCounter` PN-Counter handle with reactive value. ```tsx function LikeButton() { const { value, increment } = usePNCounter('likes:post-123'); return ; } ``` ```typescript function usePNCounter(name: string): UsePNCounterResult ``` `UsePNCounterResult` exposes `value` plus `increment`, `decrement`, and `addAndGet` callbacks. ## Server-Side Logic ### `useEventJournal` Subscribe to the journal of map-change events. Useful for audit trails, activity feeds, undo stacks. ```typescript function useEventJournal( options?: { mapName?: string; types?: JournalEventType[]; limit?: number } ): { events: JournalEvent[]; loading: boolean; error: Error | null } ``` ## Conflict Resolvers ### `useMergeRejections` Surface merge rejections (the built-in CRDT merge logic rejected a remote change). Useful for telling the user "your edit conflicted". ```typescript function useMergeRejections( options?: { mapName?: string; key?: string } ): MergeRejection[] ``` > Custom user-defined resolvers (`useConflictResolver`, `client.executeOnKey`, server-side entry processors) require a WASM sandbox on the v2.x roadmap. See [/docs/roadmap](/docs/roadmap). The SDK surface for those features was removed pre-launch to avoid runtime errors. ## Search ### `useSearch` Live BM25 full-text search. Re-runs when `query` changes. ```tsx function ArticleSearch() { const [q, setQ] = useState(''); const { results, loading } = useSearch
('articles', q, { limit: 20, minScore: 0.5, }); return ( <> setQ(e.target.value)} /> {loading ?

: results.map(r => (
  • {r.value.title} ({r.score.toFixed(2)})
  • ))} ); } ``` ```typescript function useSearch( mapName: string, query: string, options?: SearchOptions, ): { results: SearchResult[]; loading: boolean; error: Error | null } ``` ### `useHybridQuery` Combine FTS predicates with traditional filter predicates (`where`/`sort`/`limit`/`cursor`) in one query. Results include `_score` for FTS ranking. ```tsx const { data } = useHybridQuery
    ('articles', { predicate: Predicates.and( Predicates.match('body', 'machine learning'), Predicates.equal('category', 'tech') ), sort: { _score: 'desc' }, limit: 20, }); ``` ```typescript function useHybridQuery( mapName: string, filter?: HybridQueryFilter, ): { data: HybridResultItem[]; loading: boolean; error: Error | null; nextCursor?: string; hasMore: boolean } ``` ### `useVectorSearch` ANN vector search via the HNSW index. ```typescript function useVectorSearch( mapName: string, queryVector: Float32Array | number[] | null, options?: VectorSearchClientOptions, ): { results: VectorSearchClientResult[]; loading: boolean; error: Error | null } ``` Passing `null` as the vector pauses the search (useful while the user is still typing). ### `useHybridSearch` Tri-hybrid search (exact + full-text + semantic, fused via Reciprocal Rank Fusion). ```typescript function useHybridSearch( mapName: string, queryText: string, options?: HybridSearchClientOptions, ): { results: HybridSearchClientResult[]; loading: boolean; error: Error | null } ``` ### `useHybridSearchSubscribe` Live tri-hybrid search subscription with ENTER/UPDATE/LEAVE deltas. Pair with optimistic UI for animated results. ```typescript function useHybridSearchSubscribe( mapName: string, queryText: string, options?: HybridSearchSubscribeOptions, ): { results: HybridSearchHandleResult[]; loading: boolean; error: Error | null } ``` --- [← Core](/docs/reference/core) · [Adapters →](/docs/reference/adapters) --- # Core > Reference for @topgunbuild/core — LWWMap, ORMap, HLC, and MerkleTree.

    Core

    `@topgunbuild/core` ships the CRDT primitives, Hybrid Logical Clock, and Merkle tree that the client and server share. This page documents the public surface (the "what / how"). For background on why TopGun picks these primitives over alternatives, see [Concepts → Data Structures](/docs/concepts/data-structures). The two primary data structures are `LWWMap` (last-write-wins) and `ORMap` (observed-remove). Both are exposed on `TopGunClient` via `getMap()` and `getORMap()` — most application code never instantiates them directly. ## LWWMap ```typescript const hlc = new HLC('node-1'); const map = new LWWMap(hlc); ``` Last-write-wins semantics: each key holds exactly one value. Concurrent writes from different nodes are resolved by HLC timestamp (highest wins, ties broken by `nodeId`). ### Constructor ```typescript new LWWMap(hlc: HLC) ``` ### Methods **`set(key, value, ttlMs?)`** — `LWWRecord` Write a value. Stamps the record with `hlc.now()`. Optional `ttlMs` for time-bounded entries. ```typescript const record = map.set('u1', { name: 'Alice', email: 'alice@example.com' }); // { value: {...}, timestamp: { millis, counter, nodeId } } // With 1-hour TTL map.set('session-123', sessionData, 3600000); ``` **`get(key)`** — `V | undefined` Read a value. Returns `undefined` for deleted or expired keys. **`getRecord(key)`** — `LWWRecord | undefined` Read the full record (value, timestamp, optional TTL). **`remove(key)`** — `LWWRecord` Delete a key by writing a tombstone (`value: null`). Returns the tombstone record. **`merge(key, remoteRecord)`** — `boolean` Merge a remote record. Returns `true` if local state was updated (i.e., the remote record's timestamp dominated). ```typescript const remote: LWWRecord = { value: { name: 'Bob' }, timestamp: { millis: Date.now(), counter: 0, nodeId: 'node-2' }, }; const updated = map.merge('u1', remote); ``` **`subscribe(callback)`** — `() => void` Subscribe to map-change notifications. The callback receives `entries: Array<[K, V]>`. Returns an unsubscribe function. ```typescript const unsubscribe = map.subscribe((entries) => { console.log('Map changed,', entries.length, 'live entries'); }); ``` **`entries()`** — `IterableIterator<[K, V]>` Iterate non-tombstoned entries (drops deleted and expired). **`allKeys()`** — `IterableIterator` Iterate every key including tombstones. **`size`** (getter) — `number` Count of entries including tombstones. **`prune(olderThan)`** — `K[]` Drop tombstones older than the given `Timestamp`. Returns the removed keys. **`clear()`** — `void` Reset to empty state. **`getMerkleTree()`** — `MerkleTree` Access the underlying Merkle tree for delta-sync diffing. ### LWWRecord shape ```typescript interface LWWRecord { value: V | null; // null indicates tombstone timestamp: Timestamp; // HLC stamp ttlMs?: number; // optional TTL } ``` ## ORMap ```typescript const hlc = new HLC('node-1'); const map = new ORMap(hlc); ``` Observed-remove semantics: each key holds a *set* of values. Concurrent adds preserve every value (tagged with unique IDs). A remove operation tombstones only the tags it observed locally, so concurrent add-vs-remove resolves correctly (no spurious deletes). Use this for tag clouds, comment threads, multi-select fields, presence rosters — anywhere two clients can legitimately contribute different values to the same key. ### Constructor ```typescript new ORMap(hlc: HLC) ``` ### Methods **`add(key, value, ttlMs?)`** — `ORMapRecord` Add a value under a key. Generates a unique tag. Returns the record. ```typescript const tags = new ORMap(hlc); tags.add('post:123', 'javascript'); tags.add('post:123', 'typescript'); // post:123 -> ['javascript', 'typescript'] ``` **`remove(key, value)`** — `string[]` Remove a specific value under a key. Returns the tombstone tags created. **`get(key)`** — `V[]` Read all live values for a key. ```typescript const postTags = tags.get('post:123'); // ['javascript', 'typescript'] ``` **`getRecords(key)`** — `ORMapRecord[]` Read full records (value + tag + timestamp) for a key. **`getTombstones()`** — `string[]` Read all current tombstone tags. **`apply(key, record)`** — `boolean` Apply a remote `ORMapRecord` to local state. Returns `true` if state changed. **`applyTombstone(tag)`** — `void` Apply a remote tombstone tag. **`merge(other)`** — `void` Merge another `ORMap` of the same key/value types in bulk. **`subscribe(callback)`** — `() => void` Subscribe to change events. The callback receives `entries: Array<[K, V[]]>`. **`allKeys()`** — `K[]` All keys with at least one live or tombstoned value. **`size`** / **`totalRecords`** (getters) — `number` `size` counts distinct keys; `totalRecords` counts every tagged record (including duplicates per key). **`prune(olderThan)`** — `string[]` Drop tombstones older than the timestamp. Returns dropped tags. **`clear()`** — `void` Reset to empty. **`getMerkleTree()`** — `ORMapMerkleTree` Underlying Merkle tree (a different shape than LWWMap's tree because ORMap stores multi-value-per-key). **`getSnapshot()`** — `ORMapSnapshot` Serializable snapshot for persistence. **`isTombstoned(tag)`** — `boolean` Check whether a tag is in the tombstone set. ### ORMapRecord shape ```typescript interface ORMapRecord { value: V; tag: string; // unique add ID timestamp: Timestamp; // HLC stamp ttlMs?: number; } ``` ## HLC (Hybrid Logical Clock) ```typescript const hlc = new HLC('node-1'); const ts = hlc.now(); // { millis: 1748025600000, counter: 0, nodeId: 'node-1' } ``` The HLC combines physical wall-clock time (`millis`) with a logical counter (`counter`) so total ordering is preserved across nodes even when clocks drift. ### Constructor ```typescript new HLC(nodeId: string, options?: HLCOptions) ``` `HLCOptions` fields: | Field | Default | Description | |---|---|---| | `strictMode` | `false` | Throw on clock drift exceeding `maxDriftMs`. | | `maxDriftMs` | `60000` | Permitted drift threshold (ms). | | `clockSource` | wall clock | Custom `ClockSource` for deterministic tests. | ### Methods **`now()`** — `Timestamp` Issue a fresh timestamp. Monotonic per node. **`update(remote)`** — `void` Update the local clock against a received remote timestamp. Bumps `lastMillis`/`lastCounter` as needed to maintain causal ordering. **`getClockSource()`** — `ClockSource` Returns the configured clock source. **`getNodeId`** / **`getStrictMode`** / **`getMaxDriftMs`** (getters) Inspect configured fields. ### Static helpers **`HLC.compare(a, b)`** — `number` Total-order comparator. Returns negative if `a < b`, zero if equal, positive if `a > b`. Ordered by `millis`, then `counter`, then `nodeId`. **`HLC.toString(ts)`** — `string` Canonical string encoding for serialization or sort keys. **`HLC.parse(str)`** — `Timestamp` Inverse of `toString`. ### Timestamp shape ```typescript interface Timestamp { millis: number; counter: number; nodeId: string; } ``` ## MerkleTree ```typescript const tree = new MerkleTree(); ``` The Merkle tree powers delta sync: clients and servers compare hash trees and exchange only differing buckets instead of full state. `LWWMap` and `ORMap` maintain their own trees internally — use this directly only when implementing custom sync providers or diagnostics. ### Constructor ```typescript new MerkleTree(records?: Map>, depth = 3) ``` `depth` controls fan-out (default 3 levels deep). ### Methods **`update(key, record)`** — `void` Insert or replace the leaf for `key` and rehash the path to the root. **`remove(key)`** — `void` Drop the leaf and rehash. **`getRootHash()`** — `number` Top-level hash. Two trees with identical content produce the same root. **`getNode(path)`** — `MerkleNode | undefined` Return the node at a given path string (used during diff walks). **`getBuckets(path)`** — `Record` Per-bucket hashes for the children of the node at `path`. **`getKeysInBucket(path)`** — `string[]` Leaf keys grouped under the given path. ## Re-exports `@topgunbuild/core` also re-exports types and helpers consumed across the stack. Highlights: - **`Predicates`** — composable filter builders (`equal`, `gt`, `lt`, `contains`, `and`, `or`, `not`, `match`, `matchPhrase`, `matchPrefix`). Used by `client.query`, `client.hybridQuery`, and the React hooks. - **`SearchOptions`** — shared FTS options shape used by `client.search`/`searchSubscribe` and `useSearch`. - **`MergeRejection`** — event shape emitted when the built-in CRDT merge logic rejects a remote change. Observed via `useMergeRejections` (React) or `client.getConflictResolvers().onRejection()`. - **`WriteConcern`** / **`ConsistencyLevel`** — durability and consistency knobs for cluster writes. - **`IndexedLWWMap`** / **`IndexedORMap`** — indexed variants used by the server's query engine. - **Full-Text Search** — `Tokenizer`, `InvertedIndex`, `BM25Scorer`, `FullTextIndex` for embedding FTS directly. - **Deterministic Simulation Testing** — `VirtualClock`, `SeededRNG`, `VirtualNetwork`, `InvariantChecker`, `ScenarioRunner` for property-based distributed tests. The complete re-export list lives in `packages/core/src/index.ts`. --- [← Client](/docs/reference/client) · [React →](/docs/reference/react) --- # Comparison > How TopGun compares to other solutions in the ecosystem. Docs Get Started Comparison # Comparison How TopGun compares to other solutions in the ecosystem. {/* TopGun claims sourced from packages/core/src/, packages/client/src/, packages/server-rust/src/ — verified 2026-05-21 */} {/* Firebase claims sourced from https://firebase.google.com/docs/firestore — verified 2026-05-21 */} {/* Supabase Realtime claims sourced from https://supabase.com/docs/guides/realtime — verified 2026-05-21 */} {/* Yjs claims sourced from https://github.com/yjs/yjs — verified 2026-05-21 */} {/* RxDB claims sourced from https://rxdb.info/replication.html — verified 2026-05-21 */} {/* Replicache claims sourced from https://doc.replicache.dev/ — verified 2026-05-21 */} {/* InstantDB claims sourced from https://www.instantdb.com/docs and https://github.com/instantdb/instant — verified 2026-05-21 */}
    Feature TopGun Firebase Supabase Realtime Yjs RxDB Replicache InstantDB
    Primary Model Local-First CRDT grid Cloud Doc DB Postgres pub/sub Collaborative CRDT framework Local-First DB Client-auth sync Local-first graph DB
    Offline Support First-Class Good Limited Excellent Excellent Good First-Class
    Latency ~0ms (in-memory) Network-dependent Network-dependent ~0ms (in-memory) ~5–10ms (IndexedDB) ~0ms local / server-validated ~0ms (in-memory)
    Backend Control Self-host (Apache-2.0) Proprietary Self-host or Supabase Cloud BYO transport / provider CouchDB / Custom Self-host or Replicache Cloud InstantDB Cloud (self-host community)
    Consistency HLC + CRDT Server-authoritative LWW Server Authority CRDT (Y-types) Revision Trees Server Authority Server-authoritative with optimistic UI
    Distributed Locks Fencing Tokens (single-node stable; cluster Raft: in progress) Not Supported Not Supported Not Supported Not Supported Not Supported Not Supported
    License Apache-2.0 Proprietary Apache-2.0 MIT Apache-2.0 / Proprietary (Premium) Apache-2.0 MIT (client) + proprietary cloud
    View Roadmap: distributed primitives maturity levels ## Why TopGun?
    • vs Firebase:

      TopGun gives you ownership of your data. Run it on your own cloud, use your own backend, and never get locked into a proprietary platform.

    • vs Supabase Realtime:

      Supabase Realtime is Postgres pub/sub — great for server-driven events, but not designed for offline-first local writes. TopGun persists data locally in IndexedDB and syncs seamlessly when the network returns.

    • vs Yjs:

      Yjs is an excellent CRDT framework for collaborative text editing but requires you to bring your own transport, persistence, and server. TopGun ships with a full server, IndexedDB persistence, and a React SDK out of the box.

    • vs RxDB:

      RxDB is a mature local-first database with good offline support, but its writes go through IndexedDB (5–10ms). TopGun operates entirely in-memory for zero-latency reads/writes, with async write-behind to the configured backend.

    • vs Replicache:

      TopGun uses CRDTs for automatic conflict resolution — mutations apply locally first and merge deterministically. Replicache is server-authoritative: mutations queue and the server validates them. Related: Zero (also from Rocicorp) offers a similar server-authoritative model with a managed cloud option. Both Replicache and TopGun are Apache-2.0 open source.

    • vs InstantDB:

      InstantDB is a local-first graph database with an excellent developer experience. It currently runs on InstantDB Cloud (community-led self-hosting available via the open-source repo). TopGun is fully self-hostable under Apache-2.0 with no cloud dependency.

    Previous Introduction Next Section Installation --- # For Coding Agents > TopGun cheatsheet for LLM coding assistants — schema-typed minimal code, structured headings, optimized for context-window ingestion. ## What TopGun is Local-first reactive data layer; CRDT-merged writes; WebSocket sync to a Rust server (single-node stable). ## Install ```bash pnpm add @topgunbuild/client @topgunbuild/react @topgunbuild/adapters ``` ## Canonical app shape (React) ```tsx // main.tsx const client = new TopGunClient({ serverUrl: 'ws://localhost:8080', storage: new IDBAdapter(), }); client.start(); // non-await: lazy init, do NOT await function App() { return ( ); } ``` ## Canonical writes ```tsx // useMutation('mapName') returns { create, update, remove } // record._key is the unique identifier on every record const { create, update, remove } = useMutation('todos'); create(crypto.randomUUID(), { title: 'Buy milk', done: false }); update(existingKey, { done: true }); remove(existingKey); ``` ## Canonical queries ```tsx // useQuery('mapName') returns { data: T[] } const { data: todos } = useQuery('todos'); // With predicates: const { data: open } = useQuery('todos', { predicate: Predicates.equal('done', false), }); // Also: Predicates.isIn(), Predicates.greaterThan() ``` ## Local-only mode ```tsx // Omit serverUrl to run fully local — no server needed const client = new TopGunClient({ storage: new IDBAdapter(), }); ``` ## Add real-time sync ```bash pnpm start:server # embedded redb backend, zero-config ``` ```tsx const client = new TopGunClient({ serverUrl: 'ws://localhost:8080', storage: new IDBAdapter(), }); ``` ## Topic-based pub/sub ```tsx // useTopic returns a TopicHandle. Pass a callback to receive messages; // publish via the returned handle. const [messages, setMessages] = useState>([]); const topic = useTopic<{ text: string }>('chat', (msg) => setMessages((prev) => [...prev, msg]), ); topic.publish({ text: 'hello' }); ``` ## Auth ```tsx // client.setAuthToken(jwt) after sign-in // TopGun does NOT issue JWTs — use your own auth provider const jwt = await myAuthServer.signIn(email, password); client.setAuthToken(jwt); ``` ## Common pitfalls - Do NOT `await client.start()` — lazy init; calling `await` is incorrect - Use `record._key` (not `record.id`) to identify records in mutation callbacks - `useMutation` requires the generic param `` for type-safety: `useMutation('todos')` - IndexedDB only persists when `IDBAdapter` is wired in `new TopGunClient({ storage: new IDBAdapter() })` ## Cross-reference Full bundle: `/llms-full.txt`. Source: `https://github.com/TopGunBuild/topgun`.