Extending Grit

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 available
grit plugin info multitenant # what it does
grit plugin add multitenant # install
grit 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.

Edited code is never overwritten

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.