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.
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.
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>; 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.
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’sIDBAdapter 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 usingIDBAdapter, 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 withEncryptedStorageAdapter 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:
- Uncomment the
serverUrlline in the client init above. - 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.
- 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.