# __PROJECT_NAME__

A SQL-first [Rindle](https://github.com/rindle-sh/rindle) app on [TanStack Start](https://tanstack.com/start),
generated by `create-rindle`. It's a tiny forum-of-rooms: each room shows a **live message count**
that the in-process incremental-view engine keeps exact on every write — no polling.

## The shape

Three tiers, same as the Rindle flagship examples:

- **Browser** — a TanStack Start SPA whose data layer *is* Rindle. The wasm IVM engine runs
  in-process: reads resolve locally and instantly, writes apply optimistically and reconcile on
  confirmation. `@rindle/tanstack` binds route preloading, client navigation readiness, and the
  SSR-to-live provider handoff. Views are co-located Relay-style fragments
  (`src/components/*.queries.ts`).
- **API authority** (`server/app-api.ts`) — resolves named queries to ASTs, drives the **same
  isomorphic mutators** the browser predicted (their logical ops rendered to SQL), and enforces
  policy: reads are public, writes require an identity, and a `"spam"` name/body
  is rejected (you'll see the optimistic write snap back + a toast). It's host-agnostic: the browser
  reaches it through TanStack Start **server routes** (`src/routes/api.rindle.{query,read,mutate}.tsx`,
  via `server/rindle-http.ts`) that run in the same server as the app, and SSR calls the very same
  factory **in-process** (no network hop).
- **Data tier** — the one topology (design 214): a `rindle-replicator` **write-master** plus a
  `rindled` **read-follower** that owns the live IVM and streams normalized deltas to subscribers.
  Writes land on the master; the follower serves reads. `followers = 1` is the *colocated pair* —
  both processes on one box, the smallest shape.

## SQL is the source of truth

The schema lives in `migrations/*.sql`. The `@rindle/client` table schema is **generated** from it
into `shared/schema.gen.ts` (re-exported by `shared/app-def.ts`, which layers on the relationships,
normalization, and mutators) — so the TypeScript can't drift from the DDL. To change the schema,
add a new ordered `migrations/*.sql`; `pnpm dev` applies it and regenerates `shared/schema.gen.ts`
on every change.

## Run it

```bash
pnpm install
pnpm dev
```

`pnpm dev` is one lifecycle command:

```bash
rindle dev --migrate --gen shared/schema.gen.ts -- vite dev --port 3000
```

It evaluates `rindle.ncl` once, supervises the write-master, follower, and stable fleet edge, waits
for the read path to be ready, applies migrations, regenerates the schema, then starts Vite with the
unified `RINDLE_URL` + `RINDLE_DATABASE_TOKEN` server bindings. Migration and follower-schema changes
are watched automatically; Ctrl-C or an app exit tears the whole process tree down. The browser has
no topology environment variable: its first query lease carries the public WebSocket endpoint and a
fresh placement ticket, so the optimistic client opens the correctly pinned connection lazily.

Normal development needs neither a second supervisor, a readiness probe, nor a topology-specific
web command.

Open two browser windows to watch writes sync live: create a room, post a message, and watch the
room's count update on the home page with no polling. Try a room name or message containing "spam" to
see the rejection path (the optimistic write snaps back + a toast).

> Requires Node ≥ 22.18. The TanStack route tree (`src/routeTree.gen.ts`) is a generated artifact —
> `pnpm dev` and `pnpm generate-routes` produce it; `pnpm typecheck` runs `tsr generate` first.

## Deploy

`rindle.ncl` describes the **one topology** — the same file `rindle up` runs locally. To run the
**data tier** on Rindle Cloud:

```bash
rindle login                  # once — authenticate to Rindle Cloud
pnpm rindle:deploy            # provision / re-attach the managed app (writes .rindle/cloud.json)
pnpm rindle:migrate:cloud     # push migrations/*.sql to the deployed write-master
```

`rindle deploy` reads `rindle.ncl` and records the binding in `.rindle/cloud.json` — commit it, so
future deploys re-attach to the same app. `followers = 1` provisions a backed-up write-master + one
follower (`replicated`, scales 1→N in place); raise it for more read replicas (`read-scaled`).
Every rung ships its journal off-box — the old one-box `colocated` rung (`localRetention = true`)
is retired, and setting that field fails the render. That deploys the **data tier**; the
**web app** (this Vite/TanStack Start app) deploys to any Node host — configure its server with the
one fleet ingress (`RINDLE_URL`) and SQL bearer (`RINDLE_DATABASE_TOKEN`). Query leases derive the
public WebSocket endpoint from that same URL, so there is no browser-side topology setting whether
the deployment has one follower or many. Set the server-only `RINDLE_WS_URL` override only when a
host exposes WebSocket ingress at a different origin.

## Devtools

In development a floating **🌊 Rindle** devtools pane is mounted (`src/devtools.tsx`): a live view of the
**mutation timeline** (the optimistic fork/rebase loop, with a snap-back highlight when a prediction
diverges from the server), the **queries inspector**, and the raw **delta stream**. It's wired up in
`src/devtools.tsx` (the panel) and `src/rindle-client.ts` (`attachDevtools`), both behind
`import.meta.env.DEV` — so `@rindle/devtools` and `@rindle/react-devtools` are tree-shaken entirely out
of `vite build` and never ship to production.

## Layout

| path | what |
|---|---|
| `migrations/*.sql` | the schema — **source of truth** |
| `shared/schema.gen.ts` | generated `@rindle/client` schema (don't edit by hand) |
| `shared/app-def.ts` | the contract root: schema re-export, relationships, isomorphic mutators |
| `shared/auth.ts` | the identity seam (`AuthProvider`) |
| `rindle.ncl` | the one topology (the colocated pair) — `rindle up` runs it locally, `rindle deploy` provisions it |
| `server/app-api.ts` | the authority: query resolution, `sharedApiMutators`, policy (host-agnostic) |
| `server/rindle-http.ts` | adapts the authority to a Web Request (the Start API routes call it) |
| `src/routes/api.rindle.*.tsx` | the three API endpoints as Start server routes (the browser's API) |
| `src/rindle-tanstack.ts` | one route-loader/provider binding for SSR and client navigation |
| `src/ssr.ts` | first-paint preload — calls the authority in-process |
| `src/components/*.queries.ts` | co-located queries + fragments |
| `src/routes/*` | TanStack routes |
| `src/devtools.tsx` | dev-only in-browser devtools pane (tree-shaken from prod) |
