Forge Extension
Mount Shield into a Forge application as a first-class extension with lifecycle management.
Shield ships a ready-made Forge extension in the extension package. It wires the engine and lifecycle management into Forge's extension system with one call.
Installation
import "github.com/xraph/shield/extension"Registering the extension
package main
import (
"github.com/xraph/forge"
"github.com/xraph/shield/extension"
"github.com/xraph/shield/observability"
)
func main() {
app := forge.New()
shieldExt := extension.New(
extension.WithPlugin(observability.NewMetricsExtension()),
)
app.RegisterExtension(shieldExt)
app.Run()
}What the extension does
| Lifecycle event | Behaviour |
|---|---|
Register | Creates the engine from the provided options, registers it in the DI container |
Start | Runs store.Migrate (unless disabled) |
Stop | Calls engine.Stop which emits OnShutdown to all plugins |
Health | Returns nil (healthy) |
Extension options
| Option | Type | Default | Description |
|---|---|---|---|
WithStore(s) | store.Store | -- | Composite store (auto-resolved from grove if not set) |
WithPlugin(p) | plugin.Plugin | -- | Lifecycle plugin (repeatable) |
WithEngineOption(opt) | engine.Option | -- | Pass-through engine option |
WithConfig(cfg) | Config | defaults | Full config struct |
WithDisableRoutes() | -- | false | Skip HTTP route registration |
WithDisableMigrate() | -- | false | Skip migrations on Start |
WithBasePath(path) | string | "" | URL prefix for shield routes |
WithGroveDatabase(name) | string | "" | Name of the grove.DB to resolve from DI |
WithRequireConfig(b) | bool | false | Require config in YAML files |
File-based configuration (YAML)
When running in a Forge application, the Shield extension automatically loads configuration from YAML config files. The extension looks for config under the following keys (in order):
extensions.shield-- standard Forge extension config namespaceshield-- top-level shorthand
Example YAML config
# forge.yaml
extensions:
shield:
disable_routes: false
disable_migrate: false
base_path: "/shield"
default_profile: ""
shutdown_timeout: "30s"
scan_concurrency: 10
enable_short_circuit: true
grove_database: ""Or using the top-level shorthand:
# forge.yaml
shield:
default_profile: "strict"
scan_concurrency: 20
enable_short_circuit: trueConfig fields
| YAML Key | Type | Default | Description |
|---|---|---|---|
disable_routes | bool | false | Skip HTTP route registration |
disable_migrate | bool | false | Skip migrations on Start |
base_path | string | "" | URL prefix for all routes |
default_profile | string | "" | Safety profile when none specified |
shutdown_timeout | duration | "30s" | Max graceful shutdown wait time |
scan_concurrency | int | 10 | Max parallel scan operations |
enable_short_circuit | bool | true | Skip deeper layers on block decision |
grove_database | string | "" | Named grove.DB to resolve from DI |
Merge behaviour
File-based configuration is merged with programmatic options. Programmatic boolean flags (DisableRoutes, DisableMigrate) always win when set to true. For other fields, YAML values take precedence, then programmatic values, then defaults.
Requiring configuration
If your deployment requires YAML config to be present, use WithRequireConfig:
ext := extension.New(
extension.WithRequireConfig(true), // error if no YAML config found
)Tenant middleware
In a Forge app, tenant scope is typically set by middleware. Implement a Forge middleware that calls shield.WithTenant and shield.WithApp before passing the request to the Shield handlers:
func tenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := r.Header.Get("X-Tenant-ID")
appID := r.Header.Get("X-App-ID")
ctx := shield.WithTenant(r.Context(), tenantID)
ctx = shield.WithApp(ctx, appID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
router.Use(tenantMiddleware)Grove database integration
When your Forge app uses the Grove extension to manage database connections, Shield 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:
ext := extension.New(
extension.WithGroveDatabase(""),
)Using a named grove database
In multi-database setups, reference a specific database by name:
ext := extension.New(
extension.WithGroveDatabase("shield"),
)This resolves the grove.DB named "shield" 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.
Store resolution order
The extension resolves its store in this order:
- Explicit store -- if
WithStore(s)was called, it is used directly and grove is ignored. - Grove database -- if
WithGroveDatabase(name)was called, the named or defaultgrove.DBis resolved from DI. - In-memory fallback -- if neither is configured, an in-memory store is used.
Adding metrics
Register the observability plugin alongside the Shield engine extension:
import "github.com/xraph/shield/observability"
metricsPlugin := observability.NewMetricsExtensionWithFactory(fapp.Metrics())
shieldExt := extension.New(
extension.WithPlugin(metricsPlugin),
)Adding audit trails
Register the audit hook plugin to bridge safety events to an audit backend:
import "github.com/xraph/shield/audit_hook"
auditPlugin := audit_hook.New(myRecorder,
audit_hook.WithActions(
audit_hook.ActionScanBlocked,
audit_hook.ActionInstinctTriggered,
audit_hook.ActionPIIDetected,
),
)
shieldExt := extension.New(
extension.WithPlugin(metricsPlugin),
extension.WithPlugin(auditPlugin),
)