Extensions System

Build and register modular extensions

Forge's extension system provides a standardized way to add capabilities to your application. Extensions are modular, lifecycle-aware components that integrate with the DI container, router, health checks, and metrics. Forge ships with 28+ official extensions, and you can build your own.

Extension Interface

Every extension implements the core Extension interface:

type Extension interface {
    // Identity
    Name() string
    Version() string
    Description() string

    // Lifecycle
    Register(app App) error       // Register services with DI container
    Start(ctx context.Context) error  // Start the extension
    Stop(ctx context.Context) error   // Graceful shutdown
    Health(ctx context.Context) error // Health check (nil = healthy)

    // Dependencies
    Dependencies() []string       // Names of required extensions
}

Lifecycle Order

Register

Called before Start(). Register services with the DI container, set up configuration, and declare dependencies. All extensions are registered before any are started.

func (e *MyExtension) Register(app forge.App) error {
    // Access core services
    e.logger = app.Logger()
    e.metrics = app.Metrics()

    // Register services with DI container
    forge.RegisterSingleton[*MyService](app.Container(), "myService",
        func(c forge.Container) (*MyService, error) {
            return NewMyService(e.config), nil
        },
    )

    return nil
}

Start

Called after all extensions are registered and the DI container has started. Start background tasks, open connections, and begin serving.

func (e *MyExtension) Start(ctx context.Context) error {
    // Resolve services from DI
    e.service = forge.Must[*MyService](e.app.Container(), "myService")

    // Start background tasks
    go e.service.Run(ctx)

    return nil
}

Health

Called periodically by the health check system. Return nil for healthy, error for unhealthy.

func (e *MyExtension) Health(ctx context.Context) error {
    if !e.service.IsReady() {
        return fmt.Errorf("service not ready")
    }
    return nil
}

Stop

Called during graceful shutdown. Extensions are stopped in reverse dependency order.

func (e *MyExtension) Stop(ctx context.Context) error {
    return e.service.Close()
}

BaseExtension Helper

BaseExtension provides default implementations for all Extension methods. Embed it to avoid boilerplate.

type MyExtension struct {
    *forge.BaseExtension
    config MyConfig
    client *MyClient
}

func NewMyExtension(config MyConfig) forge.Extension {
    return &MyExtension{
        BaseExtension: forge.NewBaseExtension(
            "my-extension",  // name
            "1.0.0",         // version
            "Does something useful", // description
        ),
        config: config,
    }
}

BaseExtension provides:

MethodDefault Behavior
Name()Returns configured name
Version()Returns configured version
Description()Returns configured description
Dependencies()Returns empty slice
Register(app)Stores app reference, sets logger and metrics
Start(ctx)Marks extension as started
Stop(ctx)Marks extension as stopped
Health(ctx)Returns nil (healthy)

Additional helpers on BaseExtension:

e.Logger()              // Access extension's logger
e.Metrics()             // Access extension's metrics
e.App()                 // Access the app instance
e.IsStarted()           // Check if extension is started
e.LoadConfig(...)       // Load config from ConfigManager
e.RegisterConstructor(fn) // Register a service constructor

Registering Extensions

app := forge.New(
    forge.WithExtensions(
        database.NewExtension(database.Config{DSN: "postgres://..."}),
        cache.NewExtension(cache.Config{Backend: "redis"}),
        NewMyExtension(MyConfig{Port: 9090}),
    ),
)
app := forge.New()
app.RegisterExtension(database.NewExtension(dbConfig))
app.RegisterExtension(cache.NewExtension(cacheConfig))

Optional Interfaces

Extensions can implement additional interfaces for enhanced capabilities.

ConfigurableExtension

For extensions that accept runtime configuration changes.

type ConfigurableExtension interface {
    Extension
    Configure(config any) error
}
func (e *MyExtension) Configure(config any) error {
    cfg, ok := config.(*MyConfig)
    if !ok {
        return fmt.Errorf("invalid config type")
    }
    e.config = *cfg
    return e.reconnect()
}

ObservableExtension

For extensions that expose custom metrics.

type ObservableExtension interface {
    Extension
    Metrics() map[string]any
}
func (e *MyExtension) Metrics() map[string]any {
    return map[string]any{
        "connections":    e.pool.ActiveCount(),
        "idle":           e.pool.IdleCount(),
        "wait_duration":  e.pool.WaitDuration().String(),
    }
}

HotReloadableExtension

For extensions that support configuration reload without restart.

type HotReloadableExtension interface {
    Extension
    Reload(ctx context.Context) error
}
func (e *MyExtension) Reload(ctx context.Context) error {
    e.Logger().Info("reloading configuration")
    newConfig, err := e.loadLatestConfig()
    if err != nil {
        return err
    }
    e.config = newConfig
    return e.reconnect()
}

