---
title: Forge Extension
description: 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

```bash
go get github.com/xraph/grove/extension
```

## Quick Start

### Programmatic Configuration

```go
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:

```yaml
# config.yaml
extensions:
  grove:
    driver: postgres
    dsn: "postgres://user:pass@localhost:5432/mydb"
```

```go
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:

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

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

## Options

### Single-DB Options

```go
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

```go
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.grove` → `grove`) → 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:

```go
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

```go
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

```yaml
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
```

```go
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:

```go
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:

```go
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:

```go
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:

```go
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:

```go
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:

```go
// 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:

```go
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:

```go
// 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:

```go
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:

```go
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:

```go
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:

```go
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:

```bash
$ 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:

```go
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
```

<Callout type="warn">
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.
</Callout>

## 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`:

```go
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:

```go
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()
```
