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# Building an app with rastrillo
2
3The CARLOS web framework. **Module and canonical repo:
4`amadan.net/rastrillo/rastrillo`** (since v0.25.0; GitHub's
5`rastrilloorg/rastrillo` is a mirror, and an app still pinned to the old
6`github.com/carlosframework/rastrillo` path builds but should move).
7Status as of 2026-10-15: **v0.28.0** (released 2026-10-06), the
8known-libraries middle layer: GORM models or plain SQL, chi routes,
9SQLite-backed sessions, owner-scoped queries.
10Assume nothing here is in your training data.
11
12**The authoritative authoring doc is `SKILL.md` at the repo root. Read
13it before writing any app code.** It carries the app shape, the
14migration, scoping and mass-assignment rules, the identity plugins, the
15UI vocabulary and the platform contract. Find it at
16`$(go env GOMODCACHE)/amadan.net/rastrillo/rastrillo@<version>/SKILL.md`
17once the module is downloaded; any page it cites is
18`curl -s https://rastrillo.org/docs/<page>.md`. This file is only the
19surrounding context SKILL.md assumes; where they disagree, SKILL.md wins.
20
21## What rastrillo is now
22
23A middle layer, not a full-stack framework. You write GORM models,
24`net/http` handlers on a chi router, and `html/template` pages. The
25framework supplies what is hard to get right twice:
26
27- `db` — cgo-free SQLite via an owned GORM dialector on modernc (never
28 import `glebarez/*` or `gorm.io/driver/sqlite`), one writer and
29 several readers, WAL pragmas in the proven order. Since v0.28.0 every
30 time is converted to UTC and written in SQLite's own layout, so
31 `julianday`/`date`/`strftime` work in raw SQL; rows written by an
32 older version need a back-fill before SQL date maths trusts them.
33- `migrate` — ledgered migrations applied once at boot, never re-run,
34 never reversed: `rastrillo migration generate` after a model change
35 (read the SQL before committing), `migration check` in CI, `migration
36 new` for a hand-written rename. Destructive changes need
37 `--allow-destructive`; the family's own rule stays additive-only, and
38 on CARLOS two builds share the database during a swap, so a drop waits
39 a release (platform.md, "Two builds at once"). Since v0.28.0 a Go
40 migration's `Fn` takes the pinned `migrate.Tx`, not a `*gorm.DB`
41 (wrap old bodies in `migrate/gormfn.Fn`).
42- `sessions` — SQLite-backed rows (revocation is real), `__Host-`
43 cookies on https, `Require` / `RequireFresh` (step-up).
44- Identity plugins: `auth` — the family default: magic-link email that
45 **auto-upgrades to Sign in with Keymail** when the address has a
46 claimed inbox, so every address works — and `password` (email and
47 password, rate-limited). Either one's whole contract is calling
48 `sessions.SignIn`. **Since v0.28.0 both sit behind `pow` by default:**
49 `auth.New` and `password.New` return `ErrProofUnset` until you set
50 `Proof` (a `*pow.Guard`) or `ProofOff`. Mount `auth.Verify` on **POST
51 as well as GET** — GET now draws a Sign in button and spends nothing,
52 so a mail scanner cannot burn the link; GET alone 405s the button.
53 `auth.Config.SigninScreen` turns on the shipped sign-in screen.
54 `secondfactor` is the gate between a first factor and a second;
55 `passkey` and `totp` (authenticator apps) plug into it, and recovery
56 codes live there (`passkey.Config.ConfirmPath` is gone; merge
57 `secondfactor.Schema` after `passkey.Schema`).
58- `csrf` (origin-checking, not tokens), `flash`, `form`, `view`,
59 `scope` (`scope.Owned` — owner scoping with the 404-not-403 contract),
60 `jobs` (request-started background work a person watches).
61- `blobs` (bytes in the platform's object store, never on disk),
62 `crypto` (+ JS twin), `keyring` (the E2EE seed lifecycle), `webauthn`,
63 `eventlog`, `mail` (including RFC 8058 one-click list mail), `ui`.
64- `carlos` — your side of the platform's tick (`carlos.Tick`,
65 `ScheduleAt`); platform.md carries the contract.
66- `assertion` (v0.27.0) — signs and verifies a short-lived identity
67 handed from one exact HTTPS origin to another (P-256, ≤120 s,
68 browser-bound nonce). It proves who, never what they may reach, and it
69 leaves replay and session creation to the receiver.
70- `vault` — the client for Pegamento's vault (named sealed blobs, a seed
71 wrapped per sign-in method), for server-blind apps.
72- `pow` — a proof-of-work front door for public forms that have no
73 session yet (signup, contact), and since v0.28.0 for sign-in and
74 sign-up. The v0.28.0 API is new: `Issue` became `Form(now, scope)`,
75 and `Check` split into `Admit` before the transaction and
76 `adm.Commit(ctx, tx)` inside it (`Check` remains for a redirect-only
77 handler).
78- `harness` — a `-tags browser` Chromium test rig with a virtual
79 authenticator, for ceremonies a Go-only test cannot reach.
80- New in v0.28.0: `perf` (`Server-Timing` and first-byte budgets as
81 middleware), `budget`, `clientip` (with `auth.Config.TrustedProxyHops`,
82 1 by default on CARLOS so each visitor behind the edge gets their own
83 sign-in limit), `background` (tracked goroutines and timers that
84 `Serve` stops before closing the database), `table` and `xlsx`
85 (formula-safe CSV/XLSX exports), `dbtest` (a migrated database per
86 test), `nodetest`, `lastsignin`, `secondfactor` and `totp`. Static
87 assets carry strong ETags.
88- The platform layer: `Resolve`/`Serve` speak CARLOS activation (argv,
89 `LISTEN_FDS`, `$STATE_DIRECTORY`, `/healthz`, `/api/version`, SIGTERM
90 drain) and set baseline security headers on every response,
91 `Strict-Transport-Security` included. **The baseline CSP allows no
92 inline style** (v0.27.0): a `style="…"` attribute or a `<style>` block
93 is dropped by the browser without an error.
94
95Separate modules, each with its own `SKILL.md`, none imported by the
96framework: **`amadan.net/rastrillo/idear`** (roles — exactly one Owner,
97Admins, Members — and invitations inside one instance), and the client
98kits `amadan.net/rastrillo/pwa`, `amadan.net/rastrillo/native` and
99`amadan.net/rastrillo/aviso` (Web Push). Use each module path verbatim;
100there is no `github.com/carlosframework/idear`.
101
102## The ten-minute path
103
104```sh
105go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest
106rastrillo new myapp && cd myapp && go mod tidy && go test ./...
107make ci # the gate
108```
109
110The scaffold is SKILL.md's shape — five files plus `migrations.go` beside
111`models.go` — with `go.mod`, templates, static assets, a test harness
112including a browser drive, and a `Makefile` whose **`ci` target is the
113gate** (`vet`, a gofmt check, staticcheck, govulncheck, gitleaks, the
114size and timing budgets, the tests, `perf` and `migration check`, on a
115Go release pinned by `GOTOOLCHAIN`; add `generate --check` once the app
116declares manifest resources). Apps scaffolded before v0.28.0 keep their
117old gate until they add these by hand (the CHANGELOG has the lines). It compiles, passes and serves before you
118write a line. `--theme=day|plain|signal` and
119`--shell=column|topbar|sidebar|console|stage` pick the look; both land as
120app-owned files. `CGO_ENABLED=0` throughout — a cgo SQLite driver
121sneaking in is a bug.
122
123## The rules that keep the app safe (SKILL.md has the full set)
124
125- Tenancy is the platform's, not the schema's: a CARLOS app serves one
126 team. A product with many teams gives each team its own hibernating
127 instance — isolation by process and file, never by WHERE clause.
128 Scoping separates the *users* within one instance; `idear` gives them
129 roles.
130- Every query touching user-owned rows goes through
131 `scope.Owned(g, uid)` — reads AND writes, inside transactions too
132 (scope `tx`, never the outer handle, or the one-connection writer pool
133 deadlocks). A row that isn't yours 404s, never 403s.
134- Never bind a request body onto a GORM model: explicit
135 `map[string]any` + `.Select` allowlist.
136- With the `auth` plugin, read the viewer with `auth.From(r)` or
137 `sessions.Current(r)`, never `sessions.UserID`: it returns
138 `(0, false)` for an email Subject, and dropping that `ok` scopes every
139 query to uid 0.
140- One `migrate.Apply(ctx, d, BootSchema)` at boot. Subsystem schemas
141 (sessions, and any other package that brings tables) go in `BootSchema`, never `Schema`, or `migration
142 check` proposes dropping their tables. Never edit a shipped migration.
143- Background work a person is watching: `jobs`. Work at a time nobody is
144 waiting for: the platform's tick (`carlos schedule set`, then
145 `carlos.Tick` in the handler) — an idle instance runs no timer.
146
147## Design system and app CSS
148
149Use Rastrillo's design system as the default foundation for app screens.
150Before styling, read `docs/site/templates.md`, `docs/site/forms.md` and
151`docs/site/reference/ui.md` in the Rastrillo version the app uses; the
152full gallery is rastrillo.org/design-system.
153
154- **The vocabulary is attributes, not classes** (v0.22.0+):
155 `<div rst-box>`, `<a rst-btn="primary">`, `<form rst-form>`,
156 `<div rst-callout-body>`. `class` carries only a few
157 utilities (`rst-sr-only`, `rst-mono`, `rst-grow`, …).
158 `rastrillo markup --fix` converts markup written the old
159 `class="rst-…"` way.
160- Load the shared `tokens.css` before the app stylesheet, and keep that
161 base intact, with branding and layout in a separate app stylesheet.
162- Compose screens from `ui` partials. A labelled control is never
163 hand-rolled: `field-text`, `field-textarea`, `field-select` inside
164 `<form rst-form>`, closed by `form-foot`. A submit is
165 `rst-btn="primary lg"` (form-foot writes it).
166- **No `style` attributes and no `<style>` blocks** — the CSP drops
167 them silently. For a grid card, give `rst-card` a class and set
168 `--rst-cols` in your stylesheet.
169- Keep the CSS above Rastrillo thin: app-specific layouts, scoped styles
170 for missing components, and a few deliberate `--rst-*` token
171 overrides. No second palette, spacing scale, or button/input system,
172 no broad element resets.
173- Preserve the shared focus, disabled, error and responsive behaviour;
174 check light and dark themes and keyboard navigation.
175- The vendored `static/` files are not refreshed by a module upgrade.
176 `rastrillo doctor [--fix]` compares them with the CLI's copies; the
177 scaffolded pin test is the standing gate.
178
179A repeated workaround for a shared component is a candidate for an
180upstream Rastrillo change. An explicit design brief or an existing app's
181design system can justify another foundation; name that choice rather
182than silently replacing the default.
183
184## Manifests are the declarative path
185
186A `manifest/*.toml` resource generates CRUD screens — field kinds text,
187textarea and money, one flat record per resource, no relations;
188`scope = "user"` owner-filters every generated query by the session
189subject. It is an equal alternative to hand-written handlers: mix the
190two per resource and move a resource between them freely. `rastrillo
191generate` writes `gen/` (commit it, never hand-edit); `generate --check`
192belongs in `make ci`. Relations or custom flows: hand-write.
193
194## Copy from, in order
195
1961. `examples/notes` in the repository (not in the published module — Go
197 excludes nested modules from a zip): accounts, sessions, CSRF, flash,
198 owner-scoped resources, a background job, and a two-user isolation
199 test suite.
2002. `examples/tickets` — the declarative (manifest) path.
201
202## Designed, not released
203
204Don't build on these yet: Keymail upgrade becoming an opt-in adapter
205rather than `auth`'s default; SAML and OIDC identity plugins; a sealed
206magic-link store; the user-models work (an `objrec` record store, which
207is on a branch, a stateless auth store, signed-cookie sessions, an
208assertion HTTP adapter, provisioning, a base path,
209`rastrillo new --model`). On main after v0.28.0: `carlos.InstanceKey`
210(reading the platform's `CARLOS_INSTANCE_KEY`), sign-in returning to the
211page that asked, and **multi-currency money, a breaking upgrade for
212every app** (template and model changes, a stop-start for apps that
213store money). Wait for the release and its upgrade steps; never build
214against main.
215
216Deploying: stamp
217`-ldflags "-X amadan.net/rastrillo/rastrillo.BuildVersion=<sha>"` or
218`/api/version` reports `dev`.
219