---
title: Forge Extension
description: Mount Chronicle into a Forge application as a first-class extension.
---

Chronicle ships as a `forge.Extension` that integrates with the Forge framework's lifecycle, DI container, and router. This is the recommended approach for Forge applications.

## Register the extension

```go
import (
    "github.com/xraph/chronicle/extension"
    "github.com/xraph/chronicle/store/postgres"
)

pgStore, err := postgres.New(pool)
if err != nil {
    log.Fatal(err)
}

ext := extension.New(
    extension.WithStore(pgStore),
    extension.WithCryptoErasure(true),
    extension.WithRetentionInterval(24 * time.Hour),
    extension.WithArchiveSink(s3Sink),
    extension.WithLogger(myLogger),
)

app.Register(ext)
```

`app.Register(ext)` calls `ext.Register(app)` which:
1. Runs `store.NewAdapter` and builds `chronicle.New`.
2. Creates `compliance.Engine`, `retention.Enforcer`, and `handler.API`.
3. Registers all 21 HTTP routes in the Forge router (unless `WithDisableRoutes(true)`).
4. Provides `chronicle.Emitter` in the Vessel DI container.

`app.Start(ctx)` calls `ext.Start(ctx)` which:
1. Runs `pgStore.Migrate(ctx)` (unless `WithDisableMigrate(true)`).
2. Launches the retention scheduler goroutine.

## Receive the Emitter via DI

Other extensions receive the `chronicle.Emitter` interface from the DI container without importing Chronicle internals:

```go
import (
    "github.com/xraph/vessel"
    "github.com/xraph/chronicle"
)

type MyExtension struct {
    emitter chronicle.Emitter
}

func (e *MyExtension) Register(app forge.App) error {
    return vessel.Invoke(app.Container(), func(emitter chronicle.Emitter) {
        e.emitter = emitter
    })
}

func (e *MyExtension) doSomething(ctx context.Context) {
    e.emitter.Info(ctx, "action", "resource", "id").
        Category("myservice").
        Record()
}
```

## Access extension components

If you need direct access to Chronicle internals (e.g. to mount custom routes or run manual enforcement):

```go
// The Chronicle engine for direct record/query
c := ext.Chronicle()

// The Emitter (same as what DI provides)
emitter := ext.Emitter()

// The compliance engine
engine := ext.ComplianceEngine()

// The retention enforcer
enforcer := ext.RetentionEnforcer()

// The handler.API (for manual route registration)
api := ext.API()

// An http.Handler for standalone use
h := ext.Handler()
```

## Disable automatic routes

If you want to mount routes on a sub-path or a different router:

```go
ext := extension.New(
    extension.WithStore(pgStore),
    extension.WithDisableRoutes(true),
)
app.Register(ext)

// Mount manually in your own router
customRouter.Mount("/audit", ext.Handler())
```

## Disable automatic migrations

```go
ext := extension.New(
    extension.WithStore(pgStore),
    extension.WithDisableMigrate(true),
)
// Run migrations yourself before starting
if err := pgStore.Migrate(ctx); err != nil {
    log.Fatal(err)
}
app.Register(ext)
```

## Grove database integration

When your Forge app uses the [Grove extension](https://github.com/xraph/grove) to manage database connections, Chronicle can automatically resolve a `grove.DB` from the DI container and construct the correct store backend based on the driver type.

### Using the default grove database

If the Grove extension registers a single database (or a default in multi-DB mode), use `WithGroveDatabase` with an empty name:

```go
ext := extension.New(
    extension.WithGroveDatabase(""),
)
```

### Using a named grove database

In multi-database setups, reference a specific database by name:

```go
ext := extension.New(
    extension.WithGroveDatabase("audit"),
)
```

This resolves the grove.DB named `"audit"` from the DI container and auto-constructs the matching store. The driver type is detected automatically -- you do not need to import individual store packages.

### Using the default grove KV store

If the Grove extension registers a KV store, use `WithGroveKV` with an empty name:

```go
ext := extension.New(
    extension.WithGroveKV(""),
)
```

### Using a named grove KV store

In multi-KV setups, reference a specific KV store by name:

```go
ext := extension.New(
    extension.WithGroveKV("chronicle-kv"),
)
```

### Store resolution order

The extension resolves its store in this order:

1. **Explicit store** -- if `WithStore(s)` was called, it is used directly and grove is ignored.
2. **Grove database** -- if `WithGroveDatabase(name)` was called (or `grove_database` is set in YAML), the named or default `grove.DB` is resolved from DI.
3. **Grove KV** -- if `WithGroveKV(name)` was called (or `grove_kv` is set in YAML), the named or default grove KV store is resolved from DI.
4. **In-memory fallback** -- if none of the above is configured, an in-memory store is used.

## YAML configuration

The Chronicle extension automatically loads configuration from your Forge app's YAML config files. It looks for the key `extensions.chronicle` first, then falls back to `chronicle`:

```yaml
# forge.yaml (or app.yaml, config.yaml, etc.)
extensions:
  chronicle:
    grove_database: audit
    grove_kv: chronicle-kv
    batch_size: 100
    flush_interval: 1s
    crypto_erasure: true
    retention_interval: 24h
    disable_routes: false
    disable_migrate: false
```

Or at the top level:

```yaml
chronicle:
  grove_database: ""
  grove_kv: ""
  batch_size: 100
  flush_interval: 1s
  crypto_erasure: false
  retention_interval: 24h
```

### Configuration reference

| Field | YAML key | Type | Default | Description |
|-------|----------|------|---------|-------------|
| `GroveDatabase` | `grove_database` | `string` | `""` | Name of the grove.DB to resolve from DI; empty uses the default DB |
| `GroveKV` | `grove_kv` | `string` | `""` | Name of the grove KV store to resolve from DI; empty uses the default KV |
| `BatchSize` | `batch_size` | `int` | `100` | Event batch size |
| `FlushInterval` | `flush_interval` | `duration` | `1s` | Batch flush interval |
| `CryptoErasure` | `crypto_erasure` | `bool` | `false` | Enable GDPR crypto-erasure |
| `RetentionInterval` | `retention_interval` | `duration` | `24h` | Retention enforcement interval (0 = off) |
| `DisableRoutes` | `disable_routes` | `bool` | `false` | Skip auto route registration |
| `DisableMigrate` | `disable_migrate` | `bool` | `false` | Skip `Migrate` on `Start` |

## Full option reference

| Option | Default | Description |
|--------|---------|-------------|
| `WithStore(s)` | — | **Required.** `store.Store` backend. |
| `WithBatchSize(n)` | 100 | Event batch size. |
| `WithFlushInterval(d)` | 1s | Batch flush interval. |
| `WithCryptoErasure(b)` | false | Enable GDPR crypto-erasure. |
| `WithRetentionInterval(d)` | 24h | Retention enforcement interval (0 = off). |
| `WithArchiveSink(s)` | nil | Archive sink for retention. |
| `WithLogger(l)` | `slog.Default()` | Logger. |
| `WithDisableRoutes(b)` | false | Skip auto route registration. |
| `WithDisableMigrate(b)` | false | Skip `Migrate` on `Start`. |
| `WithGroveDatabase(name)` | `""` | Resolve a grove.DB from DI by name; empty uses the default DB. |
| `WithGroveKV(name)` | `""` | Resolve a grove KV store from DI by name; empty uses the default KV. |
