Skip to content

Plugin System

Stoa has a built-in plugin system that lets you extend the platform without modifying core code.

Plugins can:

  • React to events — e.g. send an email after an order
  • Prevent operations — e.g. validate before a cart change
  • Provide custom API endpoints
  • Access the database directly

Claude Code Skill

Stoa includes a Claude Code skill for plugin development. Run /plugin in Claude Code to activate it — it provides the full SDK reference, all hook constants, entity types, and ready-to-use templates.

Plugin Interface

Every plugin implements the sdk.Plugin interface from pkg/sdk:

go
type Plugin interface {
    Name() string
    Version() string
    Description() string
    Init(app *AppContext) error
    Shutdown() error
}

The Init method receives an AppContext with everything the plugin needs:

go
type AppContext struct {
    DB     *pgxpool.Pool
    Router chi.Router
    Hooks  *HookRegistry
    Config map[string]interface{}
    Logger zerolog.Logger
}

Example: Email on New Order

go
package orderemail

import (
    "context"
    "github.com/epoxx-arch/stoa/internal/domain/order"
    "github.com/epoxx-arch/stoa/pkg/sdk"
)

type Plugin struct{ logger zerolog.Logger }

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

func (p *Plugin) Name() string        { return "order-email" }
func (p *Plugin) Version() string     { return "1.0.0" }
func (p *Plugin) Description() string { return "Sends confirmation emails after orders" }
func (p *Plugin) Shutdown() error     { return nil }

func (p *Plugin) Init(app *sdk.AppContext) error {
    p.logger = app.Logger
    app.Hooks.On(sdk.HookAfterOrderCreate, func(ctx context.Context, event *sdk.HookEvent) error {
        o := event.Entity.(*order.Order)
        p.logger.Info().Str("order", o.OrderNumber).Msg("sending confirmation email")
        // send email here
        return nil
    })
    return nil
}

Example: Minimum Order Value

Before-hooks can prevent operations by returning an error:

go
func (p *Plugin) Init(app *sdk.AppContext) error {
    app.Hooks.On(sdk.HookBeforeCheckout, func(ctx context.Context, event *sdk.HookEvent) error {
        o := event.Entity.(*order.Order)
        if o.Total < 1000 { // prices in cents
            return fmt.Errorf("minimum order value: 10.00 EUR")
        }
        return nil
    })
    return nil
}

Example: Custom API Endpoints

go
func (p *Plugin) Init(app *sdk.AppContext) error {
    app.Router.Route("/api/v1/wishlist", func(r chi.Router) {
        r.Get("/", p.handleList)
        r.Post("/", p.handleAdd)
        r.Delete("/{id}", p.handleRemove)
    })
    return nil
}

Registering a Plugin

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

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

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

Available Hooks

HookTimingCan cancel?
product.before_createBefore product creationYes
product.after_createAfter product creationNo
product.before_updateBefore product updateYes
product.after_updateAfter product updateNo
product.before_deleteBefore product deletionYes
product.after_deleteAfter product deletionNo
order.before_createBefore order creationYes
order.after_createAfter order creationNo
order.before_updateBefore status changeYes
order.after_updateAfter status changeNo
cart.before_add_itemBefore adding to cartYes
cart.after_add_itemAfter adding to cartNo
cart.before_update_itemBefore quantity changeYes
cart.after_update_itemAfter quantity changeNo
cart.before_remove_itemBefore item removalYes
cart.after_remove_itemAfter item removalNo
customer.before_createBefore customer registrationYes
customer.after_createAfter customer registrationNo
customer.before_updateBefore customer updateYes
customer.after_updateAfter customer updateNo
category.before_createBefore category creationYes
category.after_createAfter category creationNo
category.before_updateBefore category updateYes
category.after_updateAfter category updateNo
category.before_deleteBefore category deletionYes
category.after_deleteAfter category deletionNo
checkout.beforeBefore checkout completionYes
checkout.afterAfter checkout completionNo
payment.after_completeAfter successful paymentNo
payment.after_failedAfter failed paymentNo

Before-hooks execute before the database operation and can cancel it by returning an error. After-hooks execute afterwards — errors are only logged and do not abort the operation.

Next Steps

Released under the MIT License.