Grove

Forge Extension

Use Grove as a first-class Forge extension with YAML configuration, DI, migrations, CRDT sync, multi-database support, and lifecycle management.

Grove provides a first-class Forge extension that handles configuration loading, driver creation, dependency injection, and lifecycle management. It follows the canonical Forge extension pattern with BaseExtension embedding.

Installation

go get github.com/xraph/grove/extension

Quick Start

Programmatic Configuration

import (
    "github.com/xraph/forge"
    "github.com/xraph/grove/drivers/pgdriver"
    groveext "github.com/xraph/grove/extension"
)

pgdb := pgdriver.New()
pgdb.Open(ctx, "postgres://user:pass@localhost:5432/mydb")

app := forge.New()
app.Use(groveext.New(
    groveext.WithDriver(pgdb),
))
app.Start(ctx)

YAML Configuration

The extension reads configuration from your Forge config file:

# config.yaml
extensions:
  grove:
    driver: postgres
    dsn: "postgres://user:pass@localhost:5432/mydb"
app := forge.New()
app.Use(groveext.New()) // Config loaded from YAML automatically
app.Start(ctx)

When both YAML and programmatic options are provided, YAML takes precedence for driver/dsn fields, while programmatic options fill in any gaps.

Configuration Reference

The Config struct supports the following fields, all configurable via YAML or programmatic options:

FieldYAML KeyTypeDescription
DriverdriverstringDriver name: postgres, sqlite, mysql, mongodb, turso, clickhouse
DSNdsnstringData source name / connection string
Databasesdatabases[]DatabaseConfigNamed database connections for multi-DB mode
DefaultdefaultstringDefault database name when using multi-DB
DisableRoutesdisable_routesboolSkip CRDT sync route registration
DisableMigratedisable_migrateboolSkip automatic migration execution
BasePathbase_pathstringURL prefix for CRDT sync routes (default: /sync)

Each entry in Databases has:

FieldYAML KeyTypeDescription
NamenamestringUnique identifier for this database
DriverdriverstringDriver name
DSNdsnstringConnection string

YAML configuration can use either key:

# Namespaced (preferred)
extensions:
  grove:
    driver: postgres
    dsn: "postgres://..."

# Legacy / top-level
grove:
  driver: postgres
  dsn: "postgres://..."

Options

Single-DB Options

groveext.New(
    // Driver — set a pre-configured driver instance (takes precedence over YAML).
    groveext.WithDriver(pgdb),

    // DSN — set driver name + connection string programmatically.
    groveext.WithDSN("postgres", "postgres://user:pass@localhost/mydb"),

    // Migrations — register migration groups.
    groveext.WithMigrations(billing.Migrations, notifications.Migrations),

    // Hooks — register lifecycle hooks with optional scope.
    groveext.WithHook(&tenantHook, hook.Scope{Tables: []string{"orders"}}),

    // Behavior flags.
    groveext.WithDisableMigrate(),   // Skip auto-migration
    groveext.WithDisableRoutes(),    // Skip CRDT sync route registration
    groveext.WithBasePath("/api/sync"), // Custom CRDT route prefix

    // Config loading.
    groveext.WithRequireConfig(true), // Error if no YAML config found

    // CRDT integration (see below).
    groveext.WithCRDT(crdtPlugin),
    groveext.WithSyncer(syncer),
    groveext.WithSyncController(crdt.WithStreamPollInterval(2 * time.Second)),
)

Multi-DB Options

groveext.New(
    // Add named databases with pre-configured drivers.
    groveext.WithDatabase("primary", pgdb),
    groveext.WithDatabase("analytics", chdb),

    // Or add named databases using driver name + DSN (resolved from registry).
    groveext.WithDatabaseDSN("cache", "sqlite", "file:cache.db"),

    // Set which database is the default (used by DB() and unnamed DI).
    groveext.WithDefaultDatabase("primary"),

    // Per-database hooks.
    groveext.WithHookFor("primary", &tenantHook, hook.Scope{Tables: []string{"orders"}}),
    groveext.WithHookFor("analytics", &readOnlyHook),

    // Per-database migrations.
    groveext.WithMigrationsFor("primary", core.Migrations, billing.Migrations),
    groveext.WithMigrationsFor("analytics", analytics.Migrations),

    // Global hooks (applied to ALL databases).
    groveext.WithHook(&auditHook),

    // CRDT on a specific database.
    groveext.WithCRDT(crdtPlugin),
    groveext.WithCRDTDatabase("primary"),
)

