CARLOS rastrillo docs

๐Ÿค– Manifests

Manifests are the declarative path: an optional, equal alternative to hand-written handlers.

Declare a resource in manifest/<name>.toml, run rastrillo generate, and you get its store, its four screens' worth of actions and templates, and their locale keys โ€” as readable committed code composing the same form and view helpers a hand-written app uses.

The two paths live side by side

Per resource. Declare the screens that are pure CRUD, hand-write the ones that are not, and move a resource between the paths whenever its needs change: eject one generated file, or delete your hand-written files and re-declare it.

examples/tickets is the fully generated proof โ€” one manifest resource, zero hand-written actions, zero ejected templates. examples/blog is the mixed case.

A resource

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" }]

The vocabulary is honestly scoped

One flat resource. Three field kinds: text, textarea, money. No relations.

Relations and custom flows take the code path. That boundary is where the generator currently stops, and reaching it is not a failure โ€” mixing the two paths is the design.

Every generated store adds fixed columns: id, created_at, updated_at, plus owner for a scoped resource. A field colliding with one of those is refused at generate time instead of producing a confusing table.

Ownership

scope = "user"

Owner-filters every generated query by the session subject, so someone else's row answers 404 โ€” the same discipline the scope package enforces by hand, declared instead of written.

Mount the resource behind sessions.Require or auth.RequireSession. The generated queries carry the filter; they do not authenticate.

examples/notes runs both halves side by side and proves them with one two-user suite.

The two stores

store = "exclusive" is the default: one SQL table, with sqlc query colocation for the store.

store = "mergeable" keeps each record as an eventlog stream โ€” one stream per record, derived reads, tombstone deletes, misses surfacing as sql.ErrNoRows. A scoped mergeable resource checks the row's owner the same way the exclusive one does.

Two limits before you choose it, both live today. Ids are writer-local, because the platform's edge-sync transport is not built yet (eventlog.Ingest is the seam it will call). And every generated event's actor is "app"; threading a real actor through is not done.

If neither matters for your resource, it works. If they do, use exclusive for now.

Schema evolution

Generated stores emit only the initial CREATE. There is no automatic diff-and-ALTER for a declared resource, so evolving one's schema is your own migration โ€” write it with rastrillo migration new.

There is a trap in how this meets the migration ledger. A generated store's gen/store/<name>/migrations.go is a raw []string you wrap into a *migrate.Set by hand. Reshape a resource after first boot and the regenerated SQL no longer matches the ledger's recorded checksum, so boot refuses. Migrations has the recovery, and its step order matters.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.