Strut
June 25, 2026 · View on GitHub
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
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.
Architecture
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 when configured, 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/).
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 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 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.