Plugins

Plugins extend your Nimbus application with reusable functionality β€” routes, middleware, config, events, scheduled tasks, CLI commands, health checks, and more β€” all packaged as a single Go struct. Build anything from a Stripe integration to a Sentry error tracker.

First-party plugins: nimbus plugin:install <name> or nimbus plugin install <name>; list names with nimbus plugin:list or nimbus plugin list (since Nimbus v1.0.0). Examples: supabase, telescope, horizon, pulse, reverb, transmit, drive, inertia, ai, mcp, unpoly, scout, nosql, socialite. See the sidebar under Plugins and Digging Deeper for docs.

Overview

A plugin is any struct that implements the nimbus.Plugin interface. Plugins are registered with app.Use() in bin/server.go and automatically integrated into the application lifecycle.

Plugins can optionally implement capability interfaces to hook into the framework at specific points. You only implement what your plugin needs β€” everything else is ignored.

Creating a plugin

Use the CLI to scaffold a plugin with a standard folder structure:

nimbus make:plugin Stripe

This generates a complete plugin skeleton:

stripe/
β”œβ”€β”€ plugin.go       # Core plugin, Register/Boot lifecycle
β”œβ”€β”€ config.go       # Configuration & defaults
β”œβ”€β”€ service.go      # Business logic & SDK wrapper
β”œβ”€β”€ routes.go       # HTTP route registration
β”œβ”€β”€ handlers.go     # HTTP handlers
β”œβ”€β”€ middleware.go    # Named middleware
β”œβ”€β”€ events.go       # Event listeners
β”œβ”€β”€ commands.go     # CLI commands
└── README.md       # Documentation

The Plugin interface

type Plugin interface {
    Name() string              // unique identifier, e.g. "stripe"
    Version() string           // semantic version, e.g. "1.0.0"
    Register(app *App) error   // bind services (don't resolve others yet)
    Boot(app *App) error       // resolve deps, initialise
}

Embed nimbus.BasePlugin to get default no-op implementations. Override only the methods you need.

Capability interfaces

Implement any of these optional interfaces to hook into the framework. All are automatically wired during boot.

InterfaceMethodPurpose
HasRoutesRegisterRoutes(r)Mount HTTP routes
HasMiddlewareMiddleware() mapNamed middleware for routes/groups
HasConfigDefaultConfig() mapDefault configuration values
HasMigrationsMigrations() []MigrationDatabase migrations
HasViewsViewsFS() fs.FSEmbedded view templates
HasShutdownShutdown() errorCleanup on app shutdown
HasBindingsBindings(c *container.Container)Register DI container bindings
HasCommandsCommands() []cli.CommandCLI commands (nimbus stripe:sync)
HasScheduleSchedule(s *schedule.Scheduler)Periodic background tasks
HasEventsListeners() map[string][]ListenerReact to application events
HasHealthChecksHealthChecks() map[string]CheckReport plugin health status

Real-world example: Stripe plugin

Here's how you'd build a Stripe integration that uses many capabilities:

plugin.go β€” Core lifecycle

 package stripe

import (
    "os"
    "github.com/CodeSyncr/nimbus"
    "github.com/CodeSyncr/nimbus/container"
)

var (
    _ nimbus.Plugin         = (*Plugin)(nil)
    _ nimbus.HasBindings    = (*Plugin)(nil)
    _ nimbus.HasRoutes      = (*Plugin)(nil)
    _ nimbus.HasEvents      = (*Plugin)(nil)
    _ nimbus.HasCommands    = (*Plugin)(nil)
    _ nimbus.HasHealthChecks = (*Plugin)(nil)
)

type Plugin struct {
    nimbus.BasePlugin
    client *StripeClient
}

func New() *Plugin {
    return &Plugin{
        BasePlugin: nimbus.BasePlugin{
            PluginName:    "stripe",
            PluginVersion: "1.0.0",
        },
    }
}

