🤖 Writing an addon
You built something in one Rastrillo app, and other apps need it too. It goes either in the framework (core) or in an addon: a separate Go module an app adds when it wants it. Decide which first, because the two paths are different work.
Core or addon
Core holds what every app needs and what is hard to get right twice. An addon holds what many apps want but not every app should carry.
Put a change in core when it fills a gap in something core already ships. For example, extending a partial in ui/partials/, or adding a hook inside a core package such as the db handle's write pool. An addon could only override or wrap the core code, which is the copying you set out to end.
Make an addon when the feature brings its own tables and its own policy, and that policy will keep changing on its own schedule. Roles, data retention and notification rules are policy. idear (accounts, roles, invitations) is an addon. So is aviso (Web Push).
"Not every app needs it" is not enough on its own. Passkeys, background jobs and manifests are core, and not every app uses them.
Core also costs more to add to. Every exported name in a core package needs a reference page, and internal/docsite/symbols_test.go fails the build when one is missing.
If you are unsure, ask before you write code. Open a discussion on the Rastrillo repository, as an Idea or a Feature Request, and say what each app built and where:
amadan discuss new rastrillo/rastrillo -title "..." -body -
What an addon costs
An addon is built against one Rastrillo version, and Go uses a single Rastrillo for the whole build. When a core package the addon uses changes in a way that breaks it, such as sessions or migrate, apps cannot upgrade Rastrillo until the addon catches up.
An addon also needs its own CI, its own frozen migration checksums and its own releases.
The four rules
Addons sets out four rules. The addon imports Rastrillo, and Rastrillo never imports the addon. The addon ships its own namespaced migrations, which the app merges into BootSchema. The addon ships its own SKILL.md inside the module. The addon does not re-implement sessions, CSRF, migrations, forms or flash.
Ask core for a hook first
If the addon needs a hook that core does not have, propose it in a discussion. Land the hook in core on its own branch, then build the addon on it.
Use an existing hook when one fits. idear fills auth.Config.Authorize, which core leaves open for membership rules.
A hook must be useful to any addon, not a feature for one. idear needed sign-up to be refusable, so password.ErrRefused landed in core first, with tests and a line on the password reference page.
Copy an existing repository
Start from aviso, the smaller of the two, and look at idear if your addon has more wiring. Two parts are not in aviso: take the frozen checksum test from idear's schema_test.go, and add your own LICENSE.
go.mod requires amadan.net/rastrillo/rastrillo at a released tag
<name>.go ... the package
migrations/ 0001_init.sql, embedded
migrations.go var Schema = migrate.MustFromFS(migrationFS, "<name>")
schema_test.go frozen migration checksums
example/ a working app
SKILL.md for agents; read instead of the source
skillmd_test.go byte budget, and the exported names it must mention
Makefile one ci target
.amadan/ci runs make ci
.amadan/ci.d/ one step per make target
README.md, LICENSE
Embed the migrations and use the addon's name as their namespace, so they never collide with the app's:
//go:embed migrations/*.sql
var migrationFS embed.FS
// Schema is the addon's migration set. Merge it into the app's
// BootSchema, never its Schema: `rastrillo migration check` diffs
// Schema against Models and would propose dropping a table Models
// does not know about.
var Schema = migrate.MustFromFS(migrationFS, "aviso")
In the app, merge the addon's set into BootSchema. The arguments are applied in order:
var BootSchema = migrate.Merge(sessions.Schema, aviso.Schema, Schema)
Migrations explains why it goes in BootSchema and not Schema.
Choose the addon's own licence. It does not inherit Rastrillo's.
Run the gate
make ci is the one definition of the addon's checks. Each step in .amadan/ci.d/ calls a make target and never keeps its own copy of a command, so change the Makefile and the step together.
The usual targets are go vet, a gofmt check, go test ./... -count=1, and a -race run with CGO_ENABLED=1 set for that target only. -count=1 stops a cached pass from standing in for a fresh run of a database test. If the addon has a browser half, drive it in a real browser with harness. Leave RASTRILLO_BROWSER_OPTIONAL unset in the gate, so a run with no browser fails and does not skip.
If the example is its own module, as aviso's is, go test ./... at the root does not reach it. Test it from its own directory. Testing has the patterns.
Write SKILL.md
An agent reads SKILL.md instead of the source, so an inaccurate line is worse than a missing one. Cover how to install the addon, how to wire it into an app, its routes or API, the mistakes people make, and a checklist for a finished mount.
Keep it under a byte budget that a test enforces. aviso's is 9,000 bytes. If it must grow, raise the budget in the test and say why there. Do not cut a fact that prevents a mistake to make it fit.
SKILL.md ships inside the module, so the copy an agent reads matches the version the app builds against:
go get amadan.net/rastrillo/aviso
cat "$(go list -m -f '{{.Dir}}' amadan.net/rastrillo/aviso)/SKILL.md"
Choose a module path
The path must work with go get. Addons the project maintains live at amadan.net/rastrillo/<name>, and amadan serves the go-import tag for them. To host yours there, ask in a discussion. Anywhere else go get reaches works too.
Before anything depends on the path, check it from a throwaway module, with your path in place of aviso's:
cd "$(mktemp -d)"
go mod init example.com/check
go get amadan.net/rastrillo/aviso
Release it
Tag semantic versions, v0.x.y while the API is settling. Pin Rastrillo to a released tag in go.mod, not a commit.
When Rastrillo releases a breaking change to a package your addon uses, update the addon and tag a release soon. Apps cannot upgrade Rastrillo until you do.
Get listed
Open a branch on rastrillo/rastrillo that adds an entry to Addons (docs/site/addons.md). Follow the existing entries: status and version, module path and source link, the go get and cat SKILL.md commands, what the addon does, and what it deliberately does not do.
Describe the branch on amadan. It is reviewed and squash-merged like any other change. AGENTS.md in the repository has the procedure.
Before you ask to be listed
- You checked it does not belong in core.
- Any hook it needed has landed in core.
- Its migrations are namespaced and merged into
BootSchema. SKILL.mdis under a tested byte budget.- The example runs and its tests pass.
make cipasses.go getworks from a throwaway module.- It has a released version and pins a released Rastrillo.
- It has its own licence.
Read this page as markdown — exact, unstyled, and cheap for an agent to fetch.