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.
| Interface | Method | Purpose |
|---|---|---|
HasRoutes | RegisterRoutes(r) | Mount HTTP routes |
HasMiddleware | Middleware() map | Named middleware for routes/groups |
HasConfig | DefaultConfig() map | Default configuration values |
HasMigrations | Migrations() []Migration | Database migrations |
HasViews | ViewsFS() fs.FS | Embedded view templates |
HasShutdown | Shutdown() error | Cleanup on app shutdown |
HasBindings | Bindings(c *container.Container) | Register DI container bindings |
HasCommands | Commands() []cli.Command | CLI commands (nimbus stripe:sync) |
HasSchedule | Schedule(s *schedule.Scheduler) | Periodic background tasks |
HasEvents | Listeners() map[string][]Listener | React to application events |
HasHealthChecks | HealthChecks() map[string]Check | Report 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:
- Create a Go module (e.g.
github.com/you/nimbus-stripe). - Implement
nimbus.Pluginand any capability interfaces. - Tag a version (
git tag v0.1.0). - Users install with
go get github.com/you/nimbus-stripe@latestand register withapp.Use().
Plugins vs Providers
| Feature | Provider | Plugin |
|---|---|---|
| Interface | Register + Boot | Name + Version + Register + Boot |
| Routes | Manual | Automatic via HasRoutes |
| Middleware | Manual | Automatic via HasMiddleware |
| Events | Manual | Automatic via HasEvents |
| Scheduled tasks | Manual | Automatic via HasSchedule |
| CLI commands | Manual | Automatic via HasCommands |
| Health checks | Not supported | Automatic via HasHealthChecks |
| DI bindings | Manual in Register() | Automatic via HasBindings |
| Shutdown | Not supported | Automatic via HasShutdown |
| CLI scaffold | No | nimbus make:plugin |
| Use case | Single service binding | Feature module (full integration) |