func (p *Plugin) Boot(app *nimbus.App) error {
    p.client = NewStripeClient(os.Getenv("STRIPE_SECRET_KEY"))
    return nil
}

service.go β€” Bindings (DI)

package stripe

import "github.com/CodeSyncr/nimbus/container"

func (p *Plugin) Bindings(c *container.Container) {
    c.Singleton("stripe", func() (*StripeClient, error) {
        return NewStripeClient(os.Getenv("STRIPE_SECRET_KEY")), nil
    })
}

type StripeClient struct {
    apiKey string
}

func NewStripeClient(apiKey string) *StripeClient {
    return &StripeClient{apiKey: apiKey}
}

func (c *StripeClient) CreateCheckout(priceID string) (string, error) {
    // Create Stripe checkout session…
    return "https://checkout.stripe.com/session_xxx", nil
}

Now any handler can resolve the client: app.Container.MustMake("stripe").(*stripe.StripeClient)

events.go β€” Event listeners

package stripe

import "github.com/CodeSyncr/nimbus/events"

func (p *Plugin) Listeners() map[string][]events.Listener {
    return map[string][]events.Listener{
        "order.placed":    {p.createCheckout},
        "payment.failed":  {p.handleFailedPayment},
    }
}

func (p *Plugin) createCheckout(payload any) error {
    order := payload.(*Order)
    url, err := p.client.CreateCheckout(order.PriceID)
    if err != nil { return err }
    // redirect user to url…
    return nil
}

func (p *Plugin) handleFailedPayment(payload any) error {
    // send notification, log, retry…
    return nil
}

Fire events from anywhere: app.Events.Dispatch("order.placed", order)

commands.go β€” CLI commands

package stripe

import "github.com/CodeSyncr/nimbus/cli"

func (p *Plugin) Commands() []cli.Command {
    return []cli.Command{
        &SyncCommand{plugin: p},
    }
}

type SyncCommand struct{ plugin *Plugin }
func (c *SyncCommand) Name() string        { return "stripe:sync" }
func (c *SyncCommand) Description() string { return "Sync products from Stripe" }
func (c *SyncCommand) Run(ctx *cli.Context) error {
    ctx.UI.Infof("Syncing products from Stripe…")
    // sync logic here
    ctx.UI.Successf("Done! Synced products.")
    return nil
}

Users run: nimbus stripe:sync

health.go β€” Health checks

package stripe

import (
    "context"
    "github.com/CodeSyncr/nimbus/health"
)

func (p *Plugin) HealthChecks() map[string]health.Check {
    return map[string]health.Check{
        "stripe": func(ctx context.Context) error {
            // ping Stripe API
            return p.client.Ping(ctx)
        },
    }
}

Events system

The application has a built-in event dispatcher on app.Events:

// Listen for events
app.Events.Listen("user.created", func(payload any) error {
    user := payload.(*models.User)
    return sendWelcomeEmail(user)
})

// Fire events synchronously (returns first error)
err := app.Events.Dispatch("user.created", user)

// Fire events asynchronously (runs in goroutines, errors are logged)
app.Events.DispatchAsync("analytics.track", trackData)

// Package-level helpers (use the global dispatcher)
events.Listen("user.created", handler)
events.Dispatch("user.created", user)

Task scheduling

Plugins can register periodic tasks via HasSchedule. The scheduler starts automatically when the app runs.

import (
    "context"
    "time"
    "github.com/CodeSyncr/nimbus/schedule"
)

func (p *Plugin) Schedule(s *schedule.Scheduler) {
    // Run every 5 minutes
    s.Every(5*time.Minute, "stripe-sync", func(ctx context.Context) error {
        return p.syncProducts(ctx)
    })

    // Run daily at 3 AM
    s.Daily("03:00", "stripe-reconcile", func(ctx context.Context) error {
        return p.reconcile(ctx)
    })

    // Convenience methods
    s.Hourly("cache-cleanup", cleanupHandler)
    s.EveryMinute("heartbeat", pingHandler)
}

