CARLOS rastrillo docs

Fast, cost-efficient, good: pick all three with rastrillo.

The web framework companion to CARLOS: libraries you already know โ€” GORM, chi, html/template โ€” on a middle layer that gets sessions, CSRF, and owner scoping right, shipped to a platform that only bills you when someone shows up.

The opening statement: what a real multi-user app costs to write
written, in examples/notes โ€” accounts, sessions, owner-scoped CRUD, a background export270 lines
proving Bob can never read Alice's notes โ€” or her bookmarks, or her export โ€” the isolation suite, in CI1,177 lines
client-side JavaScript required, new DSLs, config formats0
Try it now Read the docs Read the source

๐Ÿค– This page was written by an LLM, on the ideas, instruction, and editing of humans. AI-written text here is always marked, visibly, like this โ€” that's factor X.

Boring machinery, deliberately

Libraries you already know, SQLite, and files you can read โ€” nothing decided behind your back.

Libraries you already know
Models are plain GORM structs, routes are a chi router, pages are html/template โ€” no bespoke DSL to learn, and nothing your Go experience (or your model's training data) doesn't already cover. errors.Is(err, gorm.ErrDuplicatedKey) just works. Rastrillo supplies the parts that are hard to get right twice, not a new language for the parts that aren't.
The security defaults
Sessions are SQLite rows โ€” sign-out and revocation are real, not a cookie hoping to be forgotten โ€” with __Host- cookies on https and step-up re-auth (RequireFresh) for sensitive routes. CSRF is an origin check on every mutating request; no tokens to mint or forget. Failed sign-ins are rate-limited per account. And owner scoping has one contract: a row that isn't yours is a row that doesn't exist โ€” 404, never 403 โ€” enforced in examples/notes by a two-user isolation suite that runs in CI.
A 15KB contract, not 150KB of source
SKILL.md at the repo root is the whole app story โ€” the file shape, the scoping rules, the mass-assignment rule, the identity plugins โ€” budgeted under 15,000 bytes and reviewed like code. It's what an agent loads instead of the framework source, and what a person reads first. The docs are part of the artifact, not an afterthought.
Platform-ready by default
rastrillo.Resolve and Serve speak the CARLOS platform's activation contract end to end โ€” hibernating instances, carlos-app@ systemd unit tenants, plain instances, and a bare dev fallback, no separate integration per deployment shape. Hibernation is the cost-efficiency receipt: your app isn't running when nobody's knocking, and the platform wakes it when someone does.
One binary, no toolchain sprawl
go build with CGO_ENABLED=0 gets you the whole app โ€” SQLite compiled in via pure-Go modernc.org/sqlite under an owned GORM dialector, a writer pool capped at one connection and a reader pool that keeps WAL reads flowing. No bundler, no codegen service, no runtime to babysit, and not one line of client-side JavaScript by default.

Or declare it: fifteen lines in, screens out

Generated, not interpreted โ€” the declarative path beside the code path: mix them per resource, move a resource between them freely, use either or both.

declared manifest/ticket_types.toml โ€” the whole file
name  = "ticket_types"route = "/admin/ticket_types"store = "exclusive"[list]columns = [{ field = "Name" }, { field = "Price", kind = "money" }, { field = "Status" }]search  = true[[list.filters]]field  = "Status"values = ["draft", "on_sale", "sold_out"][form]basics   = [{ name = "Name", required = true }, { name = "Price", kind = "money", required = true }, { name = "Status" }]advanced = [{ name = "MaxPerOrder" }]
generated what rastrillo generate wrote from it
files committed under gen/, diffable in review like any code25
lines of plain Go, SQL, HTML, and locale keys โ€” each action opens with one form.Parse call, the same documented helper a hand-written handler uses1,202
actions, wired into the router โ€” list, create, show, edit, and the delete flow9
screens โ€” a list with search, a working Status filter and paging; a two-tier form; a show page; a delete confirm4
typed store โ€” schema, additive migrations, sqlc queries1
TOML parsed at request time0

required = true is server-side: the generated actions 400 with the field's own message, re-rendered in the form. There is no actions/ directory in this app at all โ€” examples/tickets is manifest-only, and it's the framework's own regression suite. Add scope = "user" and every generated query is owner-filtered by the signed-in subject โ€” someone else's row answers 404, the same contract the hand-written path enforces; examples/notes runs a declared, scoped resource beside hand-written handlers under one two-user isolation suite. And when a generated screen stops being enough, eject it: the file becomes yours, and the generator leaves it alone.

Deployedlive ยท real edge

Not a mockup

Rastrillo's example app is deployed on a real CARLOS platform right now: real S3-backed storage, a real edge, a real certificate, a hibernating instance that wakes when you knock. hello.bdf.oncarlos.com โ€” the same framework you'll have running locally two minutes from now, actually shipped (/api/version names the exact commit). This site is served the same way.

Run it locally

Nothing to install but Go: clone, run, and try to break the isolation yourself.

1

Clone the repo

git clone https://github.com/rastrilloorg/rastrillo && cd rastrillo/examples/notes

examples/notes is the front-door example: accounts, sessions, CSRF, flash, and owner-scoped notes โ€” 163 lines of domain code.

2

Run it

CGO_ENABLED=0 go run ./cmd/notes -addr localhost:8080

Visit localhost:8080 โ€” sign up, write a note. Open a private window, sign up as someone else, and try to reach the first account's note by URL: 404. That isolation is the framework's owner-scoping contract, and the same check runs as a test suite in CI.

3

Start your own

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

The scaffold is the same five-file shape, tests passing before you write a line. Then read SKILL.md โ€” the whole app story in one file. Building with an agent? That file is exactly what it should load.

Deploy it for real

CARLOS is self-hostable โ€” one binary, a bucket, and DNS. No PaaS account to sign up for.

1

Install the platform binary

brew install carlosframework/tap/carlos

Or grab a binary directly from carlosframework/releases.

2

Build your app for the target host

GOOS=linux GOARCH=arm64 go build -o myapp-binary ./cmd/myapp
3

Ship, promote, route

carlos ship -app myapp -version $(git rev-parse --short HEAD) myapp-binary
carlos promote -app myapp <version> stable
carlos add -app myapp -kind instance myapp.yourdomain.com

The same sequence โ€” against a real S3-backed bucket, a real edge, a real certificate โ€” that put rastrillo's own example live at hello.bdf.oncarlos.com. Standing up the box, bucket, and DNS is the one-off part; from then on every deploy is ship then promote.

What's real today

A young framework that ships its status on its homepage โ€” this list moves when the work lands, not before.

Posted

  • The middle layer: db (GORM on cgo-free SQLite, split writer/reader pools, translated constraint errors), sessions (revocable rows, step-up RequireFresh), csrf, flash, form, view, scope โ€” each importable alone, tested alone
  • rastrillo/auth โ€” the family default sign-in: magic-link email that auto-upgrades to Keymail when the address has a claimed inbox, over keymaildev/signin
  • rastrillo/password โ€” classic email+password on the sessions core: stdlib PBKDF2, per-account rate limiting, one non-oracle failure message
  • SKILL.md โ€” the authoring contract, under 15,000 bytes, reviewed like code: what an agent loads instead of the framework source
  • examples/notes โ€” accounts, sessions, CSRF, flash, owner-scoped CRUD, and a background export in 270 domain lines, with a two-user isolation suite in CI
  • rastrillo/crypto โ€” the family envelope: P-256 sealing and signing, invite derivation and key wrapping, a WebCrypto JS twin โ€” all pinned by golden vectors from real consumers
  • rastrillo/webauthn โ€” passkeys, verify-only, with a test authenticator and the browser module included
  • rastrillo/eventlog โ€” the mergeable store shape: append-only streams, pure Derive, deterministic merge pinned by vectors
  • rastrillo/blobs โ€” content-addressed bytes on the platform's object store, presigned URLs, sealed E2EE wrapper
  • rastrillo.Resolve/Serve/Run โ€” the full platform activation contract, hibernate and systemd tenants included
  • SQLite bootstrap: pragma ordering, single-writer, additive migrations
  • Localization: locale-resolved routes, T/Tf with fallback, a ship-gate catalog check
  • rastrillo/ui โ€” the component vocabulary: 27 partials, documented class idioms, design tokens โ€” light and dark, WCAG AA enforced by tests, zero JavaScript
  • The manifest path: a declared Resource generates store, actions, screens, and locale keys โ€” an optional, equal alternative to hand-written handlers, mixed per resource; generated actions compose the same documented form.Parse/view helpers a hand-written app uses; examples/tickets is its regression suite
  • Manifest resources on the mergeable store (v0.16.0) โ€” store = "mergeable" generates the same screens over an eventlog-backed store: each record one append-only stream, deletes appended as tombstones, every read derived by replaying the merged history; examples/tickets' announcements resource is the generated proof, tombstone test included
  • Manifest resources with per-user scoping โ€” scope = "user" owner-filters every generated query by the session subject, so someone else's row answers 404, the same contract the hand-written path enforces; examples/notes mixes a declared, scoped resource beside its hand handlers, proven by one two-user isolation suite
  • rastrillo new โ€” scaffolds the middle-layer five-file shape with a passing test suite out of the box; the declarative path stays one TOML away (manifest/README.md carries the mounting recipe)
  • rastrillo/passkey โ€” the WebAuthn second factor, both places it belongs: on the step-up seam (enroll while signed in, satisfy RequireFresh with an assertion instead of a full re-sign-in) and at first sign-in (Gate on the identity plugins' SecondFactor hook: a verified first factor becomes a pending half-session only an assertion completes โ€” the minted session names both factors) โ€” single-use, subject-bound challenges and half-sessions throughout. And the escape hatch (v0.14.0): ten single-use recovery codes, minted behind step-up and shown once, redeem a lost passkey at the gate by plain form POST โ€” no JavaScript, because that's exactly when WebAuthn isn't working; sign-in only, so step-up still takes a real assertion
  • Agents: actions opt in as tools; the generated registry dispatches consent-gated, actor-attributed calls; sidecars speak the platform contract
  • Background work made observable โ€” jobs gives a goroutine an owner-scoped handle and a status page that works with scripts off: a noscript refresh while it runs, a 303 to the result when it's done. The one script in the framework is rastrillo.js, a ~130-line app-owned shim that polls HTML fragments for the smooth version โ€” no JS required, htmx a choice rather than a dependency; examples/notes ships a background export under the same isolation suite. Bounded, too (v0.13.0): an owner holds at most four running jobs, and a stuck one is failed after fifteen minutes, its slot freed. And pushed (v0.15.0): status pages ride Server-Sent Events where the browser supports them โ€” heartbeats, per-write deadlines, a bounded stream โ€” and fall back to plain polling on their own
  • rastrillo/carlos (v0.19.0) โ€” scheduled work on a platform that keeps your app asleep: carlos.Tick is the constant-time bearer check that separates a real platform tick from an internet request to the same public path, TickOccurrence is the dedupe key that stays the same across at-least-once redelivery, and ScheduleAt/ScheduleCancel register one-shot timers that live in the box's registry rather than in your process, so a restart loses none of them; recurring schedules are declared from outside the app with carlos schedule set, and the app's whole part is a guarded handler

Pending

  • Mergeable transport โ€” edge sync is the platform's designed territory; eventlog.Ingest is the seam waiting for it, and until it lands, generated mergeable ids stay writer-local
  • Schema evolution for declared resources โ€” generated stores emit only the initial CREATE; changing a manifest's fields is still the app's own ALTER, not a generated diff
  • Event provenance โ€” every generated mergeable mutation is stamped actor "app" today; threading the session's actor into store events is the recorded follow-up