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
}

Installing a Plugin

Stoa provides a CLI command to install plugins. Run it from the Stoa source directory:

bash
stoa plugin install n8n
# or with a full import path:
stoa plugin install github.com/stoa-hq/stoa-plugins/n8n

The command fetches the package, adds it to the auto-generated imports file, and rebuilds the binary. Restart Stoa afterwards to activate the plugin.

bash
stoa plugin list    # show installed plugins
stoa plugin remove n8n  # uninstall

See Installing Plugins for the full reference.

Self-registration

Plugins register themselves via init() — no changes to app.go are needed:

go
func init() {
    sdk.Register(New())
}

Stoa automatically initialises every plugin that called sdk.Register() on startup.

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
warehouse.before_createBefore warehouse creationYes
warehouse.after_createAfter warehouse creationNo
warehouse.before_updateBefore warehouse updateYes
warehouse.after_updateAfter warehouse updateNo
warehouse.before_deleteBefore warehouse deletionYes
warehouse.after_deleteAfter warehouse deletionNo
warehouse.before_stock_updateBefore manual stock changeYes
warehouse.after_stock_updateAfter manual stock changeNo
warehouse.after_stock_deductAfter order stock deductionNo

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 APACHE 2.0 License.