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. Two paths:
Using the CLI (recommended — works from any directory once the server binary is present):
# Install the unified TopGun developer CLI
npm install -g @topgunbuild/cli
# Check your environment
topgun doctor
# Interactive project setup — writes .env with zero-config defaults
# (TOPGUN_NO_AUTH=1 for local dev, STORAGE_BACKEND=redb for embedded storage)
topgun setup
# Start the development server
topgun dev
The setup→dev flow is cohesive: topgun setup writes a .env that topgun dev reads to start the server without any JWT or database configuration. Run topgun setup --yes to skip prompts and accept all defaults.
Zero-install path (no Rust toolchain required):
npx @topgunbuild/server
This downloads a prebuilt binary for your platform and starts the server immediately. No compiler, no Docker.
Inside the TopGun monorepo (contributor path):
pnpm start:server
pnpm start:server uses a prebuilt binary when present and falls back to cargo run --bin topgun-server --release for contributors who have the Rust toolchain.
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.
7. Explore your data visually
The admin dashboard gives you a live visual map of your data — Maps, Explorer, Cluster topology, Query Playground, and Prometheus metrics — all connected to your local server over WebSocket.
Zero-install path (no monorepo, no Rust toolchain required):
npx @topgunbuild/server
The prebuilt server ships with the admin dashboard bundled in. Open http://localhost:8080/admin/ in your browser — the SPA is served directly by the server, no extra process or build step. When the server is running in zero-config mode (TOPGUN_NO_AUTH=1, the npx default), the dashboard opens straight to the Dashboard screen — no login credentials required.
Inside the TopGun monorepo (contributor path with live reload):
topgun dev --admin
This launches the Vite dev server alongside the Rust server for hot-reload while you work on the dashboard. Open http://localhost:5173/admin/.
Custom build? Point
TOPGUN_ADMIN_DIRat your own admin build directory to override the bundled SPA. Leave it unset to serve the bundled dashboard (npm) or the monorepo build (./admin-dashboard/dist).
Other ways to reach the admin:
- Hosted demo: https://demo.topgun.build — connects to a shared demo server.
- Docker Compose:
docker compose --profile admin up— starts the server and the admin dashboard at http://localhost:3001.