Container bindings

Plugins register services via HasBindings. The container supports three binding types:

func (p *Plugin) Bindings(c *container.Container) {
    // Singleton β€” built once, reused
    c.Singleton("stripe", func() (*StripeClient, error) {
        return NewStripeClient(os.Getenv("STRIPE_KEY")), nil
    })

    // Bind β€” new instance every call
    c.Bind("mailer", func() (*Mailer, error) {
        return NewMailer(), nil
    })

    // Instance β€” pre-built value
    c.Instance("logger", myLogger)
}

// Resolve anywhere:
client := app.Container.MustMake("stripe").(*StripeClient)
// Check existence:
if app.Container.Has("stripe") { … }

Plugin lifecycle

Plugins participate in the full application boot sequence:

app.Run()
  └─ app.Boot()
       1. Provider.Register()      β€” all providers
       2. Plugin.Register()        β€” all plugins
          └─ HasBindings applied   β€” container bindings
       3. Plugin.DefaultConfig collected
       4. Provider.Boot()          β€” all providers
       5. Plugin.Boot()            β€” all plugins
       6. Plugin capabilities applied:
          β”œβ”€ HasRoutes          β†’ routes mounted
          β”œβ”€ HasMiddleware      β†’ named middleware merged
          β”œβ”€ HasCommands        β†’ CLI commands registered
          β”œβ”€ HasSchedule        β†’ tasks added to scheduler
          β”œβ”€ HasEvents          β†’ listeners registered
          └─ HasHealthChecks    β†’ checks added to health checker
       7. App-level boot hooks
  └─ Scheduler.Start()  (if tasks registered)
  └─ ListenAndServe()

app.Shutdown()
  └─ Scheduler.Stop()
  └─ HasShutdown.Shutdown() for each plugin (reverse order)

Registering plugins

Plugins are registered with app.Use(). In this Nimbus Starter project, they are registered inside the registerPlugins function within bin/server.go:

// bin/server.go
package bin

// ... imports ...

func registerPlugins(app *nimbus.App) {
    // 1. Configure and boot first-party plugins
    app.Use(horizon.NewWithOptions(horizon.Options{
        Config: &horizon.Config{
            Environments: toHorizonEnvs(config.Horizon.Environments),
            Defaults: horizon.SupervisorDefaults{
                Connection: config.Horizon.Defaults.Connection,
                Timeout:    config.Horizon.Defaults.Timeout,
                Tries:      config.Horizon.Defaults.Tries,
                Backoff:    config.Horizon.Defaults.Backoff,
            },
            Waits:    config.Horizon.Waits,
            Silenced: config.Horizon.Silenced,
        },
        RedisURL: config.Horizon.RedisURL,
    }))

    mcpPlugin := nimbusmcp.New()
    mcpPlugin.Web("/mcp/weather", appmcp.WeatherServer)

    transmitCfg := &transmit.Config{
        Path:         config.Transmit.Path,
        PingInterval: config.Transmit.PingInterval,
    }
    if config.Transmit.Transport == "redis" {
        if rt, err := transmit.NewRedisTransport(transmit.RedisTransportConfig{
            URL:     config.Transmit.Redis.URL,
            Channel: config.Transmit.Redis.Channel,
        }); err == nil {
            transmitCfg.Transport = rt
        }
    }

    shieldCfg := toShieldConfig(config.Shield)

    // 2. Load all plugins onto the application instance
    app.Use(
        shield.NewPlugin(shieldCfg),
        unpoly.New(),
        ai.New(),
        telescope.New(),
        transmit.New(transmitCfg),
        mcpPlugin,
        analytics.New(), // Custom local plugin
    )
}

Scaffolded Example: The Analytics Plugin

This project includes a fully functional, local plugin under app/plugins/analytics/. It demonstrates the basic capabilities (routes, middleware, configuration) and serves as an excellent starting point for writing your own plugins.

1. Core Entrypoint (plugin.go)

