This is the three-tier architecture as a real app you can run —
the worked, complete version of the synced-app quickstart.
It lives in the repo at apps/example-issue-tracker and boots all three tiers — the
Rust data tier (a rindle-replicator write-master + its rindled follower, the
colocated pair), the API server, and the Vite/React
client — with a single command.
The quickstart builds a true subset of this app by hand, file by file; this page is the finished thing — run it, click it, and read it.
Live demo: it’s hosted at issues.rindle.sh — a shared sandbox that resets daily. Or run the whole stack locally in one command with
pnpm dev(below).
Same schema, at scale
It’s the same normalized schema as the quickstart — user · issue · tag ·
comment, joined back together at query time — wired through the same
createRindleClient (browser), registerQueries + sharedApiMutators (API server),
and the colocated pair (write-master + follower). Two things a toy dataset can’t show
you, this app does:
A growing live window over a big table. The write-master is seeded with 5,000
issues at boot, so the client never asks for everything — it holds a growing live window, and
infinite scroll just ratchets limit up a page at a time (one named subscription, not
a stack of cursor windows). IVM keeps the window correct: a new issue enters the top
incrementally (the last row falls out as it does), and the window backfills after a
delete.
Idempotent bulk seeding through the write-master. server/seed.ts inserts the
5,000-row corpus as one idempotency-keyed bulk transaction on the write-master, so
it never re-seeds on restart:
await daemon.executeSqlTxn({
idempotencyKey: `seed-issues-v3-${count}`,
statements: seedStatements(count), // ≤100 rows/statement (SQLite's 999-param cap)
});
Two rejection paths, live
The mutators are isomorphic — one shared body per
name, auto-driven on the server by sharedApiMutators (the code is
the quickstart’s step 5). The only explicit server
entries are the authority the client must not predict — and both rejection behaviors
are wired through them so you can watch them in two browser windows, each window
picking its own user via the x-user header:
- Hard reject — creating an issue whose title contains “spam” is refused (a server-only policy guard wrapped around the shared body); the client shows a rejection toast and the optimistic row snaps back.
- Accepted-but-no-op — deleting an issue you don’t own changes nothing (an
owner-gated cascade is relational authority a keyed op can’t express, so it stays
raw SQL:
… WHERE id = ? AND ownerId = ?); the optimistic delete snaps back when the empty authoritative change syncs in.
Run it
# once, from the repo root — install and build the artifacts the app needs:
pnpm install
pnpm run build:wasm # the browser engine (packages/wasm/pkg)
cargo build -p rindle-server -p rindle-replicator -p rindle-cli -p rindle-dev-edge # the local fleet binaries
cd apps/example-issue-tracker
pnpm dev # boots the colocated pair (via `rindle up`), seeds 5,000 issues, then Vite
Open the printed URL in two browser windows and watch edits sync live across them. For a headless, CI-able proof of the whole wiring:
pnpm smoke
Where to look
| Concern | File |
|---|---|
| schema + relationships + isomorphic mutators (the shared contract) | shared/app-def.ts |
co-located named queries (defineQuery) + fragments |
src/components/*.queries.ts |
the one-call client wire-up (createRindleClient) |
src/rindle-client.ts |
window accumulation + fragment-rooted React binding (useRoot) over copy-on-write views |
src/AppChrome.tsx |
the sharedApiMutators spread + server-only authority overrides + registerQueries + policy + auth |
server/app-api.ts |
| idempotent bulk seeding through the write-master | server/seed.ts |
| the colocated pair (write-master + follower) boot + 3-tier wiring | package.json — the dev scripts (rindle up + concurrently); entrypoint.sh for the deployed ROLE=colocated box |
Next steps
- Synced-app quickstart — build a subset of this app by hand, every file and command.
- The three-tier architecture — the topology this realizes.
- The browser client · The API server — the runtime tiers you write and wire.
@rindle/cli— the local pair workflow used by this app.- Server rendering — the first-paint preload pattern used by TanStack Start apps.
- Devtools — the dev-only mutation timeline, query inspector, and delta stream.
- The change model — the normalized deltas the daemon streams.