App Logo
Integrations

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 PluginContext with Api, EventBus, Middleware, and DB access.
  • Lifecycle control: Init, Migrations, Routes, and Close give 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 EventBus and 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, and Close.

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) in Init so p.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() and DatabaseHooks() 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.Options and return clear errors from Init.
  • Use the host Middleware for 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.

On this page