Skip to content
Use plugins

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/postmark
Adding 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 migrations

anetos 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:env

A 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 . migrate

4. Check what it adds

go run . plugins:list
PLUGIN    REQUIRES             ROUTES     ADDS
postmark  >= v0.2.0, < v0.3.0  /postmark  config, migrations, commands, jobs, routes

A 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/postmark

This 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:

WhatName
SettingsPOSTMARK_WEBHOOK_USER, POSTMARK_WEBHOOK_PASSWORD: the webhook’s basic auth
RoutePOST /postmark/webhook (postmark.webhook): checks the credentials, queues Bounce, SpamComplaint and SubscriptionChange events, answers 204
Jobpostmark: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
Tablepostmark_suppressions (migration set postmark)
Commandspostmark:suppressions lists the addresses; postmark:unsuppress <email> removes one
Functionpostmark.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

ProblemCauseFix
plugins.go is missingA project made before v0.2Create 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 AnetosCheck the module’s docs and the version you asked for
requires Anetos >= v0.3.0, but this is v0.2.1The 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.LoadThe plugin adds to a service your app doesn’t set up, or sets up after ext.LoadSet it up in setup, before ext.Load
plugin postmark: settings: … when the app startsA setting is missing or invalidgo run . plugins:env postmark lists them; set them in .env
A plugin’s route conflicts with yoursBoth use the same pathMount the plugin elsewhere with ext.Mount

Next steps

Coming from Laravel? anetos add is composer require plus package discovery, done once as generated code (plugins.go) instead of at every request. A plugin is a service provider whose register()/boot() are split by what it adds, and its settings are environment variables read into its own struct: there is no vendor:publish of config files. Migrations aren’t copied into your app; they run from the plugin with yours.