Skip to content

Creating a Plugin

This guide walks through building a plugin from scratch.

1. Create the directory

bash
mkdir -p plugins/myplugin

2. Implement the Plugin interface

Every plugin must implement sdk.Plugin from pkg/sdk:

go
package myplugin

import (
    "github.com/epoxx-arch/stoa/pkg/sdk"
)

type Plugin struct{}

func New() *Plugin { return &Plugin{} }

func (p *Plugin) Name() string        { return "my-plugin" }
func (p *Plugin) Version() string     { return "1.0.0" }
func (p *Plugin) Description() string { return "Does something useful" }
func (p *Plugin) Shutdown() error     { return nil }

func (p *Plugin) Init(app *sdk.AppContext) error {
    app.Logger.Info().Msg("my-plugin initialized")
    return nil
}

The Name() return value must be unique across all registered plugins.

3. Use the AppContext

Init receives an AppContext with everything your plugin needs:

go
type AppContext struct {
    DB     *pgxpool.Pool    // PostgreSQL connection pool
    Router chi.Router       // HTTP router for custom endpoints
    Hooks  *HookRegistry    // Event system
    Config map[string]interface{} // Plugin-specific config from config.yaml
    Logger zerolog.Logger   // Structured logger
}

Store what you need as fields on your plugin struct:

go
type Plugin struct {
    db     *pgxpool.Pool
    logger zerolog.Logger
}

func (p *Plugin) Init(app *sdk.AppContext) error {
    p.db = app.DB
    p.logger = app.Logger
    // ...
    return nil
}

4. Register the plugin

Add the plugin to internal/app/app.go:

go
import "github.com/epoxx-arch/stoa/plugins/myplugin"

func (a *App) RegisterPlugins() error {
    appCtx := &plugin.AppContext{
        DB:     a.DB.Pool,
        Router: a.Server.Router(),
        Logger: a.Logger,
    }
    return a.PluginRegistry.Register(myplugin.New(), appCtx)
}

5. Pass configuration (optional)

Plugin-specific config can be read from config.yaml and passed via AppContext.Config:

go
appCtx := &plugin.AppContext{
    // ...
    Config: map[string]interface{}{
        "webhook_url": cfg.Plugins["myplugin"]["webhook_url"],
    },
}

Inside the plugin:

go
func (p *Plugin) Init(app *sdk.AppContext) error {
    url, _ := app.Config["webhook_url"].(string)
    p.webhookURL = url
    return nil
}

Error handling

If Init returns an error, the plugin is not registered and the error is propagated — Stoa will not start.

If an after-hook handler returns an error, it is logged but does not abort the operation. Use before-hooks if you need to cancel an operation.

Next Steps

Released under the MIT License.