Lifecycle

Register Phase

  1. Calls BaseExtension.Register() to store the Forge app, logger, and metrics
  2. Loads configuration: YAML (extensions.grovegrove) → merge with programmatic → defaults
  3. Routes to single-DB or multi-DB initialization based on configuration
  4. Opens the database(s) and registers hooks
  5. Provides database instances in the Forge DI container via Vessel
  6. Registers CRDT sync controller if enabled (and routes not disabled)

Start Phase

  1. Starts the CRDT background syncer if configured
  2. Marks the extension as started

Stop Phase

  1. In single-DB mode: closes the Grove DB
  2. In multi-DB mode: closes all databases via the DBManager
  3. Marks the extension as stopped

Health Check

  • In single-DB mode: returns an error if the DB has not been initialized
  • In multi-DB mode: checks all registered databases and returns the first error

Accessing the DB

After registration, resolve the DB from any Forge handler:

func myHandler(ctx forge.Context) error {
    db := forge.Inject[*grove.DB](ctx)

    pgdb := pgdriver.Unwrap(db)
    var users []User
    err := pgdb.NewSelect(&users).
        Where("active = $1", true).
        OrderExpr("created_at DESC").
        Limit(10).
        Scan(ctx.Request().Context())
    if err != nil {
        return err
    }

    return ctx.JSON(200, users)
}

Multi-Database Support

The extension supports managing multiple named database connections. This is useful when your application needs different databases for different concerns — for example, a primary Postgres for relational data, a ClickHouse for analytics, or a SQLite for local cache.

Programmatic Setup

import (
    "github.com/xraph/forge"
    "github.com/xraph/grove/drivers/pgdriver"
    "github.com/xraph/grove/drivers/clickhousedriver"
    groveext "github.com/xraph/grove/extension"
)

pgdb := pgdriver.New()
pgdb.Open(ctx, "postgres://user:pass@localhost:5432/app")

chdb := clickhousedriver.New()
chdb.Open(ctx, "clickhouse://localhost:9000/analytics")

app := forge.New()
app.Use(groveext.New(
    groveext.WithDatabase("primary", pgdb),
    groveext.WithDatabase("analytics", chdb),
    groveext.WithDefaultDatabase("primary"),
))
app.Start(ctx)

YAML Configuration

extensions:
  grove:
    databases:
      - name: primary
        driver: postgres
        dsn: "postgres://user:pass@localhost:5432/app"
      - name: analytics
        driver: clickhouse
        dsn: "clickhouse://localhost:9000/analytics"
      - name: cache
        driver: sqlite
        dsn: "file:cache.db"
    default: primary
app := forge.New()
app.Use(groveext.New()) // Multi-DB config loaded from YAML
app.Start(ctx)

Accessing Named Databases

Use Vessel's named injection to resolve specific databases:

import (
    "github.com/xraph/grove"
    "github.com/xraph/vessel"
    groveext "github.com/xraph/grove/extension"
)

func analyticsHandler(ctx forge.Context) error {
    // Get the default database (unnamed injection).
    defaultDB := forge.Inject[*grove.DB](ctx)

    // Get a specific named database.
    analyticsDB, err := vessel.InjectNamed[*grove.DB](ctx.App().Container(), "analytics")
    if err != nil {
        return err
    }

    // Get the DBManager for dynamic access.
    mgr := forge.Inject[*groveext.DBManager](ctx)

    // Access any database by name.
    cacheDB, err := mgr.Get("cache")
    if err != nil {
        return err
    }

    _ = defaultDB
    _ = analyticsDB
    _ = cacheDB
    return nil
}

DI Registration

In multi-DB mode, the extension registers three types of services in the DI container:

ServiceResolutionDescription
*grove.DBvessel.Inject[*grove.DB]()The default database (backward compatible)
*grove.DB (named)vessel.InjectNamed[*grove.DB](c, "name")A specific named database
*extension.DBManagervessel.Inject[*extension.DBManager]()The manager for dynamic access

DBManager API

The DBManager is the central type for multi-database management:

mgr := ext.Manager() // nil in single-DB mode

// Get a named database.
db, err := mgr.Get("analytics")

// Get the default database.
db, err := mgr.Default()

// Get the default database name.
name := mgr.DefaultName()

