Use plugins
Add features to your app with plugins: packages that bring their own
routes, database tables, settings, commands, queue jobs, scheduled tasks
or event listeners. anetos add installs one with one command. The
complete app is examples/queue, which uses
the Postmark plugin to stop emailing addresses that bounced.
Warning: A plugin is Go code compiled into your app, with the app’s privileges: its database, its secrets, its network. There is no sandbox. Add only plugins you trust, as you would any dependency.
Before you start
You have a project made with anetos new (v0.2 or later), with the
anetos tool installed in it (go get -tool anetos.dev/anetos/cli/cmd/anetos). Its main.go loads the
plugins listed in plugins.go at the end of setup. A project made
before v0.2 needs both: see step 1.
Steps
1. Load plugins in setup
Projects made with anetos new already do this. At the end of setup,
after the services plugins add to (web.NewServer, migrate.ForApp,
queue.ForApp, schedule.ForApp, events.ForApp):
// The plugins in plugins.go (anetos add), last: they use the services
// above. The postmark plugin's webhook is POST /postmark/webhook.
if err := ext.Load(app, plugins()); err != nil {
return nil, err
}(Copied from examples/queue, region plugins.)
plugins() is in plugins.go, which anetos add and anetos remove
write; don’t edit it. If your project has none (anetos add says
so), create it with an empty list, as anetos new writes it:
// Code generated by anetos add. DO NOT EDIT.
package main
import (
"anetos.dev/anetos/ext"
)
// plugins returns the app's plugins, in the order they were added:
// anetos add and anetos remove rewrite this file. setup loads them with
// ext.Load.
func plugins() []ext.Plugin {
return []ext.Plugin{}
}If a plugin needs a service your app doesn’t set up, ext.Load says
which call is missing: plugin postmark: it has jobs: call queue.ForApp before ext.Load.
2. Add a plugin
From the project’s directory:
go tool anetos add anetos.dev/anetos/plugins/postmarkAdding anetos.dev/anetos/plugins/postmark@latest. Plugins run with your app's privileges: add only code you trust.
Installed anetos.dev/anetos/plugins/postmark v0.2.0.
Listed it in plugins.go.
Added its settings to .env.example: POSTMARK_WEBHOOK_USER, POSTMARK_WEBHOOK_PASSWORD. Set them in .env.
Next:
go run . plugins:list what it adds
go run . migrate if it adds migrationsanetos add runs go get, lists the plugin in plugins.go, tidies
go.mod, builds the app, loads the plugin as the app does (its plugins:env command, which
doesn’t boot the app), and adds the plugin’s settings to .env.example.
If the plugin isn’t a plugin, doesn’t compile, or the app refuses it
(it requires another version of Anetos, or its name is taken), go.mod,
go.sum and plugins.go are left as they were. If the plugin needs a
newer Anetos than your app had, go get upgrades it, and anetos add
says so. Add @v1.2.3 to the module for a version other than the
latest. (A project made with anetos new --replace takes Anetos’s
first-party plugins from that checkout.)
3. Configure and migrate
Set the plugin’s settings in .env (and in production’s environment).
Every setting starts with the plugin’s name in capitals; list them, with
their defaults, with:
go run . plugins:envA plugin whose required settings are missing stops the app from booting, with an error that names them. Then run its migrations, which never run by themselves:
go run . migrate4. Check what it adds
go run . plugins:listPLUGIN REQUIRES ROUTES ADDS
postmark >= v0.2.0, < v0.3.0 /postmark config, migrations, commands, jobs, routesA plugin’s routes are under /<name> and named <name>.…
(routes:list shows them); its commands are named <name>:… (help
lists them); its jobs run on your app’s workers. To serve its routes
somewhere else, mount it in setup:
// illustrative
if err := ext.Load(app, plugins(), ext.Mount("postmark", "/hooks/postmark")); err != nil {
return nil, err
}5. Use it from your code
A plugin’s package can export functions for your code. The Postmark
plugin records the addresses Postmark stopped sending to (hard bounces,
spam complaints, unsubscribes), and postmark.Suppressed checks one;
the receipt listener of examples/queue/events.go
skips them:
// illustrative: in a listener with ctx and the order event e
if bad, err := postmark.Suppressed(ctx, e.Email); err != nil || bad {
return err // nil for a suppressed address: nothing to send
}6. Test
anetostest.New runs your setup, so tests get the plugins too, and
their migrations. Set their settings with anetostest.Env:
// The postmark plugin's webhook records a hard bounce; the receipt to
// that address isn't sent.
func TestBouncedAddress(t *testing.T) {
fakeGateway(t, &FakeGateway{})
app := anetostest.New(t, setup, anetostest.Env(map[string]string{
"QUEUE_DRIVER": "sync",
"POSTMARK_WEBHOOK_USER": "postmark",
"POSTMARK_WEBHOOK_PASSWORD": "s3cret",
}))
req := httptest.NewRequest(http.MethodPost, "/postmark/webhook",
strings.NewReader(`{"RecordType":"Bounce","Type":"HardBounce","Email":"ada@example.com","Inactive":true}`))
req.SetBasicAuth("postmark", "s3cret")
app.Do(req).AssertNoContent()
placeOrder(t, app, "Lamp", 4250)
if sent := sentMail(app); len(sent) != 0 {
t.Errorf("%d emails to a bounced address", len(sent))
}
}(Copied from examples/queue/main_test.go, region test-plugin.)
7. Remove a plugin
go tool anetos remove anetos.dev/anetos/plugins/postmarkThis takes it out of plugins.go, runs go mod tidy and builds the
app. Its settings stay in .env and .env.example, and its tables in
the database. To drop the tables, add a migration of your own that drops
them (go tool anetos make:migration drop_postmark_suppressions), and
forgets that the plugin’s migrations ran, so that adding the plugin
again later creates them again:
// illustrative: the up function of drop_postmark_suppressions
func(s *migrate.Schema) error {
if err := s.DropIfExists("postmark_suppressions"); err != nil {
return err
}
return s.Exec("DELETE FROM migrations WHERE source = ?", "postmark")
}Don’t use migrate:rollback for this: it rolls back the last batch,
which holds your app’s migrations too if they ran with the plugin’s.
The Postmark plugin
anetos.dev/anetos/plugins/postmark is the first first-party
plugin. Besides the Postmark mail transport (Send email), it
adds:
| What | Name |
|---|---|
| Settings | POSTMARK_WEBHOOK_USER, POSTMARK_WEBHOOK_PASSWORD: the webhook’s basic auth |
| Route | POST /postmark/webhook (postmark.webhook): checks the credentials, queues Bounce, SpamComplaint and SubscriptionChange events, answers 204 |
| Job | postmark:webhook: adds the address to the list when Postmark deactivated it (a hard bounce, a complaint) or an unsubscribe suppresses it; removes it when a subscription change resumes sending. Soft bounces change nothing |
| Table | postmark_suppressions (migration set postmark) |
| Commands | postmark:suppressions lists the addresses; postmark:unsuppress <email> removes one |
| Function | postmark.Suppressed(ctx, email) |
In Postmark, add a webhook to your stream with the URL
https://USER:PASSWORD@example.com/postmark/webhook and the Bounce,
Spam Complaint and Subscription Change events. Without both settings,
the webhook refuses every request.
How it works
Go has no runtime plugin discovery: plugins are packages compiled into
your app. anetos add is code generation: it writes plugins.go, a
list of each plugin package’s Plugin(), and setup passes the list
to ext.Load. For each plugin, in order, Load checks its name and the
Anetos versions it supports (anetos.Version()), fills its settings,
and adds its migrations, commands, jobs, routes, scheduled tasks,
listeners and boot work to your app, each in the plugin’s namespace.
Nothing else is wired in, and nothing runs until the app does.
Common problems
| Problem | Cause | Fix |
|---|---|---|
plugins.go is missing | A project made before v0.2 | Create it and call ext.Load: step 1 |
the app doesn't build with … | The module has no Plugin() ext.Plugin function, or doesn’t compile with your version of Anetos | Check the module’s docs and the version you asked for |
requires Anetos >= v0.3.0, but this is v0.2.1 | The plugin supports other versions of Anetos (anetos add refuses it; an update of Anetos can bring this up later) | Add a version of the plugin that supports yours, or update Anetos |
call queue.ForApp before ext.Load | The plugin adds to a service your app doesn’t set up, or sets up after ext.Load | Set it up in setup, before ext.Load |
plugin postmark: settings: … when the app starts | A setting is missing or invalid | go run . plugins:env postmark lists them; set them in .env |
| A plugin’s route conflicts with yours | Both use the same path | Mount the plugin elsewhere with ext.Mount |
Next steps
- Write a plugin: build your own.
- CLI reference:
anetos add,anetos remove,plugins:list,plugins:env. - Configuration reference: the plugins’ settings.
Coming from Laravel?
anetos addiscomposer requireplus package discovery, done once as generated code (plugins.go) instead of at every request. A plugin is a service provider whoseregister()/boot()are split by what it adds, and its settings are environment variables read into its own struct: there is novendor:publishof config files. Migrations aren’t copied into your app; they run from the plugin with yours.