Skip to content

Extensibility

Everything HOG runs is a module. A module is a Go value — a chain.Middleware, an http.Handler, a session.StateStore, an idp.IdP — built by a named factory registered on a single process-wide registry. The registry package maps a (kind, name) pair to a Factory:

type Factory func(instanceName string, cfg RawConfig) (any, error)
func Register(kind, name string, f Factory)
func (r *Registry) Build(kind, name string, cfg RawConfig) (any, error)

RawConfig wraps an undecoded YAML node, so each factory decodes its own typed config struct — the registry itself never needs to know a module's shape. Configuration selects modules by name: a route's handler.type builds a TerminalHandler, a plugin resource's spec.type builds a RequestPlugin or ResponsePlugin, and the Gateway's stateProvider.type builds a StateProvider.

Built-ins are modules too

HOG's own terminal handlers — health, static, reverse-proxy, api — and its built-in oidc IdP connector are registered on this exact same contract; nothing about them is privileged. The difference is only how they get registered: hog.Run calls terminal.Register(reg) and idp.Register(reg) explicitly, so every HOG binary carries them regardless of which plugins were compiled in.

The middleware skeleton, by contrast, is not a module at all — it is hard-coded in chain.Skeleton at fixed positions in the chain (see request lifecycle). No module, first-party or third-party, can reorder it or run ahead of its gates. Developer code only ever runs in the two guarded slots — request-plugin and response-plugin — or as a terminal handler, IdP, or state provider that a route or the gateway explicitly selects.

Compile-time only

A module's factory registers itself from a package's init() — there is no runtime .so/plugin loading. A module exists in a binary only if its package was imported, directly or with a blank _ import, when that binary was built. This trades runtime flexibility for full Go tooling, pinned dependency versions, and no per-request dispatch overhead.

Two delivery paths, one mechanism

Both ways of shipping a HOG binary reduce to the same thing: import the built-ins, blank-import your plugin packages so their init()s register, and go build.

Framework mode. import "github.com/paulopiriquito/hog/v2", blank-import your plugin packages, and call hog.Main() (or drive hog.Run yourself). You own main.go and go.mod directly — this is how you'd embed HOG inside a larger Go program.

Base image / build manifest. List your plugin import paths (optionally @version) under the Gateway resource's spec.plugins. The hog-build CLI (cmd/hog-build, backed by internal/hogbuild) reads that manifest, renders a throwaway main.go that blank-imports every listed package plus a go.mod that pins the HOG module, and runs go build. The hog-builder base image packages this as a Dockerfile build stage, so a custom HOG image is built the same way the base hog-runtime/hog-static images are. In this mode, the config is the build manifest: the same Gateway resource that configures the running gateway also decides what gets compiled into it.

Continue with writing plugins for the plugin contracts and package layout, or design: delivery for the reasoning behind compile-time-only extensibility.