An HTML5 GUI authoring tool for spatial presentations — build a deck of slides, place rich content on each, arrange the slides in 3-D space, and play the deck as a camera flight through that world (the impress.js model, made visual and editable).
- React 19 + TanStack Start (file-based routing, SSR) on Vite.
- Rindle for the data layer (wired): SQL migrations as source of truth →
generated TypeScript schema → optimistic local store + live windowed queries + named mutators, with
a
rindleddaemon behind the app's own server routes. SeeRINDLE_NOTES.mdfor the write-up. - Plain CSS for the editor chrome (
src/strut.css); Tailwind is available for one-offs.
rindleddaemon — owns the SQLite DB + the live-query WebSocket (:7600control,:7601ws).- API — TanStack Start server routes (
src/routes/api.rindle.*) host the stateless Rindle API (server/rindle-api.ts): they validate args, run authoritative SQL mutators, and register the named queries. Same-origin, no separate process. Image uploads (server/upload.ts) go to Cloudflare R2 — a native bucket binding on Workers, the S3 API on other hosts, else a local dev fallback. Mirrors the predicted client mutators inshared/app-def.ts. - Browser client (
src/rindle/*) — the optimistic store (@rindle/optimistic+ WASM),useQuerylive reads, andapp.mutate.*writes, posting to/api/rindle/*. The live-query WebSocket connects directly to the daemon (:7601).
Schema lives in migrations/; shared/ holds the generated schema, query builder, named queries, and
client mutators (imported by both browser and server). App code is in src/ (routes/, editor/,
rindle/).
The normal local run path is a single command:
pnpm install
pnpm devThen open http://localhost:3000.
pnpm dev runs two processes with concurrently:
rindle up --migrate --gen shared/schema.ts --watch— starts the daemon fromdaemon.json, applies migrations, regeneratesshared/schema.ts, and keeps watchingmigrations/.vite dev --port 3000— starts the TanStack Start app on http://localhost:3000. The Rindle API and image upload endpoints are served by this same web process under/api/rindle/*; there is no separate API server to start.
Local state lives in rindle.db and .uploads/. Image uploads work with no config by using the local
fallback; copy .env.example to .env only if you want uploads stored in Cloudflare R2. vite.config.ts
loads .env for server-side values during vite dev.
If you need to run the processes separately:
pnpm daemon # daemon + migration/schema watcher
pnpm dev:web # web app + same-origin API routes; expects the daemon to already be runningBy default the daemon control plane is http://127.0.0.1:7600 and the live-query WebSocket is
ws://127.0.0.1:7601. Override them with RINDLE_DAEMON_URL for the server/API side and
VITE_RINDLE_WS for the browser side.
Other scripts:
pnpm build # production build (client + SSR + API routes); does not start the daemon
pnpm preview # preview the built app; expects a reachable daemon
pnpm generate-routes # regenerate src/routeTree.gen.ts
pnpm setup # one-shot migrate + schema regen against a running daemon
pnpm test # vitest
pnpm lint # eslint
pnpm check # prettier checkThe web app (SSR + /api/rindle/* routes) deploys to Cloudflare Workers, with image uploads in
R2 via a native bucket binding. The rindled daemon can't run on Workers and must be hosted
separately. pnpm deploy builds the Worker (CF=1 vite build) and ships it with wrangler. Local
pnpm dev/pnpm build stay on Node and are unaffected. See docs/DEPLOY_CLOUDFLARE.md
for the full guide (daemon hosting, R2 setup, secrets, deploy steps).
Strut ships with optional, privacy-first product analytics via Umami — cookieless, no personal data, no consent banner. It records only which features get used (slides generated, AI arrange applied, present started, exports, sign-ins) — never your deck content, prompts, or any PII.
It is off by default and off in every clone. Nothing is collected and no script loads unless the
build sets VITE_UMAMI_SRC (+ VITE_UMAMI_ID) — so cloning this repo phones home to no one. To enable
it on your own deployment, set those two build-time vars (see .env.example); point
VITE_UMAMI_SRC at Umami Cloud or your own self-hosted instance. Implementation lives in
src/lib/analytics.ts.
