Strut

June 25, 2026 · View on GitHub

CleanShot 2026-06-25 at 08 58 27@2x

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).

This repository is a ground-up rewrite. The authoritative description of what Strut is and how it behaves lives in docs/STRUT_SPEC.md — a framework-agnostic behavior + data-model spec reverse-engineered from the feature-complete 2012 build. Treat it as the source of truth; this app implements it.

Stack

  • 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 rindled daemon behind the app's own server routes. See RINDLE_NOTES.md for the write-up.
  • Plain CSS for the editor chrome (src/strut.css); Tailwind is available for one-offs.

Architecture

  • rindled daemon — owns the SQLite DB + the live-query WebSocket (:7600 control, :7601 ws).
  • 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 when configured, else a local dev fallback. Mirrors the predicted client mutators in shared/app-def.ts.
  • Browser client (src/rindle/*) — the optimistic store (@rindle/optimistic + WASM), useQuery live reads, and app.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/).

Getting started

The normal local run path is a single command:

pnpm install

pnpm dev

Then open http://localhost:3000.

pnpm dev runs two processes with concurrently:

  • rindle up --migrate --gen shared/schema.ts --watch — starts the daemon from daemon.json, applies migrations, regenerates shared/schema.ts, and keeps watching migrations/.
  • 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 running

By 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 check

History

The prior in-progress React + cr-sqlite (vlcn.io) rewrite is archived at branch archive/cr-sqlite-rewrite (tag rewrite-archive-v1) for reference. The feature-complete original lives at origin/old-master.