MiddlewareExtension

For extensions that provide global middleware applied to all routes.

type MiddlewareExtension interface {
    Extension
    Middlewares() []forge.Middleware
}
func (e *AuthExtension) Middlewares() []forge.Middleware {
    return []forge.Middleware{
        e.authMiddleware(),
        e.rateLimitMiddleware(),
    }
}

DependencySpecExtension

For extensions that need fine-grained dependency control (eager, lazy, optional).

type DependencySpecExtension interface {
    Extension
    DepsSpec() []forge.Dep
}
func (e *QueueExtension) DepsSpec() []forge.Dep {
    deps := []forge.Dep{
        forge.DepEagerSpec("database"),    // Must be ready before start
    }
    if e.config.UseRedis {
        deps = append(deps, forge.DepLazySpec("cache")) // Resolve on first use
    }
    return deps
}

InternalExtension

For extensions whose routes should be excluded from API documentation (OpenAPI, AsyncAPI).

type InternalExtension interface {
    ExcludeFromSchemas() bool
}
func (e *DebugExtension) ExcludeFromSchemas() bool {
    return true // Routes won't appear in OpenAPI/AsyncAPI specs
}

RunnableExtension

For extensions that manage long-running processes, goroutines, or external applications.

type RunnableExtension interface {
    Extension
    Run(ctx context.Context) error      // Start long-running processes
    Shutdown(ctx context.Context) error // Stop them
}
func (e *WorkerExtension) Run(ctx context.Context) error {
    go e.processJobs(ctx)
    return nil
}

func (e *WorkerExtension) Shutdown(ctx context.Context) error {
    close(e.quit)
    return nil
}

MigratableExtension

For extensions that provide database migrations. Extensions implementing this interface are auto-discovered by cli.RunApp() and exposed as migrate up, migrate down, and migrate status CLI commands.

type MigratableExtension interface {
    Extension

    // Migrate runs all pending migrations forward.
    Migrate(ctx context.Context) (*MigrationResult, error)

    // Rollback rolls back the last batch of applied migrations.
    Rollback(ctx context.Context) (*MigrationResult, error)

    // MigrationStatus returns the current state of all migrations.
    MigrationStatus(ctx context.Context) ([]*MigrationGroupInfo, error)
}

The interface uses three database-agnostic types for reporting:

TypeFieldsDescription
MigrationResultApplied int, RolledBack int, Names []stringOutcome of a Migrate or Rollback operation
MigrationGroupInfoName string, Applied []*MigrationInfo, Pending []*MigrationInfoStatus of all migrations in a group
MigrationInfoName, Version, Group, Comment, Applied bool, AppliedAtA single migration's metadata and state

Example implementation:

func (e *MyDBExtension) Migrate(ctx context.Context) (*forge.MigrationResult, error) {
    result, err := e.orchestrator.Migrate(ctx)
    if err != nil {
        return nil, err
    }
    names := make([]string, len(result.Applied))
    for i, m := range result.Applied {
        names[i] = m.Group + "/" + m.Name
    }
    return &forge.MigrationResult{Applied: len(result.Applied), Names: names}, nil
}

func (e *MyDBExtension) Rollback(ctx context.Context) (*forge.MigrationResult, error) {
    result, err := e.orchestrator.Rollback(ctx)
    if err != nil {
        return nil, err
    }
    return &forge.MigrationResult{RolledBack: len(result.Rollback)}, nil
}

func (e *MyDBExtension) MigrationStatus(ctx context.Context) ([]*forge.MigrationGroupInfo, error) {
    // Convert your internal migration status to forge types...
    return groups, nil
}

Grove implements this out of the box. When you use the Grove extension with WithMigrations(), it automatically implements MigratableExtension — no manual wiring needed. See Grove Forge Extension.

CLICommandProvider

For extensions that contribute CLI commands when the app is wrapped with cli.RunApp().

type CLICommandProvider interface {
    Extension

    // CLICommands returns CLI commands contributed by this extension.
    // Each element in the returned slice must implement cli.Command.
    CLICommands() []any
}

The return type is []any (rather than []cli.Command) to avoid a circular import. The CLI wrapper performs type assertions at registration time.

func (e *MyExtension) CLICommands() []any {
    return []any{
        cli.NewCommand("seed", "Seed the database", e.handleSeed),
        cli.NewCommand("dump", "Dump database schema", e.handleDump),
    }
}

Each element in the returned slice must implement cli.Command. Values that don't will be skipped with a warning log.

Building a Custom Extension

Here is a complete example of a custom extension that provides a rate limiter.

package ratelimit

import (
    "context"
    "fmt"
    "sync"
    "time"

    "github.com/xraph/forge"
)

