Build a synced app

Scaffold with create-rindle

Generate a SQL-first Rindle app on TanStack Start: browser client, API authority, unified local data tier, migrations, generated schema, SSR, and devtools.

View as Markdown

The synced-app quickstart builds a Rindle app by hand so you can see every seam. create-rindle is the shortcut after that: it generates the same three-tier shape as a small, running TanStack Start app.

Use it when you want a project to edit instead of copy-pasting the quickstart. The template is intentionally small - a rooms-and-messages app with live message counts, optimistic writes, a rejection path, SSR, generated schema, and dev-only Rindle devtools.

Create an app

npm create rindle@latest my-app
# or
pnpm create rindle my-app
# or
npx create-rindle my-app

Then run it:

cd my-app
pnpm dev

pnpm dev runs one lifecycle command:

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

It evaluates the committed rindle.ncl once, then supervises and waits for the write-master, follower, and fleet edge. It applies migrations and regenerates shared/schema.gen.ts. Then it starts TanStack Start with RINDLE_URL + RINDLE_DATABASE_TOKEN. Signals and teardown stay inside that same lifecycle. There is no concurrently, readiness probe, or nested rindle exec.

Open two browser windows and create a room. As you post messages, the room count updates live in both. Try a room name or message containing "spam" to watch an optimistic write snap back after the API authority rejects it.

RINDLE_DATABASE_TOKEN remains server-only. The browser calls same-origin /api/rindle/* routes. Its first query lease returns the public WebSocket endpoint and an opaque follower-affinity ticket, so no browser topology configuration is needed.

The generated app requires Node >= 22.18.

What it generates

It is the same architecture as the quickstart, packaged as a starter:

Tier In the template What it does
Browser src/rindle-client.ts, src/rindle-tanstack.ts, src/components/*.queries.ts, src/routes/* Runs createRindleClient, local wasm IVM, optimistic writes, first-class TanStack route readiness/SSR, useRoot/useFragment, and co-located queries/fragments
API authority server/app-api.ts, server/rindle-http.ts, src/routes/api.rindle.*.tsx Resolves named queries, runs authoritative SQL mutators through the unified Rindle ingress, and exposes the Rindle API through TanStack Start server routes
Data tier rindle.ncl, migrations/*.sql rindle dev renders and supervises the write-master + follower + fleet edge, applies migrations, generates schema, and streams live deltas

The schema is SQL-first. Edit or add migrations/*.sql. The dev loop applies the migration to the write master and regenerates shared/schema.gen.ts from the follower’s introspected schema. A file can be pure DDL (including reviewed destructive drops) or pure DML for seeds and bounded backfills. Keep the two kinds in separate ordered files. Keep relationships, normalization, and mutators in shared/app-def.ts.

Why TanStack Start

The starter uses TanStack Start because it gives the template one coherent home for the browser, SSR, and server routes:

  • src/routes/api.rindle.query.tsx, api.rindle.read.tsx, and api.rindle.mutate.tsx are the browser-facing API routes.
  • SSR reads call the same app authority in-process, so first paint and client subscriptions use the same query registry.
  • @rindle/tanstack lets each file route declare its query once, then owns server preloading, client navigation readiness, cancellation, and the seed-to-live provider handoff.
  • TanStack Router’s file routes keep app screens and Rindle query modules close without forcing the Rindle APIs themselves to depend on TanStack.

Rindle is not TanStack-specific. The important parts are the three tiers and the contracts: SQL migrations, generated schema, named queries, predicted mutators in the browser, authoritative mutators on the server, and the write-master + follower as the live data tier. TanStack Start is the starter’s application shell.

Where to look first

File Why it matters
migrations/0001_init.sql The source-of-truth SQL schema
shared/schema.gen.ts Generated @rindle/client schema; do not hand-edit
shared/app-def.ts Schema re-export, relationships, normalization, and predicted mutators
src/components/*.queries.ts Named root queries and fragments, co-located with UI
src/rindle-client.ts The one browser client setup call
src/rindle-tanstack.ts The shared route-loader/provider integration
src/ssr.ts Server-only first-paint preload helper
src/devtools.tsx Dev-only Rindle devtools panel mount
server/app-api.ts Authoritative query registry, SQL mutators, policy
rindle.ncl The topology rindle dev (local) and rindle deploy (cloud) both read — followers = 1, the colocated pair; loopback in dev
AGENTS.md The rules of the app for coding agents (never edit schema.gen.ts, deterministic mutators, named queries, …) — Claude Code, Cursor, and Codex read it automatically

Customize it

Treat the generated app as a working baseline:

  • Change tables in migrations/*.sql, then let pnpm dev regenerate the typed schema.
  • Add relationships and mutators in shared/app-def.ts.
  • Add named root queries or fragments beside the components that read them.
  • Replace the demo auth seam in shared/auth.ts and the policy in server/app-api.ts.
  • If you point the app at Rindle Cloud or a self-hosted fleet, keep RINDLE_DATABASE_TOKEN server-only.

The same rindle.ncl drives the cloud:

  1. Run rindle login.
  2. Run pnpm rindle:deploy. It provisions (or re-attaches) the Sync plan — a managed master + follower on Rindle’s packed OVH fleet — and records the binding in .rindle/cloud.json. Commit that file.
  3. Run pnpm rindle:migrate:cloud to push migrations/*.sql through the Cloud proxy.

Local dev reads the same authored topology through rindle dev. See deploy.

For the underlying commands, see @rindle/cli. For the manual version of the same app shape, keep the quickstart open beside the generated project.

Next steps