DocsGet StartedQuickstart

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.

1. Installation

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

terminal
npm install @topgunbuild/client @topgunbuild/adapters @topgunbuild/react zod

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.

src/app.tsx
import { useQuery, useMutation, TopGunProvider } from '@topgunbuild/react';
import { TopGunClient } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';
import { type Todo } from './schema';

export const client = new TopGunClient({
  storage: new IDBAdapter(),
  // Uncomment to connect to a server:
  // serverUrl: 'ws://localhost:8080',
});

function TodoApp() {
  const { data: todos = [] } = useQuery<Todo>('todos');
  const { create } = useMutation<Todo>('todos');
  const add = () => create(`todo-${Date.now()}`, { text: 'Ship v2', done: false });
  return (
    <>
      <button onClick={add}>Add</button>
      <ul>{todos.map(t => <li key={t._key}>{t.text}</li>)}</ul>
    </>
  );
}

export default () => <TopGunProvider client={client}><TodoApp /></TopGunProvider>;
src/schema.ts
import { z } from 'zod';
export const TodoSchema = z.object({ text: z.string(), done: z.boolean() });
export type Todo = z.infer<typeof TodoSchema>;

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<Todo>('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<Todo>('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:

// schema.ts
export const TodoSchema = z.object({ text: z.string(), done: z.boolean() });
export type Todo = z.infer<typeof TodoSchema>;
// app.tsx
import { type Todo } from './schema';
const { data: todos = [] } = useQuery<Todo>('todos');

z.infer<typeof TodoSchema> produces { text: string; done: boolean }. Pass that as the generic to useQuery<Todo> 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.

example.ts
const todos = client.getMap('todos');
todos.set(`todo-${Date.now()}`, { text: 'Ship v2', done: false });

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:
pnpm start:server

The server boots in seconds with embedded storage (./topgun.redb) — no Postgres, no Docker required.

  1. 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 for TLS configuration.