The host and its lifecycle
The host starts your plugins in a safe order, gives you what they declare, and shuts them down again:
host := pluginkit.NewHost(registered...)if err := host.Start(ctx); err != nil { return err}defer host.Stop(ctx)NewHost panics if two plugins share an id. That is a wiring
mistake rather than a runtime condition, so it fails immediately at
startup instead of misbehaving later.
Start gives you four guarantees:
- Every database migration runs before any plugin starts, so no plugin ever runs against a schema that is not ready.
- Plugins start in the order they were registered.
- If one fails to start, the host stops the ones already started, in reverse order, then returns the original failure along with any errors from stopping. You never end up half started.
- If a plugin panics, the host turns it into an ordinary error naming the plugin and what it was doing. One bad plugin cannot bring down the process.
Sample data is not part of booting. Seed is a separate call, so a
production start never writes it:
if err := host.Seed(ctx); err != nil { return err}Plugins seed in registration order, and the first failure stops the
run. A plugin that panics becomes an error naming it, the same
protection Start has. Plugins without the capability are simply
skipped. Call Seed from a development subcommand of your binary,
never from the serve path.
Routes and public paths
Section titled “Routes and public paths”routes := host.Routes() // map[string]http.Handlerpublic := host.PublicPaths() // map[string][]stringBoth maps are keyed by plugin id, and only contain the plugins that declare them. The host does not mount anything itself, so you stay in charge of your router and your URL layout.
Guarding a namespace
Section titled “Guarding a namespace”Most plugin routes should require a login, but a few, such as an
incoming webhook, cannot have one. Protect wraps a plugin’s
routes in your authentication middleware while letting its declared
public paths through:
for id, handler := range host.Routes() { prefix := "/api/plugins/" + id guarded := pluginkit.Protect(handler, host.PublicPaths()[id], auth.RequireSession) router.Mount(prefix, http.StripPrefix(prefix, guarded))}Three things to know about the matching:
- A public path must match exactly. There is no prefix or wildcard matching, so nothing is accidentally exposed.
- A match applies to every HTTP method.
- Public paths are written relative to the plugin’s namespace, so
/webhookand not the full URL. That is becauseStripPrefixhas already removed the prefix by the timeProtectsees the request.
The middleware is any func(http.Handler) http.Handler. The
example above uses RequireSession from
authkit.
Stop shuts plugins down in reverse registration order. If one
fails it keeps going and returns every error together, so a single
bad shutdown never leaves the rest running.
Call it before you close anything your plugins use, such as your database pool. Call it on both paths out of your program: the normal shutdown, and the one where your server returned an error.