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/extensionQuick 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:
| Field | YAML Key | Type | Description |
|---|---|---|---|
| Driver | driver | string | Driver name: postgres, sqlite, mysql, mongodb, turso, clickhouse |
| DSN | dsn | string | Data source name / connection string |
| Databases | databases | []DatabaseConfig | Named database connections for multi-DB mode |
| Default | default | string | Default database name when using multi-DB |
| DisableRoutes | disable_routes | bool | Skip CRDT sync route registration |
| DisableMigrate | disable_migrate | bool | Skip automatic migration execution |
| BasePath | base_path | string | URL prefix for CRDT sync routes (default: /sync) |
Each entry in Databases has:
| Field | YAML Key | Type | Description |
|---|---|---|---|
| Name | name | string | Unique identifier for this database |
| Driver | driver | string | Driver name |
| DSN | dsn | string | Connection 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
- Calls
BaseExtension.Register()to store the Forge app, logger, and metrics - Loads configuration: YAML (
extensions.grove→grove) → merge with programmatic → defaults - Routes to single-DB or multi-DB initialization based on configuration
- Opens the database(s) and registers hooks
- Provides database instances in the Forge DI container via Vessel
- Registers CRDT sync controller if enabled (and routes not disabled)
Start Phase
- Starts the CRDT background syncer if configured
- Marks the extension as started
Stop Phase
- In single-DB mode: closes the Grove DB
- In multi-DB mode: closes all databases via the
DBManager - 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: primaryapp := 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:
| Service | Resolution | Description |
|---|---|---|
*grove.DB | vessel.Inject[*grove.DB]() | The default database (backward compatible) |
*grove.DB (named) | vessel.InjectNamed[*grove.DB](c, "name") | A specific named database |
*extension.DBManager | vessel.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 modesext.Manager()returnsnilin 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:
| Method | Path | Description |
|---|---|---|
| POST | /sync/pull | Remote nodes pull changes from this node |
| POST | /sync/push | Remote nodes push changes to this node |
| GET | /sync/stream | SSE 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
| Command | Description |
|---|---|
myapp migrate up | Run all pending migrations |
myapp migrate down | Rollback the last migration batch (with confirmation prompt) |
myapp migrate down --force | Rollback without confirmation |
myapp migrate status | Show 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" // SQLiteIf 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()