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.
Installation01
go get github.com/xraph/grove/extensionQuick Start02
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 Reference03
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://..."Options04
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"),
)Lifecycle05
Register Phase
Calls
BaseExtension.Register()to store the Forge app, logger, and metricsLoads configuration: YAML (
extensions.grove→grove) → merge with programmatic → defaultsRoutes 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
DBManagerMarks 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 DB06
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 Support07
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 beforeext.DB()always returns the default database in both modesext.Manager()returnsnilin single-DB mode, non-nil in multi-DB modeUnnamed 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 Registry08
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.
Hooks09
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 Integration10
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 Composition11
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 Commands12
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 Integration13
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 Usage14
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()