type Config struct {
    RequestsPerMinute int
    BurstSize         int
}

func DefaultConfig() Config {
    return Config{
        RequestsPerMinute: 60,
        BurstSize:         10,
    }
}

type RateLimiter struct {
    config  Config
    clients map[string]*clientState
    mu      sync.RWMutex
}

type clientState struct {
    tokens    int
    lastReset time.Time
}

func NewRateLimiter(config Config) *RateLimiter {
    return &RateLimiter{
        config:  config,
        clients: make(map[string]*clientState),
    }
}

func (rl *RateLimiter) Allow(clientIP string) bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    state, ok := rl.clients[clientIP]
    if !ok {
        state = &clientState{tokens: rl.config.BurstSize, lastReset: time.Now()}
        rl.clients[clientIP] = state
    }

    // Refill tokens
    elapsed := time.Since(state.lastReset)
    refill := int(elapsed.Minutes()) * rl.config.RequestsPerMinute
    if refill > 0 {
        state.tokens = min(state.tokens+refill, rl.config.BurstSize)
        state.lastReset = time.Now()
    }

    if state.tokens > 0 {
        state.tokens--
        return true
    }
    return false
}

// Extension wraps the rate limiter as a Forge extension
type Extension struct {
    *forge.BaseExtension
    config  Config
    limiter *RateLimiter
}

func NewExtension(config Config) forge.Extension {
    return &Extension{
        BaseExtension: forge.NewBaseExtension(
            "rate-limiter",
            "1.0.0",
            "Token bucket rate limiter",
        ),
        config: config,
    }
}

func (e *Extension) Register(app forge.App) error {
    if err := e.BaseExtension.Register(app); err != nil {
        return err
    }

    // Load config from ConfigManager (file/env), falling back to programmatic config
    finalConfig := DefaultConfig()
    if err := e.LoadConfig("rate-limiter", &finalConfig, e.config, DefaultConfig(), false); err != nil {
        return err
    }
    e.config = finalConfig

    // Create the rate limiter
    e.limiter = NewRateLimiter(e.config)

    // Register in DI container
    return forge.RegisterValue[*RateLimiter](app.Container(), "rateLimiter", e.limiter)
}

func (e *Extension) Start(ctx context.Context) error {
    e.BaseExtension.Start(ctx)
    e.Logger().Info("rate limiter started",
        forge.Int("requests_per_minute", e.config.RequestsPerMinute),
        forge.Int("burst_size", e.config.BurstSize),
    )
    return nil
}

func (e *Extension) Health(ctx context.Context) error {
    if e.limiter == nil {
        return fmt.Errorf("rate limiter not initialized")
    }
    return nil
}

// Implement MiddlewareExtension to apply globally
func (e *Extension) Middlewares() []forge.Middleware {
    return []forge.Middleware{
        func(next forge.Handler) forge.Handler {
            return func(ctx forge.Context) error {
                clientIP := ctx.Request().RemoteAddr
                if !e.limiter.Allow(clientIP) {
                    return forge.NewHTTPError(429, "rate limit exceeded")
                }
                return next(ctx)
            }
        },
    }
}

Using the Extension

app := forge.New(
    forge.WithExtensions(
        ratelimit.NewExtension(ratelimit.Config{
            RequestsPerMinute: 100,
            BurstSize:         20,
        }),
    ),
)

Extension Route Registration

Extensions can register routes during Start(). Use WithExtensionExclusion to automatically respect InternalExtension.

func (e *MyExtension) Start(ctx context.Context) error {
    e.BaseExtension.Start(ctx)

    r := e.App().Router()
    opts := forge.ExtensionRoutes(e) // Auto-applies schema exclusion if internal

    r.GET("/my-ext/status", e.statusHandler, opts...)
    r.GET("/my-ext/metrics", e.metricsHandler, opts...)

    return nil
}

External Application Extensions

Manage external processes (Redis, Elasticsearch, etc.) as Forge extensions using ExternalAppExtension.

redisExt := forge.NewExternalAppExtension(forge.ExternalAppConfig{
    Name:             "redis-server",
    Command:          "redis-server",
    Args:             []string{"--port", "6379"},
    RestartOnFailure: true,
    RestartDelay:     5 * time.Second,
    ShutdownTimeout:  30 * time.Second,
    ForwardOutput:    true,
})

app := forge.New(
    forge.WithExtensions(redisExt),
)

Querying Extensions

// List all registered extensions
for _, ext := range app.Extensions() {
    fmt.Printf("%s v%s\n", ext.Name(), ext.Version())
}

// Get a specific extension
dbExt, err := app.GetExtension("database")
if err != nil {
    // Extension not registered
}

How is this guide?

On this page