// Change the default at runtime.
err := mgr.SetDefault("analytics")

// Get all databases as a map (returns a copy).
all := mgr.All()

// Get the count of registered databases.
n := mgr.Len()

// Close all databases.
err := mgr.Close()

Per-Database Hooks

Hooks can be scoped to specific databases or applied globally:

groveext.New(
    groveext.WithDatabase("primary", pgdb),
    groveext.WithDatabase("analytics", chdb),

    // Global hook — applied to ALL databases.
    groveext.WithHook(&auditHook),

    // Per-database hooks — applied to a specific database only.
    groveext.WithHookFor("primary", &tenantIsolation, hook.Scope{
        Tables:   []string{"orders", "invoices"},
        Priority: 1,
    }),
    groveext.WithHookFor("analytics", &readOnlyEnforcer),
)

Per-Database Migrations

Migration groups can target specific databases:

groveext.New(
    groveext.WithDatabase("primary", pgdb),
    groveext.WithDatabase("analytics", chdb),

    groveext.WithMigrationsFor("primary", core.Migrations, billing.Migrations),
    groveext.WithMigrationsFor("analytics", analytics.Migrations),
)

Backward Compatibility

The multi-database feature is fully backward compatible:

  • Single-DB mode (WithDriver / WithDSN) works exactly as before
  • ext.DB() always returns the default database in both modes
  • ext.Manager() returns nil in single-DB mode, non-nil in multi-DB mode
  • Unnamed DI injection (vessel.Inject[*grove.DB]()) resolves the default database in both modes

CRDT with Multi-DB

When using CRDT with multiple databases, specify which database should receive CRDT hooks:

groveext.New(
    groveext.WithDatabase("primary", pgdb),
    groveext.WithDatabase("analytics", chdb),

    groveext.WithCRDT(crdtPlugin, hook.Scope{Tables: []string{"documents"}}),
    groveext.WithCRDTDatabase("primary"), // CRDT hooks on primary only
    groveext.WithSyncer(syncer),
)

If WithCRDTDatabase is not specified, CRDT hooks are applied to the default database.

Driver Registry

Grove provides a driver registry that allows creating drivers by name. This powers the YAML configuration path — when no WithDriver() is provided, the extension looks up the driver by name and creates it with the DSN.

Registering a Driver

Each driver module can register its factory:

// In your driver package or application init
import "github.com/xraph/grove"

func init() {
    grove.RegisterDriver("postgres", func(ctx context.Context, dsn string) (grove.GroveDriver, error) {
        db := pgdriver.New()
        if err := db.Open(ctx, dsn); err != nil {
            return nil, err
        }
        return db, nil
    })
}

Using the Registry

Once registered, drivers can be created by name:

drv, err := grove.OpenDriver(ctx, "postgres", "postgres://user:pass@localhost/mydb")

The extension uses this automatically when YAML config provides driver and dsn, including for each entry in the databases list.

Hooks

Register hooks during extension setup or access them from the DB:

// During setup
app.Use(groveext.New(
    groveext.WithDriver(pgdb),
    groveext.WithHook(&TenantIsolation{}, hook.Scope{
        Tables:   []string{"orders", "invoices"},
        Priority: 1,
    }),
    groveext.WithHook(&AuditLogger{}, hook.Scope{
        Priority: 100,
    }),
))

// Or from a handler
func setupHooks(ctx forge.Context) error {
    db := forge.Inject[*grove.DB](ctx)
    db.Hooks().AddHook(&CustomHook{}, hook.Scope{Priority: 50})
    return nil
}

CRDT Integration

The extension supports CRDT-based conflict-free replication with automatic sync route registration:

import (
    "github.com/xraph/grove/crdt"
    groveext "github.com/xraph/grove/extension"
)

plugin := crdt.NewPlugin(crdt.PluginConfig{
    NodeID: "node-1",
    Tables: []string{"documents", "comments"},
})

syncer := crdt.NewSyncer(plugin, crdt.SyncerConfig{
    Peers: []string{"http://node-2:8080", "http://node-3:8080"},
})

app.Use(groveext.New(
    groveext.WithDriver(pgdb),
    groveext.WithCRDT(plugin, hook.Scope{Tables: []string{"documents", "comments"}}),
    groveext.WithSyncer(syncer),
    groveext.WithSyncController(
        crdt.WithStreamPollInterval(2 * time.Second),
        crdt.WithStreamKeepAlive(30 * time.Second),
    ),
))

