carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1# TodoMVC: Go + Rastrillo
2
3The bakeoff's TodoMVC built the way Rastrillo v0.28 recommends: a
4server-rendered app that works with JavaScript off, plus one small ES
5module that makes it behave like the reference TodoMVC when JavaScript is
6on. Live at
7[https://todo-go-rastrillo.bfj.oncarlos.com](https://todo-go-rastrillo.bfj.oncarlos.com).
8
9## The stack, and why
10
11| Piece | Choice | Why |
12|---|---|---|
13| Framework | [Rastrillo](https://rastrillo.org) v0.28.0 | The CARLOS framework. `rastrillo.Resolve` + `Serve` speak the platform contract (unix socket, `/healthz`, `/api/version`, SIGTERM drain, baseline security headers), so the app doesn't hand-roll any of it |
14| Router | `github.com/go-chi/chi/v5` | Rastrillo's router: `net/http` handlers, middleware per group |
15| Data | GORM over `rastrillo/db` | Rastrillo's default data layer. `db.Open` sets up SQLite (modernc, no cgo) in WAL mode with one writer connection and a reader pool |
16| Migrations | `rastrillo/migrate`, generated by `rastrillo migration generate` | Ledgered, applied once at boot, additive only. `make test` runs `rastrillo migration check` |
17| Visitors | `rastrillo/sessions` | Each browser's list is an anonymous session (below) |
18| Scoping | `rastrillo/scope` | Every todo query goes through one `owned(r)` seam that adds `WHERE user_id = ?` |
19| Forms | `rastrillo/form` | `form.Parse` reads and trims each field by name; nothing is bound onto a model |
20| Templates | `html/template`, embedded | Titles are always escaped text |
21| Static files | `rastrillo.NewAssets` | Pages link fingerprinted, cache-forever URLs; the bare names (`/static/base.css`) are served too, byte for byte |
22| JavaScript | `static/app.js`, one ES module, no build step | Posts the page's own forms with `fetch` and swaps in the server's markup |
23
24The UI is TodoMVC's own markup and CSS (`base.css`, `index.css`), not
25Rastrillo's design system: the bakeoff requires it. `static/app.css` adds
26one rule that matters, `.todoapp form { display: contents }`, so the forms
27that make every action work without JavaScript don't change the layout.
28
29### How the anonymous lists work
30
31Rastrillo's `sessions` stores signed-in state as SQLite rows keyed by
32the SHA-256 of a 256-bit random cookie token. This app treats "being
33given a list" as the sign-in: the first request with no live session
34creates a `visitors` row and calls `sessions.SignIn` with that row's id as
35the subject (method `anonymous`). From then on `sessions.UserID(r)` is
36the visitor's id and `scope.Owned` filters on it, exactly as it would for
37a signed-in user. An unknown or garbled cookie resolves to no session, so
38that browser gets a fresh, empty list. The cookie is
39`HttpOnly; SameSite=Lax; Path=/`, and on https it's `Secure` with the
40`__Host-` prefix. Lists live 400 days from the first visit instead of
41sessions' default of 30.
42
43### Cross-site protection: the contract's Origin check, not `csrf.Protect`
44
45Rastrillo's `csrf.Protect` refuses any POST that carries none of
46`Sec-Fetch-Site`, `Origin` or `Referer`. The bakeoff's shared tests and
47load tests post exactly like that, and the contract allows it. Protect
48also trusts `Sec-Fetch-Site` ahead of `Origin`, so it would let through a
49request claiming `same-origin` that carries a foreign `Origin`. So
50`origin.go` does the contract's check instead: a POST whose `Origin`
51header is not exactly this app's origin gets 403, and a POST with no
52`Origin` header is allowed. There's no CSRF token, as the contract says.
53
54Form posts are capped at 1 MiB and parsed before any handler runs. A
55body that's too big gets 413 and a malformed one gets 400, and neither
56changes anything.
57
58### JavaScript on and off
59
60Every action is a plain form post that answers 303. With JavaScript off,
61`<noscript>` loads `static/nojs.css`, which shows the contract's submit
62controls (`[data-action="toggle"]`, `[data-action="toggle-all"]` and the
63`a[data-action="edit"]` link) as transparent buttons laid over the circle,
64the ❯ and the title. The page looks the same, and clicking where you'd
65click with JavaScript does the same thing through a page load.
66
67With JavaScript on, `app.js` sends the same forms with an
68`X-Todo-Fragment` header, and the server answers with the new contents
69of `section.todoapp` instead of a redirect. The checkboxes, double-click
70editing, Enter, blur and Escape behave as in TodoMVC. Without that header,
71nothing changes.
72
73## Build, run, test, deploy
74
75Go 1.25 or newer. On docs-t1, `source ../../scripts/env.sh` first.
76
77```sh
78make build # dist/app for this machine
79make build-arm64 # dist/app-linux-arm64, static, what ships
80make test # vet, gofmt, go test with Rastrillo's time budgets, migration check
81make perf # every GET screen under 150ms to first byte, cold boot under 500ms
82```
83
84Run it locally. It refuses any TCP address not on `127.0.0.1`, and
85defaults to `127.0.0.1:8080`:
86
87```sh
88dist/app --listen 127.0.0.1:8080 --db "$TMPDIR/go-rastrillo.db"
89../../scripts/test-app.sh http://127.0.0.1:8080 "$TMPDIR/go-rastrillo-suite.json"
90```
91
92On Carloku the platform starts it as `app --socket <path> --db <path>` and
93sets `CARLOS_HOST`; the app's origin is then `https://$CARLOS_HOST`.
94`--listen` is the bakeoff's name for Rastrillo's `--addr`, and `main.go`
95translates it before `rastrillo.Resolve` reads the arguments.
96
97```sh
98make build-arm64
99../../scripts/boot-check.sh dist/app-linux-arm64
100make deploy MESSAGE="What changed"
101```
102
103The version `/api/version` reports is the git short SHA, stamped into
104`rastrillo.BuildVersion` at build time.
105
106`make clean` removes `dist/`. `make clean-deps` does the same, because
107Go keeps compiled dependencies in the shared `GOCACHE`, which stays
108untouched on this machine: `make build-arm64 COLD=1` rebuilds everything
109from scratch with `-a` instead.
110
111## Honest trade-offs
112
113- **Trust class C (2026-10-23).** Todo titles are stored as plain text in
114 SQLite, and the server can read them.
115- **No sign-in.** A list belongs to a cookie. Clear it, or use another
116 browser, and that list is gone for you.
117- **GORM costs some speed.** Rastrillo's own measurements put GORM reads
118 at about twice the time of plain `database/sql`. Rastrillo recommends it
119 for CRUD apps like this one, so this entry uses it.
120