# Full app: the issue tracker

A real issue tracker on all three tiers — rindled daemon, API server, optimistic React client — with paginated live windows over 5,000 issues and two live rejection paths. One command.

This is the [three-tier architecture](/docs/architecture) as a real app you can run —
the worked, complete version of the [synced-app quickstart](/docs/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](/docs/deploy#1-colocated-pair)), the API server, and the Vite/React
client — with a single command.

The [quickstart](/docs/synced-app-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](https://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:

```ts
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](/docs/client#isomorphic-mutators) — one shared body per
name, auto-driven on the server by `sharedApiMutators` (the code is
[the quickstart's step 5](/docs/synced-app-quickstart)). 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

```bash
# 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:

```bash
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](/docs/synced-app-quickstart) — build a subset of this app by
  hand, every file and command.
- [The three-tier architecture](/docs/architecture) — the topology this realizes.
- [The browser client](/docs/client) · [The API server](/docs/api-server) — the
  runtime tiers you write and wire.
- [`@rindle/cli`](/docs/rindle-cli) — the local pair workflow used by this app.
- [Server rendering](/docs/ssr) — the first-paint preload pattern used by
  TanStack Start apps.
- [Devtools](/docs/devtools) — the dev-only mutation timeline, query inspector,
  and delta stream.
- [The change model](/docs/change-model) — the normalized deltas the daemon streams.

---

[View this page on Rindle](https://rindle.sh/docs/example-issue-tracker)