This registers three sync endpoints:

MethodPathDescription
POST/sync/pullRemote nodes pull changes from this node
POST/sync/pushRemote nodes push changes to this node
GET/sync/streamSSE stream of real-time changes

Use WithBasePath() to change the route prefix, or WithDisableRoutes() to skip route registration entirely (useful when running the CRDT plugin without HTTP sync).

Migration Composition

When multiple Forge extensions register migration groups, they compose automatically:

app.Use(groveext.New(
    groveext.WithDriver(pgdb),
    groveext.WithMigrations(core.Migrations),
))
app.Use(billingExt.New())        // billing migrations
app.Use(notificationsExt.New())  // notification migrations

// At startup: core -> billing -> notifications (topological order)

CLI Migration Commands

Grove implements forge.MigratableExtension, so when your app is wrapped with cli.RunApp() you get migration CLI commands for free:

package main

import (
    "github.com/xraph/forge"
    "github.com/xraph/forge/cli"

    groveext "github.com/xraph/grove/extension"
    _ "github.com/xraph/grove/drivers/pgdriver/pgmigrate" // register pg executor

    "myapp/migrations/core"
)

func main() {
    grove := groveext.New(
        groveext.WithDSN("postgres://localhost:5432/myapp"),
        groveext.WithMigrations(core.Migrations),
    )

    app := forge.New(
        forge.WithAppName("myapp"),
        forge.WithExtensions(grove),
    )

    cli.RunApp(app)
}

Available Commands

CommandDescription
myapp migrate upRun all pending migrations
myapp migrate downRollback the last migration batch (with confirmation prompt)
myapp migrate down --forceRollback without confirmation
myapp migrate statusShow applied/pending migrations per group

Auto-Migration on Serve

Enable WithAutoMigrate() to run pending migrations automatically before the HTTP server starts:

cli.RunApp(app, cli.WithAutoMigrate())

Migrations run during PhaseBeforeRun (after extensions are initialized, before accepting requests).

Multi-Database Mode

When using multi-database mode, migration names are prefixed with the database name for clarity:

$ myapp migrate status
  Group: analytics:events
  ┌────────────────┬──────────────────┬─────────┐
 Version Name Status
  ├────────────────┼──────────────────┼─────────┤
 20240101000000 create_events applied
  └────────────────┴──────────────────┴─────────┘

  Group: core
  ┌────────────────┬──────────────────┬─────────┐
 Version Name Status
  ├────────────────┼──────────────────┼─────────┤
 20240101000000 create_users applied
 20240201000000 add_email_index pending
  └────────────────┴──────────────────┴─────────┘

Executor Auto-Registration

Make sure to import the migrate package for your driver so the executor factory is registered:

import _ "github.com/xraph/grove/drivers/pgdriver/pgmigrate"     // PostgreSQL
import _ "github.com/xraph/grove/drivers/mysqldriver/mysqlmigrate" // MySQL
import _ "github.com/xraph/grove/drivers/sqlitedriver/sqlitemigrate" // SQLite

If you see no executor registered for driver "pg", you're missing the driver's migrate package import. Add the blank import for your driver as shown above.

KV Store Integration

The KV module can also be used with Forge. Register a *kv.Store in the DI container alongside or independently of *grove.DB:

import (
    "github.com/xraph/grove/kv"
    "github.com/xraph/grove/kv/drivers/redisdriver"
    "github.com/xraph/grove/kv/codec"
)

// In your Forge extension's Register phase:
store, _ := kv.Open(redisdriver.New(), "redis://localhost:6379",
    kv.WithCodec(codec.MsgPack{}),
)
vessel.Provide(app, store) // Now injectable as *kv.Store

// In a handler:
func myHandler(ctx forge.Context) error {
    store := forge.Inject[*kv.Store](ctx)
    var user User
    err := store.Get(ctx.Request().Context(), "user:123", &user)
    // ...
}

The KV store reuses the same grove/hook system, so hooks registered on the KV store follow the same patterns as ORM hooks.

Standalone Usage

The extension's Init() method can be called outside of Forge for standalone use:

ext := groveext.New(
    groveext.WithDriver(pgdb),
    groveext.WithHook(&auditHook),
)

// Initialize without Forge
if err := ext.Init(nil); err != nil {
    log.Fatal(err)
}

db := ext.DB()
defer db.Close()

On this page