Defines the plugin struct and ensures compile-time compliance with the capability interfaces.

package analytics

import "github.com/CodeSyncr/nimbus"

// Compile-time interface checks.
var (
    _ nimbus.Plugin        = (*AnalyticsPlugin)(nil)
    _ nimbus.HasRoutes     = (*AnalyticsPlugin)(nil)
    _ nimbus.HasMiddleware = (*AnalyticsPlugin)(nil)
    _ nimbus.HasConfig     = (*AnalyticsPlugin)(nil)
)

type AnalyticsPlugin struct {
    nimbus.BasePlugin
}

func New() *AnalyticsPlugin {
    return &AnalyticsPlugin{
        BasePlugin: nimbus.BasePlugin{
            PluginName:    "analytics",
            PluginVersion: "0.1.0",
        },
    }
}

2. Routes Registration (routes.go)

Defines custom endpoints exposed by the plugin.

package analytics

import (
    "github.com/CodeSyncr/nimbus/http"
    "github.com/CodeSyncr/nimbus/router"
)

func (p *AnalyticsPlugin) RegisterRoutes(r *router.Router) {
    r.Get("/analytics/status", p.statusHandler)
}

func (p *AnalyticsPlugin) statusHandler(c *http.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "plugin":  p.Name(),
        "version": p.Version(),
        "status":  "ok",
    })
}

3. Named Middleware (middleware.go)

Exposes named middleware that can be applied to route definitions elsewhere in the application.

package analytics

import (
    "github.com/CodeSyncr/nimbus/http"
    "github.com/CodeSyncr/nimbus/router"
)

func (p *AnalyticsPlugin) Middleware() map[string]router.Middleware {
    return map[string]router.Middleware{
        "analytics": p.exampleMiddleware(),
    }
}

func (p *AnalyticsPlugin) exampleMiddleware() router.Middleware {
    return func(next router.HandlerFunc) router.HandlerFunc {
        return func(c *http.Context) error {
            // Log or execute analytics logic before running the handler
            err := next(c)
            // Execute analytics logic after running the handler
            return err
        }
    }
}

You can apply this middleware to any endpoint in start/routes.go like so:

// Resolve the named middleware from the app
app.Router.Get("/dashboard", dashboardHandler).Use(app.NamedMiddleware()["analytics"])

4. Configuration (config.go)

Defines default settings for the plugin which can be queried via the application configuration service.

package analytics

func (p *AnalyticsPlugin) DefaultConfig() map[string]any {
    return map[string]any{
        "enabled": true,
    }
}

Accessing plugins at runtime

// Get a specific plugin by name
p := app.Plugin("stripe")

// List all plugins
for _, p := range app.Plugins() {
    fmt.Printf("  %s v%s\n", p.Name(), p.Version())
}

// Access config, middleware, events, health
cfg := app.PluginConfig("stripe")
mw := app.NamedMiddleware()
app.Events.Dispatch("order.placed", order)
result := app.Health.Run(ctx)

Publishing a plugin

A plugin can live inside your app (app/plugins/) or be published as a standalone Go module:

  1. Create a Go module (e.g. github.com/you/nimbus-stripe).
  2. Implement nimbus.Plugin and any capability interfaces.
  3. Tag a version (git tag v0.1.0).
  4. Users install with go get github.com/you/nimbus-stripe@latest and register with app.Use().

Plugins vs Providers

FeatureProviderPlugin
InterfaceRegister + BootName + Version + Register + Boot
RoutesManualAutomatic via HasRoutes
MiddlewareManualAutomatic via HasMiddleware
EventsManualAutomatic via HasEvents
Scheduled tasksManualAutomatic via HasSchedule
CLI commandsManualAutomatic via HasCommands
Health checksNot supportedAutomatic via HasHealthChecks
DI bindingsManual in Register()Automatic via HasBindings
ShutdownNot supportedAutomatic via HasShutdown
CLI scaffoldNonimbus make:plugin
Use caseSingle service bindingFeature module (full integration)