Plugins
A Grit plugin generates code into your project. It isn't a runtime dependency you import — it writes models, routes and pages into your repo, which you then own and can edit like anything else.
Using plugins
grit plugin list # what's availablegrit plugin info multitenant # what it doesgrit plugin add multitenant # installgrit plugin remove multitenant
Why generated code, not a library
Grit is a code generator. grit new emits a Go + React app that has no dependency on Grit afterwards — there is no runtime framework object for a plugin to hook into. So plugins write code instead.
That trade is deliberate, and it cuts both ways:
- You own the code. Read it, edit it, delete it. No digging through a vendor directory to work out what a plugin does.
- No automatic upgrades. A plugin that ships a fix won't update code already in your repo. Remove and re-add to take a new version, and review the diff.
Removal is exact
Installation records every file written and every snippet injected in .grit/plugins.lock.json. Removal replays that record backwards. Commit the lockfile — it's how your teammates know what's installed.
This is the part worth understanding if you write a plugin: there is no uninstall code to write. You describe what to install; removal is derived. A separate hand-maintained removal list is exactly how this kind of tooling drifts and starts leaving projects that don't compile.
If you change an injected block, removal reports it and leaves it alone rather than guessing. You'll be told which file to tidy by hand.
Writing a plugin
A plugin is a value describing files, injections and dependencies:
plugin.Plugin{Name: "audit-trail",Version: "1.0.0",Summary: "Records who changed what",// Requires other plugins be installed first.Requires: []string{},Files: func(ctx plugin.Context) map[string]string {return map[string]string{"apps/api/internal/audit/audit.go": auditSource(ctx),}},Injections: func(ctx plugin.Context) []plugin.Injection {return []plugin.Injection{{File: "apps/api/internal/routes/routes.go",Marker: "// grit:routes:protected",Code: "\t\tprotected.GET(\"/audit\", auditHandler.List)",}}},NextSteps: []string{"Run migrations: go run cmd/migrate/main.go"},}
Files and Injections are functions, not data, so a plugin can adapt to the project. ctx carries the module path, architecture and frontend — a --triple app needs admin pages an --api project does not.
Injections
Code is inserted on the line before a marker comment. The markers a scaffolded project provides:
// grit:models AutoMigrate registry// grit:handlers handler construction// grit:routes:protected authenticated routes// grit:routes:admin admin-only routes// grit:routes:custom public routes// grit:seeders seed registration// grit:cron-tasks scheduled tasks
Mark an injection Optional when the target legitimately may not exist — a frontend file in an --api project. A missing marker is then a warning instead of a failed install.
Rules the installer enforces
- Never overwrites an existing file. Removal would otherwise delete something your plugin didn't create.
- A missing marker fails the install unless the injection is optional. A silent partial install is worse than a clear error.
- Installing twice is refused — remove first.
- Requirements are enforced both ways. You can't install without dependencies, or remove something still depended on.
A worked example
The multitenant plugin is the reference implementation: models, a GORM callback, middleware, an API, and tests — five files and three injections. Read it in internal/plugin/multitenant.go.
