rastrillo

package module
v0.27.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2026 License: MPL-2.0 Imports: 33 Imported by: 0

README

🤖 Rastrillo

The CARLOS web framework — the shape of a CARLOS app, the way the platform (carlosframework/platform) is the shape of the deployment substrate it runs on. Read the full design at carlosframework/platform's spec library (approved and merged 2026-08-01).

Status

v1 was a walking skeleton, built overnight to prove the core loop end to end. Two overnight passes since then split the remaining design between them: one built the manifest system, the ui vocabulary, fingerprinted assets and the scaffolded harness on main; the other (docs/superpowers/specs/2026-08-17-completion-design.md) built the subsystem packages against what the family's apps had hand-rolled in the meantime. This list is their union. Built:

  • rastrillo new [flags] <name> — scaffolds a Go app: go.mod, one starter action, a main.go wiring rastrillo.Run. Runs generate once so go build works immediately. Takes --icons, --icon-delivery and --ux — see Icons and UX conventions below.

  • Icons and UX conventions--icons (lucide, the default, or font-awesome), --icon-delivery (inline, the default, or cdn or js), and --ux (considered, the default, or standard). All six set × delivery combinations scaffold, compile and pass --check.

    The chosen set is written into the app as an ordinary app-owned internal/<app>/icons package — the same terms as tokens.css and rastrillo.js: delivered once, yours from then on. Its map is keyed by rastrillo's slugs whatever the set, so {{icon "search"}} means the same thing in every app and the shipped ui/ partials never change when the set does. The scaffold wires both seams into the generated render.go and puts {{iconAssets}} in the layout's <head>; that renders empty for the inline default, so switching delivery later needs no template edit.

    Those slugs are rastrillo's own vocabulary, not a vendor's, and that is load-bearing rather than pedantic: five of the eleven differ from Lucide's canonical names (kebab is Lucide's ellipsis-vertical, and v1 renamed check-circle, alert-triangle, x-circle and help-circle), so even the Lucide set carries a translation table. rastrillo.IconSlugs is the list, and internal/iconsets asserts every scaffoldable set covers all of it — an icon added to icons.go that a set cannot answer would otherwise vanish the moment someone passed --icons.

    Inline vendoring stays the default and the recommendation — no build step, no second origin, works offline, which is what icons.go has always been for. cdn and js are fully supported rather than discouraged: each prints its specific cost once at scaffold time, records it as a comment in the generated package, and is never mentioned again. The cost worth repeating is js's — icons do not render at all without JavaScript. Both remote modes pin exact versions with real SRI hashes.

    --icons=font-awesome means Font Awesome Free. Pro is a paid product rastrillo cannot vendor or link on your behalf, so Pro-only icons will not resolve; a Pro licensee wires their own kit through the same seam, since the icons package is app-owned source. Choosing font-awesome also writes the CC BY 4.0 attribution its licence requires — that obligation is the app's, so it travels with the code.

    Versions are pinned (lucide@1.33.0, lucide-static@1.33.0, @fortawesome/fontawesome-free@7.3.1) and nothing re-pins them automatically. A version changed without its hash fails as an unstyled page rather than an error, so check both together with:

    go test -tags pins ./internal/iconsets/
    

    That verifies every pinned URL still hashes to the integrity value shipped beside it — a mismatch means the bytes changed under a version that is supposed to be immutable, which is serious — and separately reports whether a newer release exists, which is only informational. Build-tagged, so the ordinary suite and CI never depend on jsdelivr or the npm registry being up: a check that fails when someone else's CDN has a bad afternoon teaches people to ignore it. Run it at release.

    rastrillo generate --check fails when a template names an icon nothing answers — both {{icon "x"}} and the commoner form where the slug reaches a partial as data (dict "ActionIcon" "plus"). At run time an unknown slug still renders nothing rather than crashing a response; this is the pre-ship gate, the same posture as the i18n catalog check. A slug computed at run time cannot be checked, as with any static gate.

    --ux seeds a UX convention profile into the app's AGENTS.md, which carries the app's instructions and is the source of truth from then on; CLAUDE.md is a one-line @AGENTS.md import so the instructions reach whatever agent someone uses rather than one of them. The profile is a seed, not a live binding. The resolved list is written once, an explicit flag beats the profile's default so the file never lies about what the app does, and nothing re-reads the profile name afterwards — which is what makes editing a line as valid as picking a profile, and what stops a rastrillo upgrade changing a shipped app's UX. Conventions marked [x] are enforced by a vendored component; [ ] ones an agent applies by hand, and the gap between the two is kept visible rather than blurred.

    The conventions in considered are rastrillo's own, and the profile is named for what it does rather than after anyone else's work. For wider reading on interface quality, impeccable.style, the WAI-ARIA Authoring Practices and Inclusive Components are all worth your time — offered as reading, not as anything this framework endorses or claims to implement.

  • rastrillo generate [dir] — the filesystem-routing generator (design doc §4): walks actions/, emits gen/router.go on a Go 1.22 http.ServeMux. Fails loudly on route collisions. Action files carry //go:build rastrillo_actions (scaffolded for you; stripped from the compiled copies under gen/) so go build ./..., go vet ./... and go test ./... skip generator input instead of failing on it — generate --check names any file missing the constraint.

  • rastrillo dev [dir] [-- app args] — the development watch loop (design doc §11): watches app/, actions/, manifest/, cmd/, internal/, locales/, templates/, and static/ by polling. On any change, reruns rastrillo generate, builds the app's ./cmd/<name> package to a temporary binary (cleaned up on exit), and restarts the running process (graceful SIGTERM). A failed generate or rebuild keeps the previous build serving; a failed restart keeps the loop watching too — either way, the next save retries. Expects the rastrillo new layout: exactly one directory under cmd/. Useful for rapid iteration: edits to actions/ require regeneration (the binary uses generated code under gen/), and dev does that automatically. It also warns — never generates — when an app's models have outrun its migrations.

  • rastrillo migration <cmd> [dir] — schema changes on the GORM path: generate diffs models.go against the migrations and writes the delta, check is the CI gate that fails when the two disagree, new stubs a hand-written migration, status --db reports what a real database has applied, and baseline --db is the operator's escape hatch for a deployed database Apply refused to adopt. generate and check need no database at all — both sides of the diff are computed in memory.

  • rastrillo.Run — the process entrypoint the scaffold wires up: it resolves whichever of the platform's two activation argv shapes the binary was invoked with — -socket/-addr/-db flags for an agent exec child (hibernate routes), or a bare serve subcommand with no flags for a carlos-app@.service unit tenant — then calls Serve. A relative -db/Options.DBPath is resolved inside $STATE_DIRECTORY when systemd provides one, since a unit tenant's cwd isn't its state dir. Hibernation requires nothing else from the app: the activator owns the restore/replicate cycle, and Serve's SIGTERM drain fits inside its SIGKILL budget. rastrillo.Resolve is the same resolution without the serving, for apps that need the resolved invocation before doing anything else with it — e.g. one that wants the resolved DBPath without going through Options.Router.

  • rastrillo.Serve — the bootstrap (design doc §5): the SQLite pragma-ordering fix, SetMaxOpenConns(1), additive migrations; the platform's activation contract (Options.Socket/Options.Addr/systemd LISTEN_FDS, matching carlosframework/platform's testdata/echoapp exactly); GET /healthz and GET /api/version answered automatically. An app that keeps its database in Ctx sets Options.Router instead of Options.Mux and is handed the *sql.DB Serve opened; rastrillo.OpenDB is the same corrected opener exported for tests. Options.Wrap is the app-middleware seam: it wraps the app's mux (sessions, CSRF, panic pages, authorization) inside the framework's chrome — GET /healthz, GET /api/version, and locale-prefix stripping stay outside it, so probes never traverse app middleware and middleware sees the same paths routes match on. Between Serve and Run, the activation contract is covered end to end: every route kind the platform runs — always-on instance, hibernating exec child, unit tenant — boots the same scaffolded app.

  • Localization (design doc §10) — Options.Locales/DefaultLocale/ LocaleFS declare an app's locale set and supply its catalogs from an embed.FS carrying locales/<code>.toml (flat key = "value" TOML). Each request resolves a locale in order: URL path prefix (stripped before the app's mux sees it, so /fr/orders and /orders reach the same route), then Accept-Language (q-ordered, so a browser sending fr-CA matches a declared fr), then the rastrillo_locale cookie, then the default. Actions call request-scoped rastrillo.T(r, key) / Tf(r, key, args...) ({name} interpolation) for translated strings; lookup falls back through the requested locale's catalog, the default locale's catalog, the framework's base catalog, and finally the key itself — a missing translation stays visible on the page, never blank. The framework base catalog (rastrillo.BaseCatalog(), wired into every Served app's Locales automatically) carries rastrillo/ui's own rastrillo.ui.* strings, so a single-locale app gets correctly-worded built-in components without writing a catalog of its own; an app catalog entry for the same key still wins. rastrillo generate --check [--default-locale <code>] fails loudly when a non-default catalog is missing keys the default has (§10's "silent fallback while iterating, loud failure before ship"); that gate runs under --check only — plain rastrillo generate (and so rastrillo dev and rastrillo new) never fails on an incomplete catalog. --check's --default-locale flag defaults to en and is not read from Options.DefaultLocale — if an app sets a different DefaultLocale, pass the matching --default-locale by hand or the check compares against the wrong catalog. Nothing writes the rastrillo_locale cookie yet; persisting a user's locale choice across requests is the app's job for now. Two honest caveats: an app that declares locale en can't serve an app route whose first path segment is also en — inherent to prefix routing, not a bug to fix; and a ServeMux trailing-slash redirect issued under a locale prefix currently emits the unprefixed path, dropping the locale on that one redirect (known limitation).

  • rastrillo/ui — the component/UI vocabulary (design doc's List screens plus the display, form, and route families): badges, meters, person cells, callouts, fields, choice cards, toggle blocks, seg-tabs, confirm forms, bulk select, modal shells, and the rest of the List screen set — with framework strings resolved through the §10 locale chain. An app registers ui.Funcs() (dict, list, icon, T) on its own template tree and ParseFSs ui.Templates() alongside its own templates; ui.TokensCSS() is the design-token stylesheet rastrillo new writes once into a new app's static/ directory, app- owned from then on. T resolves a partial's own hardcoded-English default (e.g. pagination's "Pagination", confirm-form's "Cancel") through the framework base catalog — a caller-supplied value always wins over it — and ui.FuncsWith lets an app rebind T to a request-scoped rastrillo.T lookup so those defaults resolve in the request's locale instead. See ui's package doc for the full class idiom vocabulary (list grid, dropdown, filter tokens, help tooltip, selection checkbox) that isn't a Go template partial.

  • examples/helloworld — a real scaffolded app, checked in, proven to ship/promote/serve through the actual carlos binary — see hack/local-deploy-demo.sh.

  • Manifests (design doc §9) — declare a rastrillo.Resource and rastrillo generate builds its store, its four screens' worth of actions and templates, and their locale keys, with sqlc query colocation for the store. See "Manifests" below.

  • examples/blog — a whole app built from stock parts: ui's partials, no JavaScript, and (new on this branch) a manifest-declared resource adopted alongside hand-written actions and ejected templates — see examples/blog/README.md.

  • examples/tickets — the fully generated proof: one manifest resource, zero hand actions, zero ejected templates — see examples/tickets/README.md.

  • rastrillo/crypto — the family envelope: ECDH P-256 ephemeral → HKDF-SHA256 → AES-256-GCM sealing (ephPub(65) ‖ iv(12) ‖ ct), ECDSA raw r‖s signing over SHA-256(context ‖ 0x00 ‖ msg), the symmetric half (Derive/SealSym/OpenSym), keypair marshalling, and a WebCrypto JS twin (crypto.JS()), all proven against amadan's pinned golden vectors — the compatibility contract that lets amadan, seapointish and keymail delete their local copies.

  • rastrillo/keyring — the E2EE seed lifecycle over crypto's primitives: one 32-byte seed per person, Ring-namespaced purpose derivation (ns/prf/v1, ns/content/v1, ns/wrap/v1, ns/grant/v1Ring{"kass"} reproduces kass's bytes exactly), the seed wrapped under a passkey's PRF output (WrapSeed/UnwrapSeed), content keys granted to members' box keys (Grant/OpenGrant), the last-wrap-is-unrevokable guard (AddWrap/RemoveWrap), and a WebCrypto JS twin (keyring.JS()) that imports ./crypto.mjs — serve both as siblings. No storage and no new cryptography: tables belong to the app, ciphers to crypto, and keyring/testdata/golden.json is hash-pinned and replayed in both languages. The app contract the package names but does not build: store a wrapped seed keyed by credential ID, return it at sign-in, accept a new one at enrol. An RPID move is a three-phase drill — old name; crossover with webauthn.Config.LegacyRPID set while fresh sign-ins re-wrap the same seed under new-RPID credentials; settled, LegacyRPID removed — and WrapSeed is the whole mechanism.

  • rastrillo/gormlite — a GORM SQLite dialector over modernc.org/sqlite, a minimal fork of glebarez/sqlite that keeps Rastrillo on current modernc without a double driver registration.

  • rastrillo/db — opens the app's SQLite database as one *gorm.DB with the pragma order OpenDB already got right, split into a writer pool capped at one connection and a multi-connection reader pool, routed transparently by dbresolver.

  • rastrillo/migrate — one ledgered schema mechanism for the GORM path, replacing AutoMigrate plus per-package raw SQL: ordered, namespaced Sets applied once each at boot, each inside its own BEGIN IMMEDIATE with its ledger row, on a pinned connection so a table rebuild can bracket PRAGMA foreign_keys. A database that predates the ledger is adopted — stamped, zero DDL run — or refused loudly with a structural diff; it is never silently migrated. Migrations are immutable once applied and forward-only: production rollback here is the platform restoring the SQLite file, not a Down function.

  • rastrillo/sessions — the SQLite-backed session core: signed-in sessions as real rows (sign-out and admin revocation both work), __Host- cookies on https origins, and the request-context surface (Current, UserID) every identity plugin calls SignIn into.

  • rastrillo/csrfProtect, a same-origin middleware for state-changing requests, checked in order of evidence quality: Sec-Fetch-Site, then Origin, then Referer.

  • rastrillo/flash — one-shot notice messages carried in an HTTP cookie and cleared once read; display state, not a record.

  • rastrillo/form — the framework-independent form helpers a generated handler needs: money parsing/formatting and a field error map, shared by generated and hand-written handlers alike.

  • rastrillo/view — the plain HTTP-response helpers a generated action needs against a *rastrillo.Ctx: Render, Fail (safe 500s, logged), and ParseID for the {id} path value.

  • rastrillo/scopeOwned/OwnedBy, the GORM query scopes that make per-user ownership the short query: a row that isn't yours is a row that doesn't exist, so handlers answer 404, never 403.

  • rastrillo/jobs — background work you can watch: Start runs a function in a goroutine and hands back an id, and NewHandlers turns that id into two routes the app mounts behind sessions.Require and renders with its own callbacks — a status page at /jobs/{id} and the fragment it polls at /jobs/{id}/fragment. Ownership is the session subject, so someone else's job, like someone else's row, 404s. The status page works with scripts off: while the job runs it carries a <noscript> meta refresh, and a finished job with somewhere to go answers 303 (the fragment's equivalent is 204 plus a Rastrillo-Location header). The registry is in-memory on purpose — a restart kills the goroutine, so a stored row would only persist a lie; work that must survive one belongs in eventlog. It is bounded, too: an owner holds at most four running jobs (Start answers ErrOwnerBusy past that), and a job still running after fifteen minutes is marked failed, its context expired. The only JavaScript in the framework is static/rastrillo.js, an app-owned shim rastrillo new writes beside tokens.css: it replaces an element carrying data-poll with the HTML fragment it fetches and stops when the new fragment stops asking, and gives every submit button a spinner and a double-submit guard while its form is out (data-busy="false" opts out). Status pages poll, or ride Server-Sent Events where the browser supports them: Events streams at /jobs/{id}/events (heartbeats, per-write deadlines, a bounded stream lifetime — the serve.go streaming recipe), and the shim's data-poll-push upgrades to an EventSource that falls back to plain polling on its own. htmx remains a choice, not a dependency — examples/notes demonstrates the whole loop with an Export flow.

  • rastrillo/password — an email+password identity plugin on the sessions core, the same one-call SignIn contract auth's keymail flow honors, leaving storage, rendering, and CSRF to the app.

  • rastrillo/auth — the family-default identity plugin on the sessions core: magic-link email sign-in that auto-upgrades to the keymail ceremony when the address has a claimed inbox (classification fails open, so every address always works), wrapping keymaildev/signin the way seapointish's reviewed integration does — explicit rate limiter, single-use links via DELETE … RETURNING, real session revocation, same-origin CSRF on every state-changing handler, an Authorize admission hook, and RequireFreshSession for step-up on sensitive routes.

  • rastrillo/passkey — the WebAuthn second factor, both places it belongs: on the step-up seam (a signed-in user enrolls, and a valid-but-stale session is made fresh again by an assertion instead of a full re-sign-in) and at first sign-in (Gate, wired into a plugin's SecondFactor hook: a verified first factor becomes a pending half-session that only an assertion completes — the session it mints names both factors, "magiclink+passkey"). Single-use server-side challenges and half-sessions, subject-bound, over rastrillo/webauthn's ceremonies and browser module. And the escape hatch: ten single-use recovery codes, minted behind RequireFresh and shown once, redeem at the gate by plain form POST — no JavaScript, because a lost passkey is exactly when WebAuthn isn't working — minting "magiclink+recovery" so the app can nudge re-enrollment. Sign-in only: step-up still takes a real assertion.

  • rastrillo/webauthn — the passkey identity half, lifted from kass tests-and-all: ES256 only, no attestation checking, the CBOR subset reader, LegacyRPID for hostname moves, plus the authtest fake authenticator as a public sub-package and the browser half as an embedded ES module (webauthn.JS()).

  • rastrillo/eventlog — the Mergeable store shape: append-only per-resource streams (many single-writer streams), a pure generic Derive fold, idempotent Ingest as the platform transport's seam, and a deterministic default merge order pinned by JSON vectors.

  • rastrillo/blobs — content-addressed bytes: S3FromEnv() over the platform's object-storage primitive (CARLOS_STORE_*), a hand-rolled SigV4 signer + presigned GET/PUT pinned against the official AWS vectors, Dir and Inline backends (with the 4 KiB rule: bigger belongs in the object store), and Sealed() for E2EE.

  • rastrillo/mail — the one outbound-email surface (SMTP or loudly-logged fallback, header-injection refused), signature- compatible with signin's Mailer.

  • rastrillo/carlos — scheduled work on a hibernating instance (platform spec 2026-08-23-scheduled-work §7): Tick(r) authenticates a platform tick against $CARLOS_ADMIN_TOKEN in constant time, TickOccurrence(r) hands back the at-least-once dedupe key, and ScheduleAt/ScheduleCancel register one-shot timers over the instance's control socket. Degrades to ErrNotOnCarlos off-platform.

  • Agents (design doc §8) — actions opt in as tools (var Tool = rastrillo.Tool{...}), the generator emits the registry (gen.Tools()), the tools package renders schemas and dispatches registry-validated, consent-gated, actor-attributed calls through the same mux; Options.Sidecar + the sidecar run argv speak the platform's sidecar contract, and Options.NextDue answers the activator's GET /api/next-due scheduled-wake poll.

  • Serve seamsOptions.Wrap (the one middleware seam), exported rastrillo.Handler (Serve minus the listener, for test harnesses), and the scaffold's host awareness: a Makefile ci gate, executable .amadan/ci + .amadan/ci.d/ steps delegating to it, an empty manifest/ with a README, and a CLAUDE.md preload (§12).

Not built yet, honestly: the mergeable store's transport (edge sync is the platform's designed territory — eventlog.Ingest is the seam it will call, and until it lands, generated mergeable ids stay writer-local and every generated event's actor is "app"); automatic manifest-diff ALTER emission (generated stores emit only the initial CREATE; evolving a declared resource's schema is the app's own rastrillo migration new — and on the GORM path, reshaping a manifest after an app's first boot trips the ledger's immutability guard, which migration baseline is the way out of); richer manifest kinds beyond text/textarea/money (Bool, Time, Select and Blob arrive as manifest slices); and any LLM client (§8 leaves the provider per app).

A known implementation decision worth flagging

The design doc's routing example puts multiple action files in the same directory (actions/orders/[id]/cancel.POST.go next to edit.GET.go) — but Go compiles one package per directory, so both files sharing a bare func Handle would collide. The generator resolves this by never compiling actions/ in place: each file is parsed, its package clause rewritten to a name unique to its route, and the result written into its own directory under gen/actions/. Normal Go imports, no AST surgery beyond the package clause. A future version could instead lift just the Handle function via full AST extraction to get closer to "one file, no package boilerplate," but that's real complexity, deliberately deferred rather than rushed — see internal/generate/generate.go's package doc.

Manifests

Manifests are the declarative path — an optional, equal alternative to hand-written handlers, not a requirement and not a legacy mode. The two paths live side by side in one app, per resource: declare the screens that are pure CRUD, hand-write the ones that aren't, and move a resource between the paths whenever its needs change (eject one generated file, or delete your hand files and re-declare). Design doc §9's Resource sugar declares an entity once and rastrillo generate builds its store, its screens, and their locale keys — a CRUD surface for a fraction of the cost of writing each of those by hand, as readable committed code that composes the same form/view helpers a hand-written app uses.

Its vocabulary today is honestly scoped: one flat resource, three field kinds (text, textarea, money), no relations. It reaches where the code path does on ownership: scope = "user" makes every generated query owner-filtered by the session subject — someone else's row answers 404, the scope package's discipline, declared instead of hand-written (examples/notes runs both halves side by side and proves them with one two-user suite). Relations and custom flows still take the code path; that boundary is where the generator currently stops, not where it is fated to stop. Drop a manifest in manifest/posts.toml when a resource fits the declared shape:

name  = "posts"
route = "/admin/posts"
store = "exclusive"

[list]
columns = [{ field = "Title" }, { field = "Status" }]
search  = true

[[list.filters]]
field  = "Status"
values = ["draft", "published"]

[form]
basics = [{ name = "Title", required = true }, { name = "Body", kind = "textarea" }]

(or build the same rastrillo.Resource value in manifest/*.go — a typed alternative for a shape TOML can't express, evaluated with go run against the app's own module, for when a resource's shape wants to be computed rather than declared literally) and rastrillo generate produces, per resource:

  • A storegen/store/<name>/: schema.sql/queries.sql (sqlc's own input, colocated per resource) plus migrations.go (the same table as CREATE TABLE IF NOT EXISTS, for Options.Migrations). Generation runs go tool sqlc generate against that input, so an app adopting a manifest must add the tool directive once: go get -tool github.com/sqlc-dev/sqlc/cmd/sqlc.
  • Actions for the four canonical states plus the delete flow — list, show, new+create, edit (basics, plus advanced when the manifest declares [form] advanced fields), and delete as its own confirm-page URL: GET <route>/{id}/delete renders the question (a GET never mutates), only the sibling POST deletes — written straight into gen/actions/, compiled normally: unlike a hand action under actions/, a manifest's action files never pass through the filesystem router's own Discover/Rewrite step, so they carry no //go:build tag. Each hands its page to the app's own template tree through Ctx.Render — the one seam generated code needs, since it cannot call an app-private helper like a hand-rolled blog.Render. Page names are always <resource>/list, <resource>/show, <resource>/form or <resource>/confirm, regardless of which of the (up to) nine action files is rendering.
  • Templatesgen/templates/<name>/{list,show,form,confirm}.html, composed entirely from the ui package's partials. list.html is gated on search at generation time: a resource with search = false gets no search box at all. A [[list.filters]] entry declares a filterable field and a set of enumerable values (e.g. field = "Status" with values = ["draft", "published"]): the generated list renders a dropdown control that filters by value and composes with search and pagination. Each filter value becomes a translation key resource.<name>.filter.<field>.<value>, plus ui.all for the all-items state. The bare filter field (superseded) validates but generates no control.
  • Locale keysgen/locales/<default>.toml (for humans/ translators) and gen/locales/locales.go (a generated BaseCatalog var, wired as Options.BaseCatalog) carry a title-cased fallback label for every field and screen a resource declares, from one source map so the two files cannot drift — layered underneath whatever catalog the app supplies.
  • gen/manifest.json — the whole resource set as one stable JSON artifact (sorted by name, two-space indent, evolution additive-only), for any future renderer or tool that wants a resource's shape without parsing TOML or running Go.

Eject a template or action file, or skip generating one at all. A hand-written file already sitting at the exact path generation would compute — templates/<name>/list.html, or actions/<route path>/index.GET.go — is left alone: the generator writes nothing there. That is the whole ejection story: copy a generated file's own content out to its hand path (each file's header names the exact path to copy to), and generation of that one file stops there; every other file for that resource keeps regenerating normally. A route claimed by two sources — hand and generated, or two resources whose computed paths collide — fails the build loudly, the same as a filesystem-router collision. rastrillo generate --check runs the whole pipeline into a scratch directory and diffs it against the committed gen/, catching both a stale/hand-edited generated file (idempotency) and a collision, without writing anything.

Filters — at most one [[list.filters]] entry per resource. Field values are validated at generation time (must name a declared list column); each declared value must be non-empty, match ^[a-z0-9_-]+$, and appear only once — they travel in URLs and double as translation keys, so they can't be arbitrary text. A filter's selection persists across search and pagination (carried in the generated hrefs); the dropdown's own open/closed <details> state does not survive navigation.

Required fieldsrequired = true on a form field marker adds a client-side required attribute via the field partial AND generates server-side validation: a blank submission re-renders the form with a 400 status and the field's own error message (e.g. "Title is required"). A Money field marked required = true still accepts "0" as valid — the field must be present and parseable, not necessarily non-zero.

Manifest-only apps — a resource need not coexist with hand actions. An app with only declared resources (and no actions/ or templates/ directory at all) is legal: rastrillo generate produces the whole store, all seven actions, and every template, compiled normally, and the app runs without any hand-written route or screen handlers.

Migrations — the generated migrations.go emits CREATE TABLE IF NOT EXISTS. A fresh database runs the generated migration and works out of the box. An existing database that predates a manifest field addition needs an app-owned additive migration (e.g. ALTER TABLE posts ADD COLUMN status TEXT): manifest edits regenerate code and migrations, but the generated migration stays idempotent (IF NOT EXISTS) — schema evolution is the app's own work (roadmap: automatic manifest-diff ALTER emission). examples/blog shows the pattern: the generated CREATE TABLE IF NOT EXISTS posts runs first, then the app's own ALTER TABLE posts ADD COLUMN published BOOLEAN runs after. This is all specific to the legacy Options.Migrations + OpenDB path; an app on the GORM path (db.Open) manages schema through the migrate package instead — see SKILL.md.

store = "mergeable" generates too (v0.16.0): the same screens over an eventlog-backed store — each record one stream, deletes appended as tombstones, reads derived by replaying the merged history — examples/tickets' announcements resource is the generated proof, tombstone test included. examples/blog shows what an app adds by hand to cover what a manifest doesn't generate; examples/tickets is the fully generated proof (one manifest resource, no hand actions or templates).

Release builds

A scaffolded app's Makefile has two build targets, doing different jobs:

  • make build — the compile check. go build ./... with more than one package matched discards its output, so this catches a broken package without producing an artifact.
  • make release — what ships: -ldflags="-s -w", dropping the symbol table and DWARF, into releases/<app>-<goos>-<goarch>, stamped with the version the built binary will report.

release cross-compiles for linux/arm64 by default, not for your own machine, because that is what carlos ship -target defaults to. Building a release on an amd64 laptop and shipping it is a silent architecture mismatch — the upload succeeds and the binary fails to exec on the instance. Override for a one-off with make release RELEASE_GOARCH=amd64. The artifact is named for its architecture, and make release prints the matching carlos ship command.

releases/ is in the scaffolded .gitignore, along with the local SQLite database the app creates when you run it and its write-ahead log.

The version stamp

make release sets rastrillo.BuildVersion from git describe --tags --always --dirty, and that string is what GET /api/version reports from the running process.

It carries more weight than a version string usually does. carlos deploy verifies against the router's x-carlos-version header — the release the platform believes it adopted — while /api/version is what the process actually running on the instance says it is. Those are two different facts, and the deploy is only verified when they agree. An app that answers dev from every binary it has ever built cannot disagree with anything, so a process that was never recycled onto the new release verifies green. That happened, with carlos deploy printing live.

make release refuses rather than stamping something untrue. A version it cannot determine — no repository, no commit yet — and a dirty tree are both build failures. An empty stamp is worse than dev, which at least says something true about itself, and a -dirty stamp names a commit plus changes nobody can name, so two different binaries can carry the same one. If you mean to ship uncommitted work, say so out loud:

make release VERSION=v0.1.0-wip

make build is untouched: the compile check is not a build, and rastrillo dev neither stamps nor strips, because the dev loop exists to give you a binary you can debug.

Apps scaffolded before v0.23.0 have the old target and report dev forever. Re-scaffold, or copy the VERSION line and the version-check target across.

Stripping is worth having because the compressed artifact is what gets transferred. Measured across this family's own apps (titogo, amadan, platform, slopbox, keymail, and a fresh scaffold): 23-31% off the raw binary, and 45-53% off it after zstd -19. A fresh scaffold goes 21.3MB → 14.7MB raw, 10.6MB → 5.1MB compressed.

What survives stripping, checked rather than assumed:

  • Panic traces are byte-identical, function names and line numbers included — Go's pclntab is not touched by these flags.
  • debug.ReadBuildInfo() works.
  • go version -m still reports module metadata.

What you lose is source-level debugging with delve or gdb. Build without the flags when you need that.

rastrillo dev never strips, deliberately: a debuggable binary is what the dev loop is for. carlos itself is built the same way — see carlosframework/platform's own Makefile — so this closes a gap rather than setting a new policy.

Browser tests

Almost everything here is covered by ordinary Go tests. What is not — a real JS engine, real focus, a real authenticator — runs under one build tag, framework and scaffolded apps alike:

go test -tags browser ./...

Build-tagged, so a plain go test ./... never half-runs a browser and chromedp stays out of the ordinary build graph (go list -deps ./... pulls none of it — CI runs that sentence as a step). A Chromium is found on PATH, via RASTRILLO_CHROME, or in a Playwright cache. A skip is not a pass: with no browser the tagged tests fail, unless RASTRILLO_BROWSER_OPTIONAL=1 makes the skip a deliberate, visible choice. CI's browser job runs the harness and webauthn packages with a pinned Chromium on every PR; the ui select drive stays a deliberate local run while issue #86 (its Enter reaches the form on that runner, nowhere else) is open.

Three packages carry the tag:

  • ui/field-select's searchable enhancement gets a single chromedp drive of the whole journey — render, enhance, filter, keyboard-select, mirror back, submit — asserting the server received the value a user picked. Its assertions are written as the bug classes they catch, and each was verified by breaking the script on purpose and watching the test fail — including the one that found a real bug during development, where the filter box kept the committed label so typing appended to it, matched nothing, and silently committed the pre-existing value.
  • harness/ — the browser rig itself, the library those drives are built on: harness.New binds a localhost listener first, hands the app its origin (http://localhost:PORT — an IP is not a WebAuthn RP ID), then launches a Chromium with a CDP virtual authenticator, PRF included. Watchers turn every console error, failed request and 4xx/5xx into a test failure (rig.Allow excuses expected probes); rig.Screen gates each screen behind a junk scan of its text, input values and aria-labels for undefined, null, [object Object], NaN — the bug class that renders perfectly and says nothing.
  • webauthn/ — real ceremonies against the virtual authenticator: enrolment, PRF at creation, sign-in, and the two-prompt prfByAssertion fallback, forced by harness.WithoutPRFAtCreation() because the CDP authenticator cannot withhold PRF at creation on its own.

rastrillo new scaffolds internal/<pkg>test/browser_test.go on the same tag — the minimal loud walk, boots the whole app through harness.New and grows with it.

chromedp is pinned to v0.14.2 rather than the latest: newer releases require Go 1.26, and a test-only dependency should not raise the module's Go floor for everyone who imports rastrillo.

Parity vectors

Any derivation over sealed content runs client-side, but the sidecar, operator tools and tests want the same derivation in Go — so the engine exists twice, and two engines drifting is the most dangerous E2EE bug class: a wrong answer with nothing looking broken. rastrillo vectors promotes kass's golden-vector discipline to a verb:

rastrillo vectors -init    # once: scaffold cmd/genvectors, test/parity.test.mjs,
                           # test/vectors.mjs and the go-test belt
rastrillo vectors          # regenerate test/vectors.json from the app's Go engine
rastrillo vectors -check   # pre-ship gate: regenerate + byte-compare, then
                           # node --test test/parity.test.mjs

The app's cmd/genvectors enumerates cases through vectors.New() / Add(name, why, fields) / WriteTo — every vector names the rule it pins, and the JS test titles become name — why. Two treaty rules ride the file: the field key names are shared with the JS suite by name (change both sides in the same commit; nothing mechanical checks they agree), and time.Time round-trips as RFC 3339 → new Date(v.now).

The comparison rule (canonical() in the vendored test/vectors.mjs) sorts keys, drops null/undefined members and drops scalar zeros (0, false, "") to match Go's omitempty — deliberately, blind spot included: a meaningful explicit zero on one side and a missing field on the other compare equal. The scaffolded suite covers that hole with a marked belt section of explicit-value assertions; keep it fed as vectors accrue. Nil normalisation is top-level only: a nil slice or map directly in fields writes as []/{}, while inner shapes stay the app's own discipline.

Vectors are opt-in: no cmd/genvectors, no gate. When the generator exists, rastrillo generate -check runs the vectors check too — one gate before ship, not two to remember. In -check a missing node is a failure, not a skip; the scaffolded go test belt skips without node so ordinary builds stay green and honest. There is deliberately no hash-pin export: app vectors change by design, and the byte-compare catches regenerate-drift strictly better than a hash an author would just update.

Try it

go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest
rastrillo new myapp
cd myapp && go mod tidy && rastrillo dev

Tags predating the move to amadan.net are history, not installable versions here: their own go.mod still names the old module path, so the proxy serves only tags cut after the move.

Then edit an action, save, refresh — rastrillo dev regenerates, rebuilds, and restarts for you. For a one-off build without the watch loop: go build ./cmd/myapp && ./myapp -addr :8080.

Or via Homebrew: brew install carlosframework/tap/rastrillo.

To see it actually deployed through the real platform binary (local directory store + local registry + carlos edge -dev, no AWS/SSH required):

PLATFORM_REPO=/path/to/carlosframework/platform hack/local-deploy-demo.sh

Live

https://hello.bdf.oncarlos.comexamples/helloworld, deployed for real on the CARLOS flagship: a real S3-backed deployment bucket, a real carlos edge, a real Let's Encrypt certificate — not the local-directory demo above. It runs as a hibernating instance (rastrillo.Run speaking the activation contract), wakes on the first request, and /api/version reports the exact rastrillo commit it was built from. The old helloworld.dev.oncarlos.com host belonged to the retired platform-dev environment and no longer resolves to a registered route. App hostnames live under oncarlos.com; carlosframework.com is reserved for platform surfaces.

See also

carlosframework.com for the architecture rastrillo builds apps on top of, and carlosframework/skills for the Claude Code skill capturing the family's conventions — including, after this framework's first pieces landed, which of those conventions rastrillo now enforces mechanically rather than asks you to remember.

Licence

Rastrillo is licensed under the Mozilla Public License 2.0. The full text is in LICENSE.

MPL-2.0 is file-level copyleft, which is the property that suits a framework: an app that imports Rastrillo is unaffected and may carry any licence you like, while changes made to Rastrillo's own files are expected to be published.

Two parts of this repository are not covered by that licence:

  • gormlite/ is MIT, not MPL. It is a minimal fork of github.com/glebarez/sqlite v1.11.0 and keeps the upstream copyright in gormlite/LICENSE.
  • Icons carry their vendors' terms. --icons=font-awesome means Font Awesome Free and writes the CC BY 4.0 attribution its licence requires into the generated package; that obligation belongs to the app, not to the framework.

Addons live in their own repositories on their own release schedules and set their own licences; amadan.net/rastrillo/idear does not inherit this one.

Copyright (c) 2026 The Rastrillo authors.

Documentation

Overview

The design-system gallery is generated and is not committed: it is 20 MB of machine output, rewritten whole every time a ui partial changes, and the website that publishes it builds it instead by running cmd/dsgen against a pinned version of this module.

`go generate ./...` writes a copy into .design-system/, which is git-ignored. It is there to be looked at — open a page, diff two runs, see what a change to a partial did to every page that renders it — and nothing reads it back. Deleting it costs nothing.

Package rastrillo is the CARLOS web framework — the shape of a CARLOS app, the way carlosframework/platform is the shape of the deployment substrate it runs on. See the design doc for the full picture: https://github.com/carlosframework/platform/blob/main/docs/superpowers/specs/2026-08-01-carlos-framework-design.md

The root package holds the process shape (Run/Serve/Handler, the activation contract, the SQLite opener, migrations), the action vocabulary (Ctx, Actor), the manifest vocabulary (Resource, Tool), fingerprinted assets, and localization. The subsystems live beside it: crypto (the family envelope), auth (keymail sign-in with the magic-link fallback), webauthn, eventlog (the Mergeable store), blobs, mail, carlos (the platform's scheduled-work contract), tools (agent dispatch), and ui (the component partials). README.md keeps the honest status list.

Index

Constants

View Source
const LocaleCookie = "rastrillo_locale"

LocaleCookie is the stored-preference cookie the resolution chain consults right after the path prefix, before Accept-Language. Design doc §10 names "a stored preference" without naming the mechanism; a cookie is the only one that survives §9's zero-JS baseline.

View Source
const LocaleSwitchPath = "/_locale"

LocaleSwitchPath is the framework route the language switcher POSTs to (spec §2.4). Mounted by Serve whenever Options.Locales is set.

Variables

View Source
var BuildVersion = "dev"

BuildVersion is what GET /api/version reports. The scaffolded Makefile's release target stamps it with -X from git describe, and refuses to build rather than stamping an empty string or a dirty tree — see makefileTemplate in cmd/rastrillo/new.go.

The platform's deploy verification polls GET /api/version on every instance socket — see blueprint.md, "The carlos core": "every instance must also serve GET /api/version reporting its build sha."

Why the stamp is the point rather than a nicety: carlos deploy checks the router's x-carlos-version header, which is the release the platform believes it ADOPTED. /api/version is what the process RUNNING on the instance says it is. They are two different facts, and the deploy is only verified when they agree. A binary that answers "dev" from every build ever made cannot disagree with anything, so a process that was never recycled onto the new release verifies green — which happened, to a real app, with deploy printing "live". Every app scaffolded before v0.23.0 has that hole in it; a re-scaffolded Makefile, or the two lines from it, closes it.

"dev" stays the default on purpose. rastrillo dev and a plain go build do not stamp, and a binary that says "dev" is saying something true about itself.

Functions

func BaseCatalogs

func BaseCatalogs() map[string]Catalog

BaseCatalogs returns a copy of every shipped catalog, keyed by locale code exactly as declared in BaseLocales.

func BaseKeys

func BaseKeys() []string

BaseKeys returns the sorted rastrillo.ui.* key set — what an app declaring a locale the framework does not ship has to translate before `rastrillo generate --check` passes (spec §3.4).

func BaseLocales

func BaseLocales() []string

BaseLocales returns the shipped locale codes, en first.

func Dir

func Dir(locale string) string

Dir is the HTML dir attribute for a locale: "rtl" for the right-to-left scripts a rastrillo app can declare, "ltr" otherwise. Decided on the primary subtag, so ar-EG mirrors as ar does.

func Handler

func Handler(opts Options) (http.Handler, func() error, error)

Handler is everything Serve builds short of the listener and the signal handling: it opens the database (if configured), applies migrations, resolves the Mux/Router choice, and assembles the full serving handler — framework endpoints, Wrap, locales and all. The returned close func releases the database handle (a no-op without one).

Exported for test harnesses: before this seam, every app's harness hand-duplicated /healthz, /api/version and the DSN pragma ordering because Serve blocks on a real listener (vitogo's vitotest says so in its own comments; seapointish copied the same shape). Now a harness is httptest.NewServer around this.

func Icon

func Icon(slug string) template.HTML

Icon renders one vendored icon by its Lucide slug for use as an html/template FuncMap entry:

tmpl.Funcs(template.FuncMap{"icon": rastrillo.Icon})
// then, in the template: {{icon "check"}}

An unknown slug renders nothing rather than panicking a page mid-response -- a typo must cost a missing icon, not a crash.

func IconSlugs

func IconSlugs() []string

IconSlugs lists every slug Icon answers, sorted.

These names are rastrillo's own vocabulary, not any vendor's: five of the twelve differ from the names lucide.dev publishes. "kebab" is Lucide's ellipsis-vertical, and v1 renamed the other four (check-circle, alert-triangle, x-circle, help-circle). "menu" is NOT one of them — that is Lucide's own slug, and it is Font Awesome that calls the glyph "bars". internal/iconsets and docs/site/icons.md say the same five, and internal/iconsets.LucideName is where the mapping actually lives, read off the vendored glyph data so it cannot drift from it. An app scaffolded with a different set answers exactly this list too, which is what lets {{icon "search"}} mean the same thing everywhere and keeps the shipped ui/ partials set-agnostic.

Exported so tooling can check the two stay in step — internal/iconsets asserts every scaffoldable set covers all of it.

func IsBaseKey

func IsBaseKey(key string) bool

IsBaseKey reports whether key is one the framework ships.

func LocaleFrom

func LocaleFrom(r *http.Request) string

LocaleFrom returns the locale Middleware resolved for r, or "" if the request never went through it.

func NewRef

func NewRef() string

NewRef mints the short reference an error page shows and the log line carries: six lowercase base32 characters over 4 bytes of crypto/rand — 30 bits, enough that two errors in the same log window will not share one, and short enough that a person will actually quote it.

It is not a secret and not an id: nothing is stored under it. Its only job is to join what the user saw to what the operator grepped, which is why it appears in exactly two places — the page and the log.

func OpenDB

func OpenDB(path string, migrations []string) (*sql.DB, error)

OpenDB applies the SQLite convention the survey found hand-propagated, with fixes, repo to repo (design doc §5): busy_timeout set *before* journal_mode=WAL — the reverse order crashes with SQLITE_BUSY under concurrent open, titogo's real fix — then SetMaxOpenConns(1), then an eager ping so the file exists on disk from boot, then migrate.

Exported so tests and non-Serve contexts get the corrected opener instead of reproducing the DSN by hand (the blog's F4).

func Run

func Run(opts Options) error

Run is the process entrypoint for a rastrillo app: it resolves the platform's activation argv, then serves. The platform invokes an app binary in two shapes (see carlosframework/platform, internal/activator/backend_exec.go and internal/host/units/):

<binary> [-socket p] [-addr a] [-db p]  agent exec child — hibernate
                                        routes; the activator spawns
                                        `<live> --socket <s> --db <d>`
<binary> serve                          carlos-app@.service unit
                                        tenant — no flags; the listener
                                        arrives via LISTEN_FDS (fd 3)
                                        and state lives in
                                        $STATE_DIRECTORY
<binary> sidecar run                    the host's sidecar — spawned
                                        beside the instance when its
                                        sidecar env file exists; runs
                                        Options.Sidecar in a loop, no
                                        listener (design doc §8)

Flags override the corresponding Options fields. A relative Options.DBPath (or -db value) is resolved inside $STATE_DIRECTORY when systemd provides one — a unit tenant's cwd is not its state dir — so the same binary and the same Options work in a dev checkout, as an exec child, and as a unit tenant. Hibernation needs nothing further from the app: the activator owns the restore/replicate cycle, and Serve's SIGTERM drain (10s) fits inside the activator's 20s budget.

func Serve

func Serve(opts Options) error

Serve opens the database (if configured), applies migrations, resolves the platform's activation contract for a listener, and serves until the process receives SIGTERM/SIGINT. It always answers GET /healthz itself — the manifest/action layer never has to remember to.

Timeouts

The server bounds two things for every app: how long a client may take to send its request headers (Options.ReadHeaderTimeout) and how long an idle keep-alive connection is kept open (Options.IdleTimeout). Neither can interrupt an in-flight request, which is what makes them safe as defaults.

Nothing here bounds a peer that stalls PART-WAY through a request body or a response. That is deliberate: net/http's ReadTimeout and WriteTimeout are total deadlines, so using them for that would cut off slow-but-healthy clients — a large upload, a git pack, an SSE feed, a WebSocket — along with the stalled ones. They are available on Options for apps that genuinely have only short requests, and off otherwise.

An app that streams must therefore bound its own streaming span, with an idle deadline re-armed as bytes move:

rc := http.NewResponseController(w)
rc.SetWriteDeadline(time.Now().Add(idle)) // before each write

Set it on the side that can actually block. A handler copying from a subprocess pipe blocks on the READ of that pipe, not on the write to the client, and a write deadline never fires there — the pipe needs its own deadline (os.File supports one) and the child needs a process-group kill to reap grandchildren still holding it open. This is not hypothetical: it wedged a production app for 158 minutes on 2026-08-19.

func T

func T(r *http.Request, key string) string

T translates key in the request's resolved locale — the lookup an action calls. Outside a request that went through Middleware it returns the key verbatim rather than guessing a locale.

func Tf

func Tf(r *http.Request, key string, args ...any) string

Tf is T plus {name} placeholder interpolation. See (*Locales).Tf for the accepted argument forms.

func WithActor

func WithActor(r *http.Request, a Actor) *http.Request

WithActor stamps who is making this request onto its context — the tools dispatcher uses it so an agent call is attributed end to end. The generated router copies it onto Ctx.Actor after the app's ctxFactory runs, so an app factory that doesn't set Actor still gets honest attribution.

Types

type Access

type Access int

Access is what a tool may do — the registry's read/write split that drives §8's consent gating.

const (
	// ToolRead observes and never changes state.
	ToolRead Access = iota
	// ToolWrite changes state, and therefore requires a Confirm
	// sentence and an explicitly confirmed call — the same consent the
	// confirm page asks a human for.
	ToolWrite
)

func (Access) String

func (a Access) String() string

type Actor

type Actor struct {
	Human bool
	Name  string // empty for a human; the agent's name otherwise
}

Actor identifies who is calling an action: a human request or a named agent. See the design doc §8 — every action's caller is attributed, never anonymous, so audit trails can say who did what honestly.

func ActorFromContext

func ActorFromContext(ctx context.Context) (Actor, bool)

ActorFromContext reports the actor WithActor stamped, if any.

func (Actor) String

func (a Actor) String() string

String is the actor's audit-trail form: "human" or "agent:<name>" — the encoding eventlog stores on every appended event, so a stream always says who did what without importing this package.

type Assets

type Assets struct {
	// contains filtered or unexported fields
}

Assets fingerprints an app's static files so they can be cached forever and still update on an ordinary reload (see the assets + TDD-scaffold design doc). Path maps a file to a URL carrying its content hash; Handler serves that URL with an immutable Cache-Control. Because the hash changes whenever the content does, the HTML always links a URL the browser has never cached stale.

The FS is served with http.FileServerFS semantics — URL path = "/" + FS path — matching how the scaffold embeds static/ (assets.go's //go:embed static): NewAssets(app.StaticFS), mounted at "GET /static/" with no StripPrefix.

func NewAssets

func NewAssets(fsys fs.FS) *Assets

NewAssets wraps a file tree — the scaffold's embedded StaticFS, or os.DirFS for an app serving a live directory — in a content-hash registry.

func (*Assets) Handler

func (a *Assets) Handler() http.Handler

Handler serves the tree with the fingerprinting contract:

  • a hashed name matching the file's current content is immutable — Cache-Control: public, max-age=31536000, immutable — because that exact URL can never serve different bytes;
  • a hashed name that no longer matches (a stale page asking for an old version) serves the *current* content with no-cache: a slightly-stale stylesheet on a stale page beats a 404;
  • a bare name serves no-cache, so deep links keep working;
  • a real file whose name merely looks hashed wins over hash-stripping.

Mount it where the FS layout says — for the scaffold's embedded static/:

mux.Handle("GET /static/", assets.Handler())

func (*Assets) Path

func (a *Assets) Path(name string) string

Path maps an FS path to its currently-hashed absolute URL path:

Path("static/tokens.css") → "/static/tokens.d1e8a70b5ccab1dc.css"

A missing file returns "/" + name unchanged, so the 404 surfaces at request time — visible in the network tab — instead of a render-time panic.

type Catalog

type Catalog map[string]string

Catalog is one locale's flat key → string table (design doc §10).

func BaseCatalog

func BaseCatalog() Catalog

BaseCatalog returns a copy of the framework's English strings — the view every existing caller (serve.go, ui's defaultT) already relies on. A copy, so a caller's edits cannot reach the shared table.

type Column

type Column struct {
	Field string `json:"field" toml:"field"`
	Kind  Kind   `json:"kind" toml:"kind"` // zero value means Text
}

Column describes a column in a resource list.

type Ctx

type Ctx struct {
	DB     *sql.DB
	Logger *slog.Logger

	// Assets is the app's fingerprinted static-file registry, when
	// the app wires one — the scaffold does, over its embedded
	// static/ tree. Actions link assets by hashed URL:
	// ctx.Assets.Path("static/tokens.css"). Nil for an app that
	// serves assets some other way — the same contract as DB.
	Assets *Assets

	// Actor records who is calling this action (design doc §8).
	Actor Actor

	// Render is the manifest system's seam (design doc's manifest
	// slice): generated actions cannot call an app-private helper like
	// a hand-rolled blog.Render, so they call ctx.Render instead. The
	// app's ctx factory sets it (e.g. &rastrillo.Ctx{DB: db, Render:
	// blog.Render}); a generated action nil-checks it and answers a
	// logged 500 rather than a nil-pointer panic when an app forgets
	// to wire it. See RenderFunc and internal/generate's action
	// emitter for the exact page names a generated action calls it
	// with.
	Render RenderFunc

	// ErrorPage renders the app's own error page — the seam view.Fail,
	// view.NotFound and view.Forbidden call so that a failure inside a
	// generated action looks like the rest of the app instead of
	// net/http's bare text. Wire the same function to
	// Options.ErrorPage and a panic gets the identical page:
	//
	//	page := func(w http.ResponseWriter, r *http.Request, status int, ref string) {
	//		blog.RenderError(w, r, status, ref) // ui's "error-page" partial
	//	}
	//	// in the ctx factory: &rastrillo.Ctx{DB: db, ErrorPage: page}
	//	// in Options:        ErrorPage: page
	//
	// Nil is legal and is the default: the helpers answer plain text,
	// which is honest, ugly, and exactly what an app that has not
	// thought about its error pages should see.
	ErrorPage ErrorPageFunc
}

Ctx is passed to every action: the app's own wiring — its database, logger, asset registry, and the Render seam generated actions call through — built once by the app's ctxFactory. Per-request state doesn't live here: identity lives in sessions.Current(r) / sessions.UserID(r), and locale is rastrillo.LocaleFrom(r), both read straight off the request rather than staged onto Ctx.

type ErrorPageFunc

type ErrorPageFunc func(w http.ResponseWriter, r *http.Request, status int, ref string)

ErrorPageFunc renders an error response body: the app's own page, in its own shell, for a status the framework or a generated action reached rather than the app. ref is the NewRef the failure was logged under, empty for the statuses that have nothing to reference (404, 403). It is the type of both Ctx.ErrorPage and Options.ErrorPage — one shape, so an app writes the function once and wires it to both.

The callback owns the status code as well as the body: it must call WriteHeader(status) itself.

type Field

type Field struct {
	Name     string `json:"name" toml:"name"`
	Kind     Kind   `json:"kind" toml:"kind"` // zero value means Text
	Required bool   `json:"required" toml:"required"`
}

Field describes an input field in a form.

type Filter

type Filter struct {
	Field  string   `json:"field" toml:"field"`
	Values []string `json:"values" toml:"values"`
}

Filter specifies a column and a set of values for filtering a list.

type Form

type Form struct {
	Basics   []Field `json:"basics" toml:"basics"`
	Advanced []Field `json:"advanced" toml:"advanced"`
}

Form describes the form views for creating and editing a resource.

type Kind

type Kind string

Kind categorizes the input type for a column or form field.

const (
	Text     Kind = "text"
	Textarea Kind = "textarea"
	Money    Kind = "money"
)

type List

type List struct {
	Columns []Column `json:"columns" toml:"columns"`
	Search  bool     `json:"search" toml:"search"`
	Filter  []string `json:"filter" toml:"filter"` // superseded by Filters; still validated, generates the WHERE clause but no control.
	Filters []Filter `json:"filters" toml:"filters"`
}

List describes the table view for a resource.

type LocaleItem

type LocaleItem struct {
	Code    string
	Name    string
	Href    string
	Current bool
}

LocaleItem is one entry of the language switcher: the declared code, its autonym (rastrillo.ui.locale_name in that locale, or the code when no catalog names it), a plain link to the same path under that locale's prefix, and whether it is the request's locale.

func LocaleItems

func LocaleItems(r *http.Request) []LocaleItem

LocaleItems builds the switcher's data for r. Empty when the request never went through Middleware or the app declares one locale — the partial renders nothing for an empty list, so a one-locale app can call it unconditionally.

type Locales

type Locales struct {
	// contains filtered or unexported fields
}

Locales is an app's declared locale set, its own catalogs, and the framework's base English catalog underneath them.

Lookup is layered, in this order: the requested locale's app catalog, the default locale's app catalog, the framework's catalog for the requested locale, when it ships one, the base catalog, then the key itself. The middle layer is design doc §10's "missing keys fall back to the declared default locale during development"; the base layer is what lets a single-locale app get correctly-worded built-in components without writing a catalog at all. Returning the key — never "" — keeps a missing string visible on the page instead of silently blanking a sentence.

func NewLocales

func NewLocales(codes []string, def string, base Catalog, fsys fs.FS) (*Locales, error)

NewLocales validates the declared set and reads locales/<code>.toml out of fsys for each declared code. fsys may be nil (framework base catalog only). A declared locale with no catalog file is not an error: a single-locale app declares "en" and ships no locales/ directory.

func (*Locales) Codes

func (l *Locales) Codes() []string

Codes returns the declared locale codes in declaration order.

func (*Locales) Default

func (l *Locales) Default() string

Default returns the declared default locale.

func (*Locales) FrameworkHas

func (l *Locales) FrameworkHas(code string) bool

FrameworkHas reports whether the framework ships a base catalog for a declared code — matched exactly, so "zh" never finds "zh-Hans" (spec §3.3).

func (*Locales) Has

func (l *Locales) Has(code string) bool

Has reports whether code is one of the declared locales.

func (*Locales) Middleware

func (l *Locales) Middleware(next http.Handler) http.Handler

Middleware resolves this request's locale and puts it, and this set, on the request context for LocaleFrom/T/Tf.

Precedence: URL path prefix, then the stored-preference cookie, then Accept-Language, then the default. The original design doc (§10) put the cookie last; that was reversed on 2026-08-28 when the framework started writing the cookie itself (SwitchHandler) — a stored choice that Accept-Language could override on the next request would make the switcher decorative.

A matched locale prefix is stripped from the path before the app's mux sees it, so one route serves every locale. §10's zero-JS locale switch is "a plain link to the same path under a different locale prefix", which only works if /fr/orders and /orders reach the same handler.

func (*Locales) SwitchHandler

func (l *Locales) SwitchHandler() http.Handler

SwitchHandler answers POST /_locale: it stores the chosen locale in LocaleCookie and 303s to the return path under that locale's prefix. Same-origin is checked the way every mutating route in this framework checks it (csrf.SameOrigin), with the origin taken from the request itself — the handler has no configured origin and needs none, because the check is "did a page of ours submit this".

func (*Locales) T

func (l *Locales) T(locale, key string) string

T looks key up for locale, layered per this type's doc comment.

func (*Locales) Tf

func (l *Locales) Tf(locale, key string, args ...any) string

Tf is T plus {name} placeholder interpolation — design doc §10's `{{Tf "key" .Args}}`. args are slog-style alternating name/value pairs (the convention this repo already uses for key/value lists), or a single map[string]any / map[string]string, which is exactly what the doc's own .Args example passes.

type Options

type Options struct {
	// Mux is the app's router — normally gen/router.go's output (design
	// doc §4). Exactly one of Mux and Router must be set.
	Mux *http.ServeMux

	// Router, if set, builds the app's mux after the database opens:
	// Serve calls it with the *sql.DB opened from DBPath — pragmas,
	// eager ping, and Migrations already applied — and serves the mux
	// it returns. This is how an app puts the framework-opened handle
	// in its per-request Ctx without hand-copying the DSN (the blog's
	// friction log, F4):
	//
	//	Router: func(db *sql.DB) (*http.ServeMux, error) {
	//		return gen.Router(func(*http.Request) *rastrillo.Ctx {
	//			return &rastrillo.Ctx{DB: db, Logger: logger}
	//		}), nil
	//	},
	//
	// Exactly one of Mux and Router must be set. With DBPath empty,
	// Router is called with a nil db — an app without a database can
	// still defer its mux construction. Serve owns the handle and
	// closes it when Serve returns; do not retain it past that. An app
	// that needs a handle outside Serve's lifetime calls OpenDB itself.
	Router func(db *sql.DB) (*http.ServeMux, error)

	// Wrap, if set, wraps the app's mux — the one seam for app
	// middleware: sessions, CSRF, panic pages, authorization
	// (gleester's friction, James 2026-08-04; also the friction behind
	// amadan's outer-catch-all-mux workaround). It runs inside the
	// framework's chrome: GET /healthz and GET /api/version are
	// answered outside it (platform probes never traverse app
	// middleware), and locale-prefix stripping happens before it,
	// so middleware sees the same paths routes match on. Nil means
	// no wrapping. Returning nil is a boot error.
	Wrap func(http.Handler) http.Handler

	// DBPath, if set, opens a SQLite database with the pragma ordering
	// and connection settings the survey found hand-propagated,
	// error-prone, repo to repo (design doc §5): busy_timeout set
	// *before* journal_mode=WAL, then SetMaxOpenConns(1).
	DBPath string

	// Migrations are applied in order at boot, idempotently: each must
	// be safe to run against a database that already has it applied
	// (CREATE TABLE IF NOT EXISTS, or an ALTER whose "duplicate column"
	// error is ignored) — additive-only, per the family's hard-won rule.
	Migrations []string

	// Socket and Addr mirror the platform's activation contract (see
	// testdata/echoapp in carlosframework/platform): a unix socket path,
	// or a TCP host:port for local dev. If both are empty, Serve checks
	// for a systemd-activated listener (LISTEN_FDS) before falling back
	// to Addr ":8080".
	Socket string
	Addr   string

	// NextDue, if set, answers the platform's scheduled-wake poll: the
	// activator asks a running instance GET /api/next-due (bearer
	// $CARLOS_ADMIN_TOKEN) and hibernates knowing when to wake it —
	// carlosframework/platform internal/activator/backend_exec.go. The
	// returned time is the next moment the app has work; zero means
	// nothing scheduled. Unset, the route does not exist and the
	// activator treats the app as having no schedule (unit tenants
	// never get the poll at all).
	NextDue func() time.Time

	// Sidecar is the app's sidecar pass — the wake → read since
	// bookmark → decide → act loop's body (design doc §8). When the
	// platform spawns `<binary> sidecar run` (it does exactly that when
	// the host's sidecar env file exists), Run calls Sidecar in a loop:
	// each pass returns when it has caught up, reporting when it next
	// has scheduled work (zero: nothing scheduled — Run re-runs after
	// a default poll interval). A pass error is logged and retried with
	// backoff, never fatal: a sidecar outliving a flaky dependency is
	// the point of having one. SIGTERM/SIGINT cancels the context and
	// ends the loop. Nil with a `sidecar run` invocation is a loud
	// startup error, not a silent serve.
	Sidecar func(ctx context.Context) (time.Time, error)

	// CSP replaces the value of the Content-Security-Policy header the
	// framework sets on every response (see the package's default
	// below: same-origin everything, no inline styles but pow's
	// honeypot by hash, framing denied). Empty keeps the default. The
	// other baseline headers — nosniff, frame denial, referrer policy, a
	// one-year host-only HSTS — have no Options
	// field on purpose: all of them — this one included — are set
	// before any app code runs, so an app that wants different values
	// sets (or deletes) its own in a handler or Options.Wrap middleware
	// and simply wins.
	CSP string

	// Locales declares the app's locale codes (design doc §10) — the
	// catalogs LocaleFS carries as locales/<code>.toml. Empty means a
	// monolingual app: no locale middleware is installed and requests
	// pay nothing.
	Locales []string

	// DefaultLocale is the locale for unprefixed requests that match
	// nothing else, and the first fallback layer for missing keys.
	// Empty defaults to Locales[0].
	DefaultLocale string

	// LocaleFS provides the locales/<code>.toml catalog files —
	// normally an embed.FS rooted at the app directory. Nil is legal:
	// lookups fall back to the key itself, which keeps a missing
	// catalog visible instead of silently blank (§10).
	LocaleFS fs.FS

	// BaseCatalog optionally supplies a base catalog that sits UNDER
	// every app catalog (Locales' own doc comment: requested locale's
	// app catalog, then the default locale's app catalog, then this) —
	// normally the generated gen/locales/locales.go var BaseCatalog a
	// manifest resource's field labels and shared ui.* chrome strings
	// compile to (design doc §9's manifest system; internal/generate's
	// EmitLocales emits it from the same map as the human-readable
	// gen/locales/en.toml, so the two cannot drift). Nil is legal — an
	// app with no manifest resources has nothing to layer.
	BaseCatalog Catalog

	// Logger defaults to slog.Default() if nil.
	Logger *slog.Logger

	// ErrorPage renders the app's own error page for a failure the app
	// never saw: today, a panic that reached the framework's recovery
	// wrapper. Nil answers a plain "Something went wrong." — correct,
	// and ugly enough that most apps will want to set it.
	//
	// It is the same function an app puts on Ctx.ErrorPage, which is
	// what view.Fail/NotFound/Forbidden call, so a 500 from a handler
	// and a 500 from a panic look identical to the person reading it.
	// ui's error-page partial is the body; ref is the reference the
	// failure was logged under.
	//
	// It is called only when nothing has been written yet in the
	// common case; see recoverPanics for the mid-stream caveat.
	ErrorPage ErrorPageFunc

	// ReadHeaderTimeout bounds how long a client may take to send its
	// request headers. Zero uses defaultReadHeaderTimeout. This is the
	// slowloris bound: it costs a legitimate client nothing, because
	// headers are small and sent up front.
	ReadHeaderTimeout time.Duration

	// IdleTimeout bounds how long an idle keep-alive connection is kept
	// open between requests. Zero uses defaultIdleTimeout. It can never
	// interrupt an in-flight request — only a connection doing nothing.
	IdleTimeout time.Duration

	// ReadTimeout and WriteTimeout are OFF by default (zero), and an app
	// should think before setting them, because net/http applies them as
	// TOTAL deadlines measured from the start of the request — not idle
	// deadlines. A 40 MB upload over a slow link, a git pack streaming
	// for minutes, a Server-Sent Events feed and a WebSocket are all
	// legitimate and all unbounded in duration, and any of them is cut
	// mid-flight by a total deadline no matter how healthy the peer is.
	//
	// Set these only for an app whose every request is known to be short
	// (a JSON API with small bodies, say). To bound a STALLED peer on a
	// long-lived request without capping a slow-but-healthy one, the tool
	// is a per-handler idle deadline via http.ResponseController's
	// SetReadDeadline/SetWriteDeadline, re-armed as bytes move — not
	// these fields. See the package docs on Serve.
	ReadTimeout  time.Duration
	WriteTimeout time.Duration
}

Options configures Serve.

func Resolve

func Resolve(opts Options) (Options, error)

Resolve applies the platform's activation argv and environment to opts — everything Run does short of serving — and returns the result. Most apps just call Run; Resolve is the seam for the ones that need the resolved invocation first. The original motivating case — an app that opens its own database before building its mux — is better served by Options.Router now, which hands back the *sql.DB Serve opened; Resolve remains for apps that need the resolved paths themselves. If you do open your own handle and blank DBPath, the boot-materialization duty transfers with it: touch the driver (a Ping, or a migration) before Serve, or a hibernate route's activator replicates a file that does not exist.

type RenderFunc

type RenderFunc func(ctx *Ctx, w http.ResponseWriter, page string, status int, data any)

RenderFunc is how a generated action hands a page to the app's own template tree — the seam generated code needs because it cannot call an app-private helper (a hand-rolled blog.Render, say). Ctx.Render carries it; the app's ctx factory sets it, and a generated action nil-checks it before use. page is always one of "<resource>/list", "<resource>/show" or "<resource>/form" — internal/generate's action emitter documents and pins the exact contract (see actions.go).

type Resource

type Resource struct {
	Name  string    `json:"name" toml:"name"`
	Route string    `json:"route" toml:"route"`
	Store StoreKind `json:"store" toml:"store"`
	Scope ScopeKind `json:"scope,omitempty" toml:"scope"` // omitempty: an unscoped resource's manifest.json stays byte-identical to pre-Scope artifacts
	List  List      `json:"list" toml:"list"`
	Form  Form      `json:"form" toml:"form"`
}

Resource is one manifest: the §9 sugar a route opts into. Its JSON encoding (the struct tags here and on the types it embeds) is the generator's stable artifact — gen/manifest.json — consumed by any renderer; evolution is additive only. It describes a CRUD interface for a data entity.

func (*Resource) Validate

func (r *Resource) Validate() error

Validate checks the resource declaration for consistency and validity. It normalizes zero values for Kind and Store in place.

type ScopeKind

type ScopeKind string

ScopeKind categorizes who a resource's rows belong to.

const (
	// Unscoped rows belong to the app: one shared table, no owner
	// column — the right shape for admin-style resources.
	Unscoped ScopeKind = ""

	// UserScoped rows belong to whoever created them: the generated
	// store adds an `owner` column holding the session Subject
	// (sessions.Session.Subject — a TEXT value, so keymail's email
	// subjects and password's numeric-string subjects both fit), every
	// generated query filters by it, and a row that isn't yours
	// answers 404 — the scope package's discipline, declared instead
	// of hand-written. Generated actions read the subject via
	// sessions.Current, so scoped routes must mount behind
	// sessions.Require / sessions.Middleware / auth.RequireSession.
	UserScoped ScopeKind = "user"
)

type StoreKind

type StoreKind string

StoreKind categorizes how a resource's data is stored and synchronized.

const (
	Exclusive StoreKind = "exclusive"
	Mergeable StoreKind = "mergeable"
)

type Tool

type Tool struct {
	Description string
	Access      Access
	// Args maps argument names to human/model-readable descriptions.
	// At dispatch, an argument matching a {param} in the route fills
	// that path segment; the rest travel as form values (POST and
	// friends) or query parameters (GET/HEAD).
	Args map[string]string
	// Confirm is the consent sentence shown before a write executes —
	// "{arg}" placeholders interpolate the call's arguments.
	Confirm string
}

Tool marks an action as agent-callable. Declared in the action file itself, next to Handle:

var Tool = rastrillo.Tool{
    Description: "Cancel one order and release its tickets.",
    Access:      rastrillo.ToolWrite,
    Args:        map[string]string{"id": "the order id"},
    Confirm:     "Cancel order {id}? Its tickets go back on sale.",
}

The generator reads it statically (the same AST pass that rewrites package clauses) and emits the registry into gen/tools.go. A ToolWrite with an empty Confirm fails `generate --check` — the buildable half of §13's agent-gate check.

type ToolDef

type ToolDef struct {
	ID     string // route-derived, stable: e.g. "orders_id_cancel_post"
	Method string
	Path   string // the mux pattern's path half, e.g. "/orders/{id}/cancel"
	Tool
}

ToolDef is one registry entry: the tool plus the route it reaches.

Source Files

  • assets.go
  • basecatalog.go
  • ctx.go
  • gen.go
  • icons.go
  • locale.go
  • localemw.go
  • manifest.go
  • ref.go
  • run.go
  • serve.go
  • sidecar.go
  • tool.go

Directories

Path Synopsis
Package assertion signs short-lived identity handoffs between exact HTTPS origins.
Package assertion signs short-lived identity handoffs between exact HTTPS origins.
Package auth is the framework's turnkey sign-in and the family default: a magic-link email that works for every address, auto-upgrading to the keymail ceremony when the address resolves to a claimed keymail inbox.
Package auth is the framework's turnkey sign-in and the family default: a magic-link email that works for every address, auto-upgrading to the keymail ceremony when the address resolves to a claimed keymail inbox.
Package blobs stores content-addressed bytes — design doc §5's blob layer, built on what the platform actually shipped: rows hold metadata (a Ref: hash, size, content type) while bytes live in a Store, keyed by their SHA-256.
Package blobs stores content-addressed bytes — design doc §5's blob layer, built on what the platform actually shipped: rows hold metadata (a Ref: hash, size, content type) while bytes live in a Store, keyed by their SHA-256.
Package carlos is the app side of the CARLOS platform's scheduled-work contract: receiving a tick, and registering a one-shot timer.
Package carlos is the app side of the CARLOS platform's scheduled-work contract: receiving a tick, and registering a one-shot timer.
cmd
dsgen command
Command dsgen writes rastrillo's design-system gallery — every partial, every class idiom, every token, in three themes and twelve languages — to a directory of static files.
Command dsgen writes rastrillo's design-system gallery — every partial, every class idiom, every token, in three themes and twelve languages — to a directory of static files.
rastrillo command
Command rastrillo is the CARLOS web framework's CLI: rastrillo new scaffolds an app, rastrillo generate runs the filesystem-routing generator, rastrillo dev runs the watch/rebuild/restart loop.
Command rastrillo is the CARLOS web framework's CLI: rastrillo new scaffolds an app, rastrillo generate runs the filesystem-routing generator, rastrillo dev runs the watch/rebuild/restart loop.
Package crypto is the family envelope (design doc §6): ECDH P-256 ephemeral → HKDF-SHA256 → AES-256-GCM asymmetric sealing, ECDSA P-256 signing with raw r‖s signatures, and the symmetric half (Derive, SealSym, OpenSym) — every operation domain-separated by a caller-supplied context string.
Package crypto is the family envelope (design doc §6): ECDH P-256 ephemeral → HKDF-SHA256 → AES-256-GCM asymmetric sealing, ECDSA P-256 signing with raw r‖s signatures, and the symmetric half (Derive, SealSym, OpenSym) — every operation domain-separated by a caller-supplied context string.
Package db opens the application's SQLite database the way a CARLOS app needs it, exposed as one *gorm.DB.
Package db opens the application's SQLite database the way a CARLOS app needs it, exposed as one *gorm.DB.
Package eventlog is the rastrillo.Mergeable store shape (design doc §5) — the Eleven shape, extracted fresh since no app had extracted it: a command never UPDATEs; it appends an immutable event to a resource's stream, and a pure Derive fold recomputes the read model.
Package eventlog is the rastrillo.Mergeable store shape (design doc §5) — the Eleven shape, extracted fresh since no app had extracted it: a command never UPDATEs; it appends an immutable event to a resource's stream, and a pure Derive fold recomputes the read model.
Package flash provides one-shot notice messages via HTTP cookies.
Package flash provides one-shot notice messages via HTTP cookies.
Package form holds the plain, framework-independent helpers a generated form handler needs: money parsing/formatting and a field error map.
Package form holds the plain, framework-independent helpers a generated form handler needs: money parsing/formatting and a field error map.
Package gormlite is a GORM SQLite dialector over modernc.org/sqlite.
Package gormlite is a GORM SQLite dialector over modernc.org/sqlite.
Package harness drives a rastrillo app in a real Chromium with a CDP virtual authenticator attached — the browser rig behind `go test -tags browser ./...`.
Package harness drives a rastrillo app in a real Chromium with a CDP virtual authenticator attached — the browser rig behind `go test -tags browser ./...`.
internal
catalog
Package catalog decodes the flat `key = "string"` TOML subset rastrillo's locale catalogs use (design doc §10: "One TOML file per locale ...
Package catalog decodes the flat `key = "string"` TOML subset rastrillo's locale catalogs use (design doc §10: "One TOML file per locale ...
designsystem
Package designsystem renders rastrillo.org/design-system: one static page per theme × locale showing every partial, every markup idiom and every design token the framework ships, plus a full-page demo of each of the four shells and one of the modal route.
Package designsystem renders rastrillo.org/design-system: one static page per theme × locale showing every partial, every markup idiom and every design token the framework ships, plus a full-page demo of each of the four shells and one of the modal route.
devloop
Package devloop implements the polling file watcher behind `rastrillo dev` (design doc §11).
Package devloop implements the polling file watcher behind `rastrillo dev` (design doc §11).
docsite
Package docsite loads the documentation corpus under docs/site and exposes it in the shape the gates need.
Package docsite loads the documentation corpus under docs/site and exposes it in the shape the gates need.
generate
Package generate's action emitter (this file) turns a validated rastrillo.Resource into the (up to) nine action files a manifest owns: gen/actions/<route>/{index.GET,index.POST,new.GET}.go and gen/actions/<route>/[id]/{index.GET,edit.GET,edit-basics.POST, delete.GET,delete.POST}.go, plus [id]/edit-advanced.POST.go when the resource declares Form.Advanced.
Package generate's action emitter (this file) turns a validated rastrillo.Resource into the (up to) nine action files a manifest owns: gen/actions/<route>/{index.GET,index.POST,new.GET}.go and gen/actions/<route>/[id]/{index.GET,edit.GET,edit-basics.POST, delete.GET,delete.POST}.go, plus [id]/edit-advanced.POST.go when the resource declares Form.Advanced.
iconsets
Package iconsets holds the vendored data for every icon set and delivery mode rastrillo can scaffold, and renders the app-owned internal/icons/icons.go from it.
Package iconsets holds the vendored data for every icon set and delivery mode rastrillo can scaffold, and renders the app-owned internal/icons/icons.go from it.
manifest
Package manifest owns manifest discovery and the JSON artifact (gen/manifest.json) the generator consumes (design doc §3).
Package manifest owns manifest discovery and the JSON artifact (gen/manifest.json) the generator consumes (design doc §3).
markup
Package markup is the class→attribute codemod for the ratified markup grammar (design spec §6-v3), and the one place that grammar is written down as code.
Package markup is the class→attribute codemod for the ratified markup grammar (design spec §6-v3), and the one place that grammar is written down as code.
Package jobs is the observable handle for background work: Start runs a function in a goroutine and hands back an ID a status page can poll with Get.
Package jobs is the observable handle for background work: Start runs a function in a goroutine and hands back an ID a status page can poll with Get.
Package keyring owns the E2EE seed lifecycle the crypto package leaves to apps: one 32-byte seed per person, HKDF purpose derivation namespaced by Ring, the seed wrapped under a passkey's PRF output, content keys granted to members' box keys, and the wraps guard that keeps the last wrap unrevokable.
Package keyring owns the E2EE seed lifecycle the crypto package leaves to apps: one 32-byte seed per person, HKDF purpose derivation namespaced by Ring, the seed wrapped under a passkey's PRF output, content keys granted to members' box keys, and the wraps guard that keeps the last wrap unrevokable.
Package mail is the framework's one outbound-email surface — the third extraction of a shape vitogo (internal/vito/mail), kass (internal/mail) and seapointish (smtpMailer) each hand-rolled: a one-method Sender interface, a stdlib net/smtp implementation, a loudly-labelled log fallback for instances with no relay configured, and the header-injection guard all three carried.
Package mail is the framework's one outbound-email surface — the third extraction of a shape vitogo (internal/vito/mail), kass (internal/mail) and seapointish (smtpMailer) each hand-rolled: a one-method Sender interface, a stdlib net/smtp implementation, a loudly-labelled log fallback for instances with no relay configured, and the header-injection guard all three carried.
Package migrate applies an app's schema exactly once per migration and records what it did, replacing the two mechanisms — GORM AutoMigrate for models, raw Migrations []string for framework subsystems — that a Rastrillo app used to run side by side at boot.
Package migrate applies an app's schema exactly once per migration and records what it did, replacing the two mechanisms — GORM AutoMigrate for models, raw Migrations []string for framework subsystems — that a Rastrillo app used to run side by side at boot.
dump
Package dump is the bridge between the rastrillo binary and an app's model structs.
Package dump is the bridge between the rastrillo binary and an app's model structs.
money module
Package passkey hardens an app's sessions with a WebAuthn second factor on the step-up seam: a signed-in user enrolls a passkey, and a valid-but-stale session (refused by sessions.RequireFresh) is made fresh again by an assertion ceremony instead of a full re-sign-in.
Package passkey hardens an app's sessions with a WebAuthn second factor on the step-up seam: a signed-in user enrolls a passkey, and a valid-but-stale session (refused by sessions.RequireFresh) is made fresh again by an assertion ceremony instead of a full re-sign-in.
Package password is an email+password identity plugin on the sessions core: it verifies a submitted credential and calls sessions.SignIn — the same one-call contract auth's keymail flow honors — while leaving user storage, page rendering, and CSRF to the app (csrf.Protect is mounted app-wide, not this package's job).
Package password is an email+password identity plugin on the sessions core: it verifies a submitted credential and calls sessions.SignIn — the same one-call contract auth's keymail flow honors — while leaving user storage, page rendering, and CSRF to the app (csrf.Protect is mounted app-wide, not this package's job).
Package pow is the front door for a form anyone on the internet can post to: an address-bound proof of work, a sealed challenge, a single-use nonce and a honeypot, with the browser half of the proof of work shipped alongside the Go half that verifies it.
Package pow is the front door for a form anyone on the internet can post to: an address-bound proof of work, a sealed challenge, a single-use nonce and a honeypot, with the browser half of the proof of work shipped alongside the Go half that verifies it.
Package scope makes the right query the short query: every model owned by a user (or team) is read through its owner filter, and a row that isn't yours is a row that doesn't exist — handlers answer 404, never 403 (matching view.ParseID's rule: a URL that was never yours was never a URL).
Package scope makes the right query the short query: every model owned by a user (or team) is read through its owner filter, and a row that isn't yours is a row that doesn't exist — handlers answer 404, never 403 (matching view.ParseID's rule: a URL that was never yours was never a URL).
Package sessions maintains signed-in sessions: SQLite-backed rows (so sign-out and admin revocation are real — a deleted row is dead even if the cookie lives on), __Host- cookies on https origins, and the request-context surface (Current, UserID) the rest of an app reads.
Package sessions maintains signed-in sessions: SQLite-backed rows (so sign-out and admin revocation are real — a deleted row is dead even if the cookie lives on), __Host- cookies on https origins, and the request-context surface (Current, UserID) the rest of an app reads.
Package tools is the runtime half of the agents system (design doc §8): it renders the generated registry (gen.Tools()) as LLM tool schemas, and dispatches a model-proposed call back through the app's own mux — "a tool call and an HTTP POST reach the identical Handle function" — with every call re-validated against the registry before it executes, the caller attributed on Ctx.Actor, and §8's consent gate enforced: a write tool refuses to run unconfirmed.
Package tools is the runtime half of the agents system (design doc §8): it renders the generated registry (gen.Tools()) as LLM tool schemas, and dispatches a model-proposed call back through the app's own mux — "a tool call and an HTTP POST reach the identical Handle function" — with every call re-validated against the registry before it executes, the caller attributed on Ctx.Actor, and §8's consent gate enforced: a write tool refuses to run unconfirmed.
Package ui is rastrillo's server-shape component library: a small starter set of List-screen html/template partials, a design-token stylesheet, and the template helpers they need — vendored the same way icons.go vendors Lucide, so an app pulls in a working component with an import and a ParseFS call, not a hand-copy.
Package ui is rastrillo's server-shape component library: a small starter set of List-screen html/template partials, a design-token stylesheet, and the template helpers they need — vendored the same way icons.go vendors Lucide, so an app pulls in a working component with an import and a ParseFS call, not a hand-copy.
Package vault is rastrillo's client half of the Pegamento vault facet: one person's named sealed blobs and per-method wrapped seed on a home service the app's operator may not run.
Package vault is rastrillo's client half of the Pegamento vault facet: one person's named sealed blobs and per-method wrapped seed on a home service the app's operator may not run.
Package vectors emits the golden vectors that pin an app's JS derivation engine to its Go one.
Package vectors emits the golden vectors that pin an app's JS derivation engine to its Go one.
Package view holds the plain HTTP-response helpers a generated action needs against a *rastrillo.Ctx: rendering a page, failing loudly but safely, and reading the {id} path value.
Package view holds the plain HTTP-response helpers a generated action needs against a *rastrillo.Ctx: rendering a page, failing loudly but safely, and reading the {id} path value.
Package webauthn verifies passkey registrations and assertions — the extraction design doc §7 names: kass and slopbox carried duplicate copies of this package (kass's internal/webauthn is the source lifted here, tests and all), and each thing it leaves out is a thing that cannot be got wrong.
Package webauthn verifies passkey registrations and assertions — the extraction design doc §7 names: kass and slopbox carried duplicate copies of this package (kass's internal/webauthn is the source lifted here, tests and all), and each thing it leaves out is a thing that cannot be got wrong.
authtest
Package authtest is a passkey authenticator for tests.
Package authtest is a passkey authenticator for tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL