Plugins
Extend GoBetterAuth functionality with custom plugins.
Plugins are the most powerful extensibility mechanism in GoBetterAuth — they let you hook into runtime events, extend the HTTP surface, add migrations, intercept database operations, and run your own background logic while reusing the platform's APIs, middleware and event bus.
This guide shows the two recommended plugin styles and explains when to use each: a concise inline option-based approach for small extensions and a full struct-embedding pattern for stateful, production-ready plugins.
Why use plugins?
- Extend behavior without forking: ship authentication, auditing, metrics, or integrations as independent modules.
- Reuse platform primitives: plugins get
PluginContextwithApi,EventBus,Middleware, and DB access. - Lifecycle control:
Init,Migrations,Routes, andClosegive you precise startup/shutdown handling. - Low friction: the host wires plugins into routing, DB migrations and the event bus.
Plugin overview (what a plugin can do)
- Register DB migrations to run alongside the host.
- Expose HTTP endpoints mounted under the auth prefix and protected by the host middleware.
- Subscribe to internal events via the
EventBusand publish or react to them. - Register database hooks to intercept CRUD operations.
- Provide rate-limiting configuration for plugin routes.
Each plugin implements the Plugin interface and receives a *PluginContext so it can collaborate with the host and other plugins.
Style A — Inline (functional options)
Best for: small utilities, prototypes, or when you want minimal functionality. Create a plugin with config.NewPlugin(...) and the WithPlugin* helpers.
Advantages:
- Fast to write and register.
- Great for simple routes, tiny event subscribers, or feature flags.
Example — inline plugin that adds a ping route and an init hook:
package main
import (
"log/slog"
"encoding/json"
"net/http"
gobetterauthconfig "github.com/GoBetterAuth/go-better-auth/config"
gobetterauthmodels "github.com/GoBetterAuth/go-better-auth/models"
)
func main() {
config := gobetterauthconfig.NewConfig(
// Other config options...
gobetterauthconfig.WithPlugins(
gobetterauthmodels.PluginsConfig{
Plugins: []gobetterauthmodels.Plugin{
gobetterauthconfig.NewPlugin(
gobetterauthconfig.WithPluginMetadata(gobetterauthmodels.PluginMetadata{
Name: "ping",
Version: "0.1.0",
Description: "A simple ping plugin",
}),
gobetterauthconfig.WithPluginConfig(gobetterauthmodels.PluginConfig{Enabled: true}),
gobetterauthconfig.WithPluginInit(func(ctx *gobetterauthmodels.PluginContext) error {
slog.Info("ping-inline initialized")
// You can access ctx.Api, ctx.EventBus, ctx.Config.DB, etc. here...
return nil
}),
gobetterauthconfig.WithPluginRoutes([]gobetterauthmodels.PluginRoute{
{
Method: "GET",
Path: "/ping",
Handler: func() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{"pong": true})
})
},
},
}),
),
},
},
),
)
}Tip: store plugin-specific settings in PluginConfig.Options and validate them in Init.
Style B — Struct-based (embed BasePlugin)
Best for: production plugins that maintain state, use services, need subscriptions, or include migrations. Embed models.BasePlugin and implement the methods you need.
Advantages:
- Clear separation between lifecycle, services, and handlers.
- Easier to maintain across multiple files.
- Full control over
Init,Migrations,Routes,DatabaseHooks,EventHooks,RateLimit, andClose.
Example — trimmed LoggerPlugin pattern:
type LoggerPlugin struct {
models.BasePlugin
service *LoggerService
logCount int
eventSubscriptions map[string]models.SubscriptionID
}
func NewLoggerPlugin(config models.PluginConfig) models.Plugin {
p := &LoggerPlugin{eventSubscriptions: make(map[string]models.SubscriptionID)}
p.SetConfig(config)
return p
}
func (p *LoggerPlugin) Init(ctx *models.PluginContext) error {
p.SetCtx(ctx)
p.service = NewLoggerService(ctx.Config.DB)
id, err := ctx.EventBus.Subscribe(models.EventUserSignedUp, func(c context.Context, e models.Event) error {
// decode payload, load user via p.Ctx().Api.Users, persist log via p.service
p.logCount++
return nil
})
if err != nil {
return err
}
p.eventSubscriptions[models.EventUserSignedUp] = id
return nil
}
func (p *LoggerPlugin) Routes() []models.PluginRoute {
return []models.PluginRoute{
{
Method: "GET",
Path: "/logger/count",
Middleware: []models.PluginRouteMiddleware{p.Ctx().Middleware.Auth()},
Handler: func() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
"logCount": p.logCount,
})
})
},
},
}
}
func (p *LoggerPlugin) Close() error {
for event, sub := range p.eventSubscriptions {
p.Ctx().EventBus.Unsubscribe(event, sub)
}
return nil
}Practical notes:
- Call
p.SetCtx(ctx)inInitsop.Ctx()is available to handlers and other methods. - Keep business logic in separate service types attached to the plugin (e.g.,
LoggerService). - Return migration structs from
Migrations()so the host can run them together with its own migrations. - Clean up subscriptions and goroutines in
Close()to avoid leaks.
How the host wires plugins
Typical host responsibilities:
- Instantiate plugins and pass a
PluginContext. - Call
Init(ctx)for each plugin. - Collect and run
Migrations(). - Mount
Routes()under the auth base path. - Apply
RateLimit()andDatabaseHooks()where appropriate. - Call
Close()on shutdown.
Hooks
You can deeply customise the behaviour of plugins by providing DatabaseHooks and EventHooks. These hooks allow you to intercept and extend both database operations and event-driven flows, making your plugin highly flexible and powerful.
Database Hooks
Database hooks let you run custom logic before or after database operations (such as CRUD or your own custom hooks) on your plugin's models. This is useful for validation, auditing, or side effects.
Example:
type LogEntryDatabaseHooks struct {
BeforeCreate func(entry *LogEntry) error
AfterCreate func(entry LogEntry) error
}
// Usage in plugin config:
LoggerPluginConfigOptions{
DatabaseHooks: &LogEntryDatabaseHooks{
BeforeCreate: func(entry *LogEntry) error {
// Custom validation or mutation before saving
return nil
},
AfterCreate: func(entry LogEntry) error {
// Side effects after saving
return nil
},
},
}To register these hooks, pass them in your plugin's config options and call SetDatabaseHooks() in your plugin constructor. The host will invoke these hooks at the appropriate points in the model lifecycle.
Event Hooks
Event hooks allow your plugin to react to internal events which you code yourself within your plugin and run custom logic when those events occur. This is ideal for integrations, notifications, or custom workflows.
Example:
type LogEntryEventHooks struct {
OnLogCreated func(entry LogEntry) error
}
// Usage in plugin config:
LoggerPluginConfigOptions{
EventHooks: &LogEntryEventHooks{
OnLogCreated: func(entry LogEntry) error {
// Notify, trigger webhooks, etc.
return nil
},
},
}Register event hooks by passing them in your plugin's config and calling SetEventHooks() in your constructor. In your event handler, you can invoke these hooks after your main logic:
// your main logic
logEntry, err := logEntryService.CreateLogEntry(...);
if err != nil {
return err
}
if plugin.logEntryEventHooks != nil && plugin.logEntryEventHooks.OnLogCreated != nil {
if err := plugin.logEntryEventHooks.OnLogCreated(*logEntry); err != nil {
return err
}
}Practical Example: LoggerPlugin
The following pattern demonstrates how to wire up both DatabaseHooks and EventHooks in a real plugin:
// plugins/logger/service.go
type LogEntry struct {
ID string `json:"id" gorm:"primaryKey"`
EventType string `json:"event_type"`
Details string `json:"details"`
CreatedAt time.Time `json:"created_at"`
}
type LoggerService struct {
db *gorm.DB
// other dependencies...
}
// rest of the logic for LoggerService...
// plugins/logger/plugin.go
type LoggerPlugin struct {
gobetterauthmodels.BasePlugin
service *LoggerService
logCount int
eventSubscriptions map[string]gobetterauthmodels.SubscriptionID
logEntryDatabaseHooks *LogEntryDatabaseHooks
logEntryEventHooks *LogEntryEventHooks
}
func NewLoggerPlugin(options LoggerPluginConfigOptions) gobetterauthmodels.Plugin {
plugin := &LoggerPlugin{
eventSubscriptions: make(map[string]gobetterauthmodels.SubscriptionID),
}
plugin.SetConfig(gobetterauthmodels.PluginConfig{
Enabled: true,
Options: options,
})
databaseHooks := options.DatabaseHooks
if databaseHooks == nil {
databaseHooks = &LogEntryDatabaseHooks{}
}
plugin.SetDatabaseHooks(databaseHooks)
eventHooks := options.EventHooks
if eventHooks == nil {
eventHooks = &LogEntryEventHooks{}
}
plugin.SetEventHooks(eventHooks)
return plugin
}
func (plugin *LoggerPlugin) Init(ctx *gobetterauthmodels.PluginContext) error {
// Store the context for later use
plugin.SetCtx(ctx)
// Initialise your plugin's service (handles business logic for your plugin)
plugin.service = NewLoggerService(ctx.Config.DB, ...)
// Example of subscribing to events from GoBetterAuth's system via the EventBus
var eventUserSignedUpSubId gobetterauthmodels.SubscriptionID
id, err := ctx.EventBus.Subscribe(
gobetterauthmodels.EventUserSignedUp,
func(ctx context.Context, event gobetterauthmodels.Event) error {
var data map[string]any
if err := json.Unmarshal(event.Payload, &data); err != nil {
slog.Error("failed to unmarshal json", "error", err)
return err
}
// Do something with the event data...
return nil
}
)
if err != nil {
return err
}
return nil
}
// Register database migrations for your plugin's models. The system will handle them alongside the host's migrations automatically.
func (plugin *LoggerPlugin) Migrations() []any {
return []any{&LogEntry{}}
}
// Define HTTP routes exposed by your plugin.
func (plugin *LoggerPlugin) Routes() []gobetterauthmodels.PluginRoute {
return []gobetterauthmodels.PluginRoute{
{
Method: "GET",
Path: "/logger/count",
Middleware: []gobetterauthmodels.PluginRouteMiddleware{
plugin.Ctx().Middleware.Auth(),
},
Handler: func() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
"logCount": plugin.logCount,
})
})
},
},
}
}
// Define rate limiting for your plugin's routes.
func (plugin *LoggerPlugin) RateLimit() *gobetterauthmodels.PluginRateLimit {
return &gobetterauthmodels.RateLimitConfig{
Enabled: true,
Window: 1 * time.Minute,
Max: 10,
}
}
// This is where you return the database hooks defined in your plugin config.
func (plugin *LoggerPlugin) DatabaseHooks() any {
return plugin.logEntryDatabaseHooks
}
// This is where you return the event hooks defined in your plugin config.
func (plugin *LoggerPlugin) EventHooks() any {
return plugin.logEntryEventHooks
}
// Clean up resources and unsubscribe from events when the plugin is closed.
func (plugin *LoggerPlugin) Close() error {
for eventType, subId := range plugin.eventSubscriptions {
plugin.Ctx().EventBus.Unsubscribe(eventType, subId)
}
slog.Info("LoggerPlugin closed and unsubscribed from events", "count", len(plugin.eventSubscriptions))
return nil
}This example showcases a highly flexible plugin architecture that leverages all aspects of the GoBetterAuth system to provide robust and extensible functionality.
Best practices
- Choose inline for small features, struct-based for anything non-trivial.
- Validate
PluginConfig.Optionsand return clear errors fromInit. - Use the host
Middlewarefor auth and CORS rather than reimplementing it. - Keep event handlers fast; delegate work to services or background jobs when needed.
- Protect public plugin routes with
RateLimit()and the built-in middleware.
