nucleus

package
v1.27.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 53 Imported by: 0

Documentation

Overview

Package nucleus — config.go implements the configuration loader surfaced by `AppBuilder.FromConfigFile`. ADR-010 §2 names this as Phase 2 work. Phase 2a (PR #73) shipped the single-file YAML loader with the 1 MiB size cap, schema strict-unknown-fields validation, and did-you-mean hints. Phase 2b (#74) layered on top:

  • TOML and JSON parsers (extension-based dispatch).
  • Multi-file merge with last-file-wins semantics, deep-merge for maps, and replace-by-default for scalars and lists.
  • `_append` / `_remove` suffix operators that survive the parser round-trip in all three formats and provide additive/subtractive semantics for list/map collections (ADR-010 §3).
  • `null` reverts the key to its struct default — except for the non-nullable security keys named in ADR-010 §14, where `null` is a boot error rather than a silent revert-to-default.
  • Mixed-format file lists (one .yaml + one .toml, for example) emit a startup warning by default and are rejected outright when `AppBuilder.WithConfigStrict(true)` is in force.

Phase 2c (this commit) layers the ADR-010 §15 strict-mode startup guard:

  • `AppBuilder.WithUnknownFields(UnknownFieldsStrict | UnknownFieldsWarn)` toggles strict schema validation per builder. Warn mode emits a `WARN`-level slog event listing the offending keys and proceeds with the load; strict mode (the default) keeps the Phase 2a reject-with-`ErrUnknownConfigKeys` behaviour.
  • `NUCLEUS_ENV=production` (case-insensitive, whitespace-trimmed; constant `EnvProduction`) is the operator escape hatch: when set, the loader forces the mode back to strict regardless of code-level configuration and emits a `WARN` recording the override.
  • Two startup `WARN`s for visibility: one when warn mode is active outside production ("do not deploy to production"), one when the production override fires.

The package-level `Run(App)` and the direct-struct surface never traverse this loader — only the builder-chain `FromConfigFile` does.

Phase 3 shipped the effective-config tooling (3a: `LoadEffective` + `config print --effective`) and the env-layer + `file:line` provenance (3.1). The `NUCLEUS_`-prefixed env layer is applied here (see loadMerged / applyEnvLayer), so the FromConfigFile path honours the ADR-010 §4 precedence `defaults < files < env`.

What's still deferred:

  • Layer 3 range/enum semantic validation (out of the 4-phase slicing; tracked as a follow-up).
  • The CLI-flags and programmatic-override layers of ADR-010 §4.

Package nucleus — jobs.go wires ModuleSpec.Jobs to the existing pkg/tasks runtime (ADR-010 Phase 2). Modules register named jobs through the JobRegistry their Jobs closure receives; the framework translates each registration into a pkg/tasks handler plus a scheduler entry on the provider selected by `jobs_provider` (memory by default, asynq when configured), starts the worker alongside the application's services, and stops scheduler and worker on shutdown. No new scheduler is built here — scheduling and execution are entirely pkg/tasks.

Package nucleus is the fluent façade over the production-grade application container in `pkg/app`. It is the recommended entry point for assembling Nucleus applications at any size — single-file demos, embedded services, and enterprise bootstrap patterns alike — and composes the existing capability packages (`pkg/router`, `pkg/db`, `pkg/auth`, `pkg/authz`, `pkg/storage`, `pkg/mail`, `pkg/observe`, `pkg/signals`, `pkg/tasks`) without duplicating any.

Three coexisting surfaces produce the same `nucleus.App{}` value:

  • Fluent: sugar over the struct, ideal for demos and embedded use.

    nucleus.New(). FromConfigFile("config/nucleus.yaml"). Use(middleware.Logger(), middleware.Recover()). Mount(articles.Module, users.Module). Start()

  • Direct struct: for tests and programmatic embedding.

    nucleus.Run(nucleus.App{ Config: app.Config{Port: 8080}, Modules: map[string]nucleus.ModuleSpec{ "articles": articles.Module, }, })

  • Bootstrap pattern: a user-space convention (no sub-package ships with the framework). Define your own constructor — typically `internal/bootstrap/bootstrap.go` — that returns `nucleus.App`, then call `nucleus.Run(bootstrap.New())`.

The package is the Phase 1 Foundation of ADR-010 (Fluent API v2 for pkg/nucleus): it pins the canonical struct shape, the `Module[C any]` generic constructor, the `Router` interface with three coexisting registration styles, and the three-surface equivalence guarantee. Configuration loading (ADR-010 Phase 2a–2d) is fully shipped: `FromConfigFile` accepts one or more paths and merges them left-to-right (last-file-wins for scalars, deep-merge for maps). Per-file size cap: 1 MiB (MaxConfigFileBytes). Supported formats: YAML (.yaml/.yml), TOML (.toml), JSON (.json). The `_append` and `_remove` suffix operators provide additive/subtractive list semantics. `null` reverts a key to its struct default — except for non-nullable security keys (e.g. `jwt_secret`) where `null` is a boot error (ErrSecurityKeyNotNullable). Mixed-format file lists emit a startup WARN by default; WithConfigStrict(true) upgrades the warning to ErrMixedConfigFormats. WithUnknownFields("warn") downgrades schema-validation failures to WARN-level slog events; NUCLEUS_ENV=production forces the mode back to strict regardless of the code-level setting.

Package nucleus — validate_module_config.go implements ADR-010 §2 layer 5 (module-specific configuration binding + validation), the fifth and final layer of the FromConfigFile validator. Where layers 2–4 validate app.Config, layer 5 binds each mounted module's `modules.<name>.*` subtree into the module author's typed Config, fills still-zero fields from `default:` struct tags, and validates the result against its `validate:` tags (go-playground/validator, via pkg/validate).

Like layers 3 and 4 it runs on BOTH surfaces: the builder path (FromConfigFile → Build → Run) supplies the file subtree captured on App.moduleConfigsRaw, while the direct-struct Run(App{}) path has no file and so only applies defaults + validation to the programmatically-set Config. Binding happens at Run time — not in FromConfigFile or Mount — because those two may be called in either order, and only at Run are both the merged config and the full module set known (the same reason layer 4's validateModuleRequires runs at Run).

Package nucleus — validate_referential.go. The config-only layer-4 implementation moved to pkg/app (config_validate_layers.go) next to layer 3, for the same reason: every config consumer — builder, direct struct, and the CLI's LoadConfig — must reach the same verdict. What stays here is the public sentinel (where consumers already import it) and the MODULE half of layer 4, which cannot move: modules are registered on the builder (Mount), not present in the loaded config.

Package nucleus — validate_semantics.go. The layer-3 implementation moved to pkg/app (config_validate_layers.go) so the CLI's LoadConfig applies the same verdict as the builder and the direct-struct Run — closing the "same file, two verdicts" gap for layers 3–4 (the DX audit's class; DX-13 closed it for unknown keys). This file keeps the public sentinel where consumers already import it.

Package nucleus — webhooks.go wires ModuleSpec.Webhooks to real routes (ADR-010 Phase 2). Each registration mounts a handler at `<webhooks_prefix>/<module-name><path>` on the application router, behind the framework's checks: method allow-list, request-body cap, and — when the spec carries a Secret — HMAC-SHA256 signature verification of the raw body against the X-Nucleus-Signature header. Webhooks authenticate by signature, not by CSRF token, so Run exempts the webhook prefix from CSRF when both are enabled.

Anti-replay is a declared limit of the signature check: a valid signed request that is captured can be re-sent verbatim and will verify again. WebhookSpec.TimestampTolerance narrows the replay window by binding a signed X-Nucleus-Timestamp into the signature material; deduplicating by event ID in the handler closes it. See WebhookSpec.Secret.

Index

Constants

View Source
const (
	UnknownFieldsStrict = "strict"
	UnknownFieldsWarn   = "warn"
)

UnknownFieldsStrict and UnknownFieldsWarn are the two values accepted by `AppBuilder.WithUnknownFields`. ADR-010 §15 specifies the strings; the framework exports them as constants so callers can avoid string-literal typos at the call site (`nucleus.UnknownFieldsWarn` reads better than `"warn"` in IDEs and survives refactors). New modes are not anticipated — if one ever lands it joins the union here and the validation in `WithUnknownFields` is extended.

View Source
const EnvProduction = "production"

EnvProduction is the value of the `NUCLEUS_ENV` environment variable that the framework treats as "production-strict": regardless of `WithUnknownFields("warn")`, the loader rejects unknown configuration keys when `NUCLEUS_ENV=production` is set. ADR-010 §15 specifies this as the operator escape hatch against a developer who accidentally left warn mode in a production build.

View Source
const MaxConfigFileBytes = 1 << 20 // 1 MiB

MaxConfigFileBytes is the per-file size cap enforced by FromConfigFile before invoking any format parser. The cap is the ADR-010 §17 compliance item — it eliminates the parser-DoS class (anchor expansion / deep nesting) that `gopkg.in/yaml.v3` is not hardened against by itself, and applies uniformly to TOML and JSON for consistency. 1 MiB is generous for application configuration in practice while still small enough to make a pathological file fail loud rather than wedge the process.

View Source
const WebhookSignatureHeader = "X-Nucleus-Signature"

WebhookSignatureHeader carries the HMAC-SHA256 body signature a caller must send when the receiving WebhookSpec sets a Secret.

View Source
const WebhookTimestampHeader = "X-Nucleus-Timestamp"

WebhookTimestampHeader carries the Unix-seconds send time a caller must include when the receiving WebhookSpec sets a TimestampTolerance. The value is bound into the signature material by SignWebhookBodyWithTimestamp, so it cannot be altered in transit without invalidating the signature.

Variables

View Source
var ErrConfigFileTooLarge = errors.New("nucleus: configuration file exceeds the per-file size cap")

ErrConfigFileTooLarge is returned when a configuration file exceeds MaxConfigFileBytes. Callers can errors.Is against this sentinel to distinguish a configuration-management problem (file is genuinely too big — split it) from a parser-side problem (bad content).

View Source
var ErrInvalidConfigReference = app.ErrInvalidConfigReference

ErrInvalidConfigReference is returned when a configuration value is individually valid but inconsistent with another related key (ADR-010 §2 layer 4). Alias of app.ErrInvalidConfigReference; errors.Is matches through either name.

View Source
var ErrInvalidConfigValue = app.ErrInvalidConfigValue

ErrInvalidConfigValue is returned when a configuration value is well-typed but semantically invalid — out of range, not a recognised enum member, or a negative duration (ADR-010 §2 layer 3). Alias of app.ErrInvalidConfigValue (the implementation moved to pkg/app so every config consumer shares it); errors.Is matches through either name.

View Source
var ErrInvalidJobSpec = errors.New("nucleus: invalid job registration")

ErrInvalidJobSpec is returned (wrapped, naming module and job) for any invalid JobRegistry.Register call. It reaches the user as a Run error: a module that declares a broken job fails boot instead of silently not running it.

View Source
var ErrInvalidModuleConfig = errors.New("nucleus: invalid module configuration")

ErrInvalidModuleConfig is returned when a module's configuration cannot be bound or fails validation (ADR-010 §2 layer 5). The wrapped message names the offending module and the failing stage (binding, defaults, or validation).

View Source
var ErrInvalidModulePolicy = errors.New("nucleus: invalid module policy")

ErrInvalidModulePolicy marks a module-declared PolicyRule or CSRFExempt entry that fails validation. Like ErrInvalidJobSpec and ErrInvalidWebhookSpec, it fails application boot: a malformed row that were silently skipped would leave the route dark with no trace — the exact "exit 0 with no effect" class the suite guards against.

View Source
var ErrInvalidUnknownFieldsMode = errors.New("nucleus: WithUnknownFields requires mode \"strict\" or \"warn\"")

ErrInvalidUnknownFieldsMode is returned by `AppBuilder.WithUnknownFields` when the supplied mode is neither `UnknownFieldsStrict` nor `UnknownFieldsWarn`. The error is deferred onto the builder so the misuse surfaces at `Build` / `Start` / `Serve` time alongside any other deferred chain error.

View Source
var ErrInvalidWebhookSpec = errors.New("nucleus: invalid webhook registration")

ErrInvalidWebhookSpec is returned (wrapped, naming module and path) for any invalid WebhookRegistry.Register call. It reaches the user as a Run error: a module that declares a broken webhook fails boot instead of silently not mounting it.

View Source
var ErrMixedConfigFormats = errors.New("nucleus: configuration files mix incompatible formats")

ErrMixedConfigFormats is returned by FromConfigFile when the configured paths use a mix of formats (e.g. one `.yaml` plus one `.toml`) AND `AppBuilder.WithConfigStrict(true)` is in force. With strict mode off (the default), a mixed-format file list emits a `WARN`-level slog event but proceeds with the merge.

View Source
var ErrSecurityKeyNotNullable = errors.New("nucleus: security key may not be null")

ErrSecurityKeyNotNullable is returned when a configuration file sets one of the non-nullable security keys to `null` / `~`. ADR-010 §14 lists the keys whose null-revert would be a silent security degradation (e.g. `cors_origins: null` reverting to `corsAllowAll: true`). On these keys, null is a boot error rather than a revert-to-default.

View Source
var ErrUnknownConfigKeys = errors.New("nucleus: unknown configuration key(s)")

ErrUnknownConfigKeys is returned when strict schema validation (the default for FromConfigFile) finds keys in the loaded file that do not map to any field on `app.Config` or its nested structs. The error's Error() reproduces the offending keys with "did you mean …?" hints when a close match exists.

View Source
var ErrUnsupportedConfigFormat = errors.New("nucleus: unsupported configuration file format")

ErrUnsupportedConfigFormat is returned when FromConfigFile is asked to parse a file whose extension is not recognised. Phase 2a supported only `.yaml` / `.yml`; Phase 2b adds `.toml` and `.json`. Anything else (`.ini`, `.xml`, …) surfaces this sentinel.

Functions

func Run

func Run(a App) error

Run is the package-level direct-struct surface. It accepts a fully populated `App` and runs the same startup sequence the fluent builder uses. Direct-struct callers — typically tests or the bootstrap pattern — invoke this function with their own constructed value.

Startup sequence (ADR-010 Phase 4 ordering):

  1. Construct `*app.App` via `app.New(&a.Config, a.Options...)`.
  2. Apply `a.Middleware` globally to the application router.
  3. Build a per-module `Runtime` handle bound to each module's `DefaultDB` alias.
  4. Run app-level `Lifecycle.OnStart`.
  5. For each module (sorted order): run `OnStart(ctx, rt)` — BEFORE route registration, so a module initialises managed resources its Routes closure can then capture (Gap 2) — and register its `OnShutdown` only after `OnStart` succeeds.
  6. Collect each module's `spec.Jobs` / `spec.Webhooks` registrations against the real registries (a broken registration fails boot here).
  7. For each module: route its `spec.Routes(Router)` under `spec.Prefix()`, applying per-module middleware first; then mount webhook routes under the `webhooks_prefix`.
  8. Start the module jobs runtime (pkg/tasks provider per `jobs_provider`) and spawn each `ServiceRegistration` Run in a goroutine; the framework cancels their shared context at shutdown.
  9. Block on `app.App.Run`.
  10. After Run returns: cancel services and the jobs worker, stop the jobs scheduler, run app-level `Lifecycle.OnShutdown` (module `OnShutdown` hooks fire inside `app.App.Run`'s shutdown path).

func RunContext

func RunContext(parent context.Context, a App) error

RunContext is Run with a caller-owned lifetime (DX-22): cancelling ctx triggers the same graceful shutdown a SIGTERM does — server drain, module OnShutdown hooks, service and jobs teardown. It exists so tests (and any embedder) can start and stop an application in-process instead of building a binary, launching a child process and polling /healthz; pkg/nucleustest wraps it into a one-call harness.

func SignWebhookBody

func SignWebhookBody(secret string, body []byte) string

SignWebhookBody returns the X-Nucleus-Signature value ("sha256=<hex>") for body under secret — the exact string the webhook verifier expects. Exported for webhook senders and for tests of signed receivers.

The signature authenticates the body only: it does not bind a send time, so a captured request can be replayed as-is (see WebhookSpec.Secret for the declared limit). Receivers that set WebhookSpec.TimestampTolerance require SignWebhookBodyWithTimestamp instead.

func SignWebhookBodyWithTimestamp

func SignWebhookBodyWithTimestamp(secret string, ts time.Time, body []byte) (signature, timestamp string)

SignWebhookBodyWithTimestamp returns the two header values a sender needs for a webhook receiver that sets WebhookSpec.TimestampTolerance: the X-Nucleus-Signature value ("sha256=<hex>") and the X-Nucleus-Timestamp value (ts as decimal Unix seconds). The signature covers `<timestamp>.<body>`, so the timestamp the verifier trusts for the tolerance check is exactly the one the sender signed.

SignWebhookBody remains the signer for receivers without a TimestampTolerance; the two schemes do not mix — a body-only signature is rejected by a timestamped receiver and vice versa.

Types

type App

type App struct {
	app.Config `yaml:",inline"`

	Modules    map[string]ModuleSpec `yaml:"-"`
	Middleware []Middleware          `yaml:"-"`
	Services   []ServiceRegistration `yaml:"-"`
	Lifecycle  LifecycleHooks        `yaml:"-"`
	Options    []Option              `yaml:"-"`

	// OpenAPI, when non-nil, mounts a JSON OpenAPI document endpoint at
	// Run time via the underlying app container (ADR-010 Phase 4, Slice 2).
	// The fluent builder sets it through AppBuilder.WithOpenAPIHandler;
	// direct-struct callers populate it explicitly. Nil means no OpenAPI
	// endpoint.
	OpenAPI *OpenAPISpec `yaml:"-"`
	// contains filtered or unexported fields
}

App is the canonical struct that every entry point — fluent builder, direct-struct call, bootstrap function — produces. It embeds `app.Config` (so every yaml-bindable production-grade option is present unchanged) and adds four Go-only wiring fields tagged `yaml:"-"` so that they cannot be expressed in a configuration file.

Modules is a map (not a slice) so configuration overlays can override individual modules by name in later phases. Middleware is a slice because registration order is significant: the router applies middleware in the order it was registered.

type AppBuilder

type AppBuilder struct {
	// contains filtered or unexported fields
}

AppBuilder is the fluent surface returned by `New()`. Methods on `AppBuilder` are non-destructive against the caller and idempotent: `Use`, `Mount`, `WithoutDefaults`, and `WithExtensions` append to the underlying slices; `FromConfigFile` records intent. Errors accumulated during chaining (a duplicate module name, a malformed config file, …) are surfaced when the builder is realised via `Build`, `Start`, or `Serve`.

func New

func New() *AppBuilder

New returns an `AppBuilder` seeded with the framework's `app.DefaultConfig()`. The default config is the same value `pkg/app` produces — sensible production defaults for port, log level, observability bootstrap, etc. Override fields via the fluent methods or by reaching into the underlying struct through `Build`.

func (*AppBuilder) Build

func (b *AppBuilder) Build() (App, error)

Build realises the builder into an `App` value plus any deferred error. The returned `App` is a copy of the builder's internal state: subsequent mutations on the builder do not affect a previously-built App. Used by `Start`, `Serve`, and the three-surface equivalence test.

func (*AppBuilder) Err

func (b *AppBuilder) Err() error

Err exposes the builder's accumulated error without realising it. Useful in tests and in callers that want to inspect chain status before deciding to call Start. Returns `nil` if no error has been recorded.

func (*AppBuilder) FromConfigFile

func (b *AppBuilder) FromConfigFile(paths ...string) *AppBuilder

FromConfigFile loads configuration from one or more files. Each file is read via the Phase 2b loader (`loadFromFiles` in config.go) which enforces, per file:

  • 1 MiB per-file size cap (see MaxConfigFileBytes) — eliminates parser-DoS classes against the underlying format parsers.
  • YAML (`.yaml` / `.yml`), TOML (`.toml`), and JSON (`.json`) formats. Any other extension surfaces `ErrUnsupportedConfigFormat`.
  • Strict-unknown-fields schema validation against `app.ContractConfigKeyPatterns()`. Unknown keys surface as `ErrUnknownConfigKeys` with did-you-mean hints for likely typos.

Multi-file merge semantics (ADR-010 §3):

  • Precedence is `struct defaults < file[0] < … < file[N-1]`.
  • Scalars replace; maps deep-merge; lists replace by default.
  • The suffix operators `<key>_append` and `<key>_remove` provide additive and subtractive list semantics that survive every parser the loader supports.
  • `null` reverts the key to its struct default — except for keys in the non-nullable security set (e.g. `jwt_secret`) where `null` is a boot error (`ErrSecurityKeyNotNullable`).
  • Mixed-format file lists emit a startup `WARN` by default; `AppBuilder.WithConfigStrict(true)` upgrades the warning to a hard `ErrMixedConfigFormats` error.

Errors accumulate on the builder and surface at `Build` / `Start` / `Serve` — the bufio.Scanner pattern. `Err()` exposes the accumulator for callers that want to inspect chain status before realising.

`WithConfigStrict(...)` must be called BEFORE `FromConfigFile` to affect the same load. The builder records the strict flag at call time; later flips do not retroactively re-evaluate a previously loaded file list.

func (*AppBuilder) Mount

func (b *AppBuilder) Mount(specs ...ModuleSpec) *AppBuilder

Mount registers one or more module specs. Each spec is stored in `App.Modules` keyed by `spec.Name()`. Two modules sharing a name is a configuration bug — the builder records the error and surfaces it when realised.

func (*AppBuilder) Serve

func (b *AppBuilder) Serve() error

Serve is an alias for `Start`. ADR-010 lists `Start` as the canonical builder terminator; `Serve` is provided as an ergonomic synonym for callers who prefer the HTTP-server-flavoured name.

func (*AppBuilder) Start

func (b *AppBuilder) Start() error

Start realises the builder and runs the resulting application until the process receives a shutdown signal or the context returned by `app.App.Run` is cancelled. Equivalent to `nucleus.Run(b.Build())`.

func (*AppBuilder) Use

func (b *AppBuilder) Use(mws ...Middleware) *AppBuilder

Use appends global middleware to be applied to the underlying router before any module routes. Registration order is preserved. To attach middleware to a specific subtree, declare it on the module's `Middleware` field or use `Router.Group` inside the module's `Routes` callback.

func (*AppBuilder) WithConfigStrict

func (b *AppBuilder) WithConfigStrict(strict bool) *AppBuilder

WithConfigStrict toggles the merge-engine's mixed-format guard for subsequent `FromConfigFile` calls on this builder. With strict mode on, a file list mixing two or more of YAML / TOML / JSON is rejected outright with `ErrMixedConfigFormats`; with strict mode off (the default), the loader emits a `WARN` slog event and proceeds with the merge. The toggle is per-builder and idempotent; re-calling with the same value is a no-op.

Call this BEFORE `FromConfigFile` — the strict flag is read at load time, not retroactively. To prevent silent misuse, calling `WithConfigStrict` AFTER `FromConfigFile` records a deferred error on the builder so the misordered chain fails loud at `Build` / `Start` / `Serve` time. Most builders set strict mode once near the top of the chain and never touch it again, so this guard is invisible to correct usage.

func (*AppBuilder) WithDatabases

func (b *AppBuilder) WithDatabases(dbs map[string]app.DatabaseConfig) *AppBuilder

WithDatabases pins the application's database aliases programmatically, overriding both the config file and the NUCLEUS_* environment layer.

It exists because the remediation the test kit prescribes was not expressible from the entry point the test kit documents (QCD-FW-14): nucleustest presents `Start(t, nucleus.New().FromConfigFile(...))` as the way in, and its DB()/MigrateDir() errors say to "set Databases in the config, e.g. nucleustest.TempSQLite" — but the builder had no setter, so following both at once was impossible.

It wins over the environment ON PURPOSE, unlike every other source. The NUCLEUS_* layer exists to override FILES in a deployment; a call written in code is not a file, and a test that pins its own database must not have it swapped by whatever the developer's shell exports. That silence is how SQLite DDL ended up running against a real PostgreSQL: the environment redirected the database under the test, and nothing said so.

func (*AppBuilder) WithExtensions

func (b *AppBuilder) WithExtensions(exts ...Extension) *AppBuilder

WithExtensions appends `app.WithExtensions(exts...)` to the option chain forwarded verbatim to `app.New`.

func (*AppBuilder) WithOpenAPIHandler

func (b *AppBuilder) WithOpenAPIHandler(pattern string, handler http.Handler) *AppBuilder

WithOpenAPIHandler registers a JSON OpenAPI document endpoint to be mounted at Run time, served by any stdlib http.Handler — typically `openapi.Handler(provider)` for a generated document factory, but any handler that writes the document JSON works (pre-rendered bytes, an embedded file, a proxy). `pattern` is the route (defaulting to "/openapi.json" when empty). A nil handler records a deferred builder error. Calling it more than once replaces the previously recorded spec (last-wins), matching the other fluent setters.

The stable builder is stdlib-only: the provider-typed WithOpenAPI was removed in v0.12.0 (DEP-2026-008).

func (*AppBuilder) WithOpenAuthz

func (b *AppBuilder) WithOpenAuthz() *AppBuilder

WithOpenAuthz appends `app.WithOpenAuthz()` to the option chain (QCD-FW-11): every public app.Option must be reachable from the documented builder — TestAppBuilderMirrorsEveryAppOption enforces it.

func (*AppBuilder) WithTemplateFuncs

func (b *AppBuilder) WithTemplateFuncs(funcs template.FuncMap) *AppBuilder

WithTemplateFuncs appends `app.WithTemplateFuncs(funcs)` to the option chain: template functions available to every template the startup loader parses from templates_dir (QCD-FW-9/11).

nucleus.New().
    FromConfigFile("nucleus.yml").
    WithTemplateFuncs(template.FuncMap{"fecha": formatFecha}).
    Mount(consola.Module()).
    Start()

func (*AppBuilder) WithTemplates

func (b *AppBuilder) WithTemplates(base *template.Template) *AppBuilder

WithTemplates appends `app.WithTemplates(base)` to the option chain: a prebuilt *template.Template as the parse base for templates_dir (QCD-FW-9/11).

func (*AppBuilder) WithTemplatesFS

func (b *AppBuilder) WithTemplatesFS(prefix string, fsys fs.FS) *AppBuilder

WithTemplatesFS appends `app.WithTemplatesFS(prefix, fsys)` to the option chain: an fs.FS of .html templates parsed under a name prefix, accumulating across calls.

func (*AppBuilder) WithUnknownFields

func (b *AppBuilder) WithUnknownFields(mode string) *AppBuilder

WithUnknownFields configures how `FromConfigFile` reacts to keys present in a file but absent from `app.Config`'s schema. Two modes are accepted (see `UnknownFieldsStrict` / `UnknownFieldsWarn` constants):

  • `"strict"` (default): unknown keys reject the load with `ErrUnknownConfigKeys` and a did-you-mean hint.
  • `"warn"`: unknown keys emit a `WARN`-level slog event listing the offending keys; the load proceeds with the unknowns stripped so they do not leak into the merged config.

ADR-010 §15: when `WithUnknownFields("warn")` is active outside production, the loader additionally emits a "do not deploy to production" WARN at load time. The `NUCLEUS_ENV=production` environment variable is the operator escape hatch: when set, the loader forces the mode back to strict regardless of code-level configuration, and emits a WARN recording the override. A future build leaving `WithUnknownFields("warn")` in production code is therefore not silently exposed to typo'd config values.

Any value other than the two accepted modes records `ErrInvalidUnknownFieldsMode` as a deferred builder error. Like `WithConfigStrict`, the call must happen BEFORE `FromConfigFile`; calling it after records the misorder error.

func (*AppBuilder) WithUserProvider

func (b *AppBuilder) WithUserProvider(provider auth.UserProvider) *AppBuilder

WithUserProvider registers the application's own user table as an authentication backend, mirroring `app.WithUserProvider`.

func (*AppBuilder) WithUserProviderNamed

func (b *AppBuilder) WithUserProviderNamed(name string, provider auth.UserProvider) *AppBuilder

WithUserProviderNamed mirrors `app.WithUserProviderNamed`, for an application whose user table should appear in the chain under a name other than "local".

func (*AppBuilder) WithoutDefaults

func (b *AppBuilder) WithoutDefaults() *AppBuilder

WithoutDefaults appends `app.WithoutDefaults()` to the option chain forwarded verbatim to `app.New`. Direct-struct callers achieve the same effect by setting `App.Options`.

type ConfigSource

type ConfigSource struct {
	Kind string `json:"kind"`
	Path string `json:"path,omitempty"`
	// Line is the 1-based source line a file-sourced key was defined on
	// (Phase 3.1). Populated for YAML files only — TOML positions are
	// available only via go-toml's explicitly-unstable API, and JSON has no
	// standard line API, so both report kind+path with Line == 0. Omitted
	// (zero) for the "default", "env", and "runtime" kinds.
	Line int `json:"line,omitempty"`
}

ConfigSource identifies where an effective configuration value came from (ADR-010 §5 / Phase 3, compliance #6). Kind is "default" for struct defaults, one of "yaml"/"toml"/"json" for a file, or "env" for a `NUCLEUS_`-prefixed environment override (Phase 3.1). Path is the file path for file kinds, the originating variable name for "env", and empty for defaults. The CLI-flags and programmatic-override layers of ADR-010 §4 are not applied in the FromConfigFile path, so they never appear as a Kind here.

type Context

type Context struct {
	*routerpkg.Context
}

Context wraps the router Context with simplified methods

func (*Context) BindForm

func (c *Context) BindForm(v interface{}) error

BindForm binds urlencoded or multipart form data to the given struct with typed conversion (ints, floats, bools, time.Time, pointers; `form:`/`json:` tags), then validates it using struct validate tags — same discipline as BindJSON. See router.BindForm for the full binding rules.

func (*Context) BindJSON

func (c *Context) BindJSON(v interface{}) error

BindJSON binds JSON body to the given struct

func (*Context) BindXML

func (c *Context) BindXML(v interface{}) error

BindXML binds an XML body to the given struct with the same discipline as BindJSON: the body is capped at 1 MiB (413 beyond it), a malformed document is a 400, and the decoded value is validated against its `validate` tags before it is returned.

func (*Context) Get

func (c *Context) Get(key string) interface{}

Get retrieves a value from context

func (*Context) HTML

func (c *Context) HTML(code int, html string) error

HTML sends a RAW HTML string as the response body. No template is involved and nothing is escaped — the string is written as-is.

NOTE the shadowing (AN-07): the embedded router.Context also has an HTML method, with a different signature and a different job — it renders a NAMED TEMPLATE through the application's template engine. This method hides it, which is why generated code used to spell c.Context.HTML for a template render. For templates use Render on this Context, which forwards to the engine; use HTML only when you already hold a fully-formed (and trusted) HTML string.

func (*Context) JSON

func (c *Context) JSON(code int, v interface{}) error

JSON sends a JSON response

func (*Context) NoContent

func (c *Context) NoContent() error

NoContent sends 204 No Content

func (*Context) Param

func (c *Context) Param(key string) string

Param returns URL path parameter

func (*Context) Query

func (c *Context) Query(key string) string

Query returns query parameters

func (*Context) Redirect

func (c *Context) Redirect(code int, url string) error

Redirect redirects to the given URL

func (*Context) Render

func (c *Context) Render(code int, templateName string, data map[string]interface{}) error

Render renders a named template through the application's template engine, merging the request's bound data with the given data — it forwards to the embedded router.Context's engine-backed HTML method under an unshadowed name (AN-07). The template name is the file's path relative to templates_dir (or `<module>/<path>` for module-embedded templates), e.g.

return c.Render(http.StatusOK, "blog/index.html", map[string]interface{}{"title": "Blog"})

Contrast with HTML on this Context, which writes a raw HTML string and touches no template.

func (*Context) RequestID

func (c *Context) RequestID() string

RequestID returns the request ID

func (*Context) SessionGetString

func (c *Context) SessionGetString(key string) string

SessionGetString reads a string value from session

func (*Context) SessionPutString

func (c *Context) SessionPutString(key, value string) error

SessionPutString writes a string value to session

func (*Context) Set

func (c *Context) Set(key string, value interface{})

Set sets a value in context (for templates)

func (*Context) Status

func (c *Context) Status(code int)

Status sends only status code

func (*Context) String

func (c *Context) String(code int, s string) error

String sends a plain text response

func (*Context) XML

func (c *Context) XML(code int, v interface{}) error

XML sends an XML response

type Creator

type Creator interface{ Create(*Context) error }

Creator handles POST /resource.

type Destroyer

type Destroyer interface{ Destroy(*Context) error }

Destroyer handles DELETE /resource/{id}.

type EffectiveConfig

type EffectiveConfig struct {
	Values []EffectiveValue `json:"values"`
}

EffectiveConfig is the fully-merged configuration with per-key provenance, sorted by Key. It backs `nucleus config print --effective` (ADR-010 Phase 3a).

func LoadEffective

func LoadEffective(paths []string, extraKeys ...string) (EffectiveConfig, error)

LoadEffective merges the given config files exactly as FromConfigFile would (struct defaults < file[0] < … < file[N-1]) and returns the effective configuration with per-key provenance and the canonical redaction applied (observe.DefaultRedactedKeys()). It is the entry point for `nucleus config print --effective`.

Sensitive values are redacted using only the canonical redaction list; pass extraKeys to extend it via the same observe.RedactionConfig.ExtraKeys mechanism the logger uses — there is no second redaction surface.

type EffectiveValue

type EffectiveValue struct {
	Key      string       `json:"key"`
	Value    any          `json:"value"`
	Source   ConfigSource `json:"source"`
	Redacted bool         `json:"redacted,omitempty"`
}

EffectiveValue is one resolved configuration key with its origin. Value holds observe.RedactionPlaceholder (and Redacted is true) when the key is sensitive per the canonical redaction list.

type EventBus

type EventBus interface {
	// SubscribeSQL streams SQL-statement events until the returned cancel runs.
	SubscribeSQL() (<-chan SQLEvent, func())
	// SubscribeHTTP streams HTTP-request events until the returned cancel runs.
	SubscribeHTTP() (<-chan HTTPEvent, func())

	// EmitSQL publishes a SQL-statement event onto the bus so it reaches every
	// SubscribeSQL consumer. It is the emit counterpart to SubscribeSQL: an
	// external producer that runs SQL outside the framework's own CRUD layer
	// (e.g. an ORM bridge) can surface those statements in the same live feed
	// without importing the lower-level pkg/observability package.
	//
	// The adapter converts the first-party SQLEvent value into the bus's
	// pooled, refcounted event and owns the Release discipline internally; the
	// caller keeps ownership of ev and its Args slice (they are copied, not
	// aliased). The caller sets EmittedAt (typically the query's completion
	// time) and, when correlation is wanted, RequestID/TraceID/UserID. Args
	// SHOULD already be sanitized by the producer — the bus does not redact on
	// emit; see SQLEvent.Args.
	EmitSQL(ev SQLEvent)
}

EventBus is a first-party, minimal view of the framework's in-process observability bus (the lower-level pkg/observability), for a module that renders a live activity feed — e.g. orbit's live SQL/HTTP view.

It exposes the subscribe operations a consumer needs plus a narrow SQL ingest (EmitSQL) for external producers, and moves nucleus-owned event VALUES (SQLEvent/HTTPEvent), not the bus's pooled, refcounted event objects. So a module never imports the lower-level package, keeps the bus types off its own surface, and is freed from the bus's pooled-event Release discipline — the adapter performs the required Release internally and hands the consumer a detached copy it owns outright (subscribe) or copies the producer's values in (emit).

Each Subscribe* method returns a receive-only channel and a cancel func. The caller MUST call cancel when finished, to unsubscribe and stop the backing goroutine; the channel is closed once cancel has run. A slow consumer drops events — the same backpressure the underlying bus applies — rather than blocking producers.

type Extension

type Extension = app.Extension

Extension is a re-export of `app.Extension`, the interface every production subsystem (storage, custom auth, the orbit admin module, …) implements to register itself with the application container. Pass values via `nucleus.WithExtensions(...)`.

type HTTPEvent

type HTTPEvent struct {
	EmittedAt time.Time
	NodeID    string
	Method    string
	Path      string
	Status    int
	Duration  time.Duration
	RequestID string
	TraceID   string
	UserID    string
	RemoteIP  string
	UserAgent string
	// PayloadPreview is an emit-time-sanitized preview: GET/DELETE show redacted
	// query params (keys containing KEY/SECRET/PASSWORD/TOKEN are masked — note
	// OAuth code/state are not), other methods show "body:redacted (...)". The
	// raw request body is never captured.
	PayloadPreview string
}

HTTPEvent is a detached, first-party copy of an HTTP-request observability event.

type Handler

type Handler func(*Context) error

Handler is the framework's handler signature. Modules and ad-hoc route registrations both produce values of this type. The framework adapts Handler to the underlying `*router.Router` Handler shape at registration time.

type Indexer

type Indexer interface{ Index(*Context) error }

Indexer handles GET /resource.

type JobRegistry

type JobRegistry interface {
	Register(name string, spec JobSpec) error
}

JobRegistry is the surface a module's Jobs closure receives to register background jobs. Jobs are executed by the provider selected with the `jobs_provider` config key: "memory" (default, in-process, backed by pkg/tasks/providers/memory) or "asynq" (Redis-backed, `jobs_redis_url` required). Registration errors — empty name, nil handler, missing or ambiguous schedule, invalid cron expression, duplicate name within a module — fail application boot.

type JobSpec

type JobSpec struct {
	// Handler runs on every scheduled tick. Required. It receives a
	// context that is cancelled on application shutdown (and bounded by
	// Timeout when one is set); a non-nil error is logged, never fatal.
	Handler func(ctx context.Context) error

	// Every schedules the job at a fixed interval (e.g. 30*time.Second).
	// Mutually exclusive with Cron.
	Every time.Duration

	// Cron schedules the job with a standard 5-field cron expression
	// ("*/5 * * * *") or a descriptor ("@hourly", "@daily", "@every 90s").
	// The framework validates the expression at registration time —
	// boot fails on an invalid spec — and translates it for the
	// configured provider, so the same spec means the same schedule on
	// both the memory and the asynq provider. Mutually exclusive with
	// Every.
	Cron string

	// Timeout bounds each run with a context deadline. Zero means no
	// per-run deadline; the run still stops when the application shuts
	// down.
	Timeout time.Duration

	// Singleton skips a tick while the previous run of this job is
	// still executing in this process, instead of overlapping runs.
	// Each skip is logged at WARN.
	Singleton bool
}

JobSpec describes one background job a module registers through JobRegistry.Register. Exactly one of Every or Cron must be set.

type LifecycleHooks

type LifecycleHooks struct {
	OnStart    func(context.Context) error
	OnShutdown func(context.Context) error
}

LifecycleHooks holds app-level callbacks that fire before the HTTP listener starts and after the listener returns. Module-level `OnStart` / `OnShutdown` continue to live on `ModuleSpec`; the hooks here are reserved for cross-cutting concerns that no module owns (e.g. external readiness signalling).

type MethodSet

type MethodSet struct {
	// contains filtered or unexported fields
}

MethodSet is the value type produced by `Methods(...)`. It carries the list of REST methods a Resource registration should expose. The framework treats the set as an unordered collection; ordering of arguments to `Methods()` does not affect routing.

func Methods

func Methods(ms ...ResourceMethod) MethodSet

Methods constructs a MethodSet from the supplied REST verbs. The canonical usage is at the call site of `Router.Resource`:

r.Resource("/tickets", ticketsController{}, nucleus.Methods(
    nucleus.Index, nucleus.Show, nucleus.Create,
))

func (MethodSet) Has

func (s MethodSet) Has(m ResourceMethod) bool

Has reports whether the given method is in the set.

type Middleware

type Middleware = func(http.Handler) http.Handler

Middleware is a standard net/http middleware function. The framework's router applies middleware in registration order; the user-facing builder method `AppBuilder.Use(...)` and the per-module `Module[C].Middleware` field both accept values of this type.

type Module

type Module[C any] struct {
	Name       string
	Prefix     string
	DefaultDB  string
	Requires   []string
	Models     []any
	Middleware []Middleware
	// Config is the module's typed configuration. At Run time (ADR-010 §2
	// layer 5) the framework binds the `modules.<Name>.*` config subtree into
	// it, fills still-zero fields from `default:` struct tags, and validates it
	// against `validate:` tags. Precedence is: a value set here (the
	// programmatic baseline) < the config file < `default:` tags fill only what
	// remains zero. Because defaulting keys off the zero value, a field
	// deliberately left at its zero value cannot be distinguished from "unset"
	// and will receive its `default:` tag value if it has one.
	Config C
	Routes func(r Router, cfg C)
	// Jobs registers background jobs on the real registry (executed by
	// the provider selected with `jobs_provider`); Webhooks mounts
	// signed inbound webhook routes under `webhooks_prefix`. Both run
	// once at startup, after OnStart. See JobRegistry / WebhookRegistry.
	Jobs       func(j JobRegistry, cfg C)
	Webhooks   func(w WebhookRegistry, cfg C)
	Migrations fs.FS
	// Templates carries the module's .html templates embedded in the
	// binary. At Run time each file registers in the application's
	// template engine under `<module-name>/<slash-path>` — before app.New,
	// the only window (html/template forbids Parse after the first
	// Execute, and sub-routers copy the engine at derivation). A handler
	// renders one with c.Render(status, "<name>/<path>", data). On a
	// name collision the host's templates_dir parses last and wins.
	Templates fs.FS
	// Policies contributes RBAC rows to the application's default-deny
	// enforcer so the module's routes work when mounted, without the host
	// editing rbac_policy.csv by hand. Objects are relative to Prefix; a
	// deny row in the host's policy file always overrides a module allow.
	// Malformed rules fail boot (ErrInvalidModulePolicy). See PolicyRule.
	Policies []PolicyRule
	// CSRFExempt lists path prefixes (relative to Prefix, raw-prefix
	// matched like csrf_exempt_paths) the module needs exempted from CSRF
	// protection — typically its JSON API paths. Declarative, not a
	// closure, because the exemption list is frozen inside the middleware
	// stack at app.New, before any module closure can run (the same
	// constraint that shaped the automatic webhook-prefix exemption).
	CSRFExempt []string
	OnStart    func(ctx context.Context, rt Runtime, cfg C) error
	OnShutdown func(ctx context.Context, rt Runtime, cfg C) error
}

Module is the generic constructor for typed module configs. Users instantiate it with their config type. The framework binds `modules.<Name>.*` into the `Config` field during configuration load (Phase 2 — the validator landing point); Phase 1 establishes the shape so module authors can adopt the generic surface today.

Call `Build()` to obtain the type-erased `ModuleSpec` that `AppBuilder.Mount` and `nucleus.App.Modules` expect.

func (Module[C]) Build

func (m Module[C]) Build() ModuleSpec

Build returns the type-erased `ModuleSpec` for this `Module[C]`, suitable for storage in `App.Modules` and `AppBuilder.Mount(...)`. The returned spec captures the module's typed `Config` by value so modifications to the Module after Build do not leak into the spec.

type ModuleSpec

type ModuleSpec interface {
	Name() string
	Prefix() string
	DefaultDB() string
	Requires() []string
	Models() []any
	Middleware() []Middleware
	Routes(r Router)
	Jobs(j JobRegistry)
	Webhooks(w WebhookRegistry)
	Migrations() fs.FS
	// OnStart runs before the module's Routes are registered (ADR-010
	// Phase 4, Gap 2), so a module initialises its managed resources here
	// — typically `rt.DB()` — and its Routes closure can then capture that
	// state directly. The `Runtime` handle replaces the former `*App`
	// config struct so modules reach the framework-managed connection pool
	// instead of opening their own.
	OnStart(ctx context.Context, rt Runtime) error
	OnShutdown(ctx context.Context, rt Runtime) error
	Config() any
}

ModuleSpec is the type-erased interface every module satisfies. It is the shape stored in `App.Modules` and consumed by `AppBuilder.Mount` and the framework's startup sequence.

Modules are self-contained units of feature organisation: a module brings its own routes, models, migrations, jobs, webhooks, policies and CSRF exemptions, and can be lifted into another application by adding it to that application's `Mount(...)` list.

Users do not implement `ModuleSpec` directly. They construct a `Module[C any]` with a typed configuration and call its `Build()` method, which returns a `ModuleSpec` wrapper.

type OpenAPISpec

type OpenAPISpec struct {
	Pattern string
	Handler http.Handler
}

OpenAPISpec declares a JSON OpenAPI document endpoint for the application to mount at Run time (ADR-010 Phase 4, Slice 2). Pattern is passed verbatim to the underlying mount, which normalises an empty value to "/openapi.json"; the struct field itself stores whatever was supplied.

Handler is any http.Handler that serves the document JSON — typically `openapi.Handler(provider)` for a generated document factory, but pre-rendered bytes, an embedded file, or a proxy work equally. The provider-typed `Provider` field was removed in v0.12.0 (DEP-2026-008); the stable surface is stdlib-only.

type Option

type Option = app.Option

Option is the configuration-time option type accepted by `Run` and stored in `App.Options`. It is a re-export of `app.Option` so callers can pass `nucleus.WithoutDefaults()` / `nucleus.WithExtensions(...)` without taking an explicit dependency on `pkg/app`.

func WithExtensions

func WithExtensions(exts ...Extension) Option

WithExtensions registers one or more production extensions to be attached during application construction. Mirrors `app.WithExtensions`.

func WithOpenAuthz

func WithOpenAuthz() Option

WithOpenAuthz is the explicit escape hatch from the default-deny Casbin enforcer mounted by `app.New` (ADR-004). Mirrors `app.WithOpenAuthz`. The framework logs a `WARN` at startup when this option is active so the choice is visible in operational telemetry.

func WithTemplateFuncs

func WithTemplateFuncs(funcs template.FuncMap) Option

WithTemplateFuncs re-exports `app.WithTemplateFuncs` (QCD-FW-11): template functions registered before the startup loader parses templates_dir. See the routing guide for the order of operations.

func WithTemplates

func WithTemplates(base *template.Template) Option

WithTemplates re-exports `app.WithTemplates` (QCD-FW-11): a prebuilt *template.Template used as the base the startup loader parses into.

func WithTemplatesFS

func WithTemplatesFS(prefix string, fsys fs.FS) Option

WithTemplatesFS re-exports `app.WithTemplatesFS`: an fs.FS whose .html files parse into the engine under a name prefix, accumulating across calls. Module authors usually declare `Module.Templates` instead, which rides this option automatically under the module's name.

func WithoutDefaults

func WithoutDefaults() Option

WithoutDefaults disables the framework's default extensions (storage, mail, authz). Mirrors `app.WithoutDefaults`. Use for lightweight services that compose their own extension set.

type Patcher

type Patcher interface{ Patch(*Context) error }

Patcher handles PATCH /resource/{id}.

type PolicyRule

type PolicyRule struct {
	Subject string
	Object  string
	Action  string
	Effect  string
}

PolicyRule is one RBAC row a module contributes to the application's default-deny enforcer, in the same shape as a rbac_policy.csv row: `p, Subject, Object, Action, Effect`.

Object is a route path RELATIVE to the module's mount Prefix (a module without a Prefix declares full paths) and supports the enforcer's keyMatch wildcards ("/notes/*"). Action is one of the framework's CRUD verbs (read|create|update|delete) or "*" — not a raw HTTP method. Effect is "allow", "deny", or empty (which defaults to "allow", the overwhelmingly common case for a module opening its own routes).

Module rows join the live in-memory ruleset only — they are never written to the host's policy file — and the Casbin policy effect (`some(allow) && !some(deny)`) means a deny row in the host's CSV always overrides a module's allow: the module proposes, the operator disposes.

type ResourceMethod

type ResourceMethod int

ResourceMethod identifies a REST verb to register on a Resource. Callers compose a `MethodSet` via `nucleus.Methods(...)` and pass it as the third argument of `Router.Resource`. The framework asserts the controller satisfies the corresponding sub-interface (Indexer, Shower, Creator, Updater, Patcher, Destroyer) for each requested method, and registers only the requested verbs. Registration is auditable: a quick read of the call site shows which routes a controller exposes.

const (
	// Index registers GET /resource backed by the Indexer sub-interface.
	Index ResourceMethod = iota
	// Show registers GET /resource/{id} backed by the Shower sub-interface.
	Show
	// Create registers POST /resource backed by the Creator sub-interface.
	Create
	// Update registers PUT /resource/{id} backed by the Updater sub-interface.
	Update
	// Patch registers PATCH /resource/{id} backed by the Patcher sub-interface.
	Patch
	// Destroy registers DELETE /resource/{id} backed by the Destroyer sub-interface.
	Destroy
)

type Router

type Router interface {
	Get(path string, handlers ...Handler)
	Post(path string, handlers ...Handler)
	Put(path string, handlers ...Handler)
	Patch(path string, handlers ...Handler)
	Delete(path string, handlers ...Handler)
	Group(prefix string, fn func(g Router))
	Resource(path string, controller any, methods MethodSet)

	// With returns a Router that applies the given middleware to every route
	// registered on the returned value, WITHOUT affecting routes registered on
	// the parent — the per-route / per-scope counterpart to a module's global
	// Middleware. Chain it before a single route to guard just that endpoint:
	//
	//	r.With(rt.Authorizer().RequireRole("admin")).Get("/billing", billing)
	//
	// Middleware is `func(http.Handler) http.Handler`, so any standard net/http
	// middleware — the framework's `Enforcer.RequireRole`, `router.CSRFMiddleware`,
	// or a hand-written guard — mounts directly, with no adapter. With composes
	// additively: each nested With / Group layer adds to the chain (outer→inner),
	// on top of any module-level Middleware.
	With(mw ...Middleware) Router

	// Mount attaches a standard http.Handler subtree at pattern (joined to the
	// module's prefix). Everything under pattern is delegated to h — use it to
	// mount a self-contained sub-application whose internal routing the framework
	// should not interpret: an admin panel's own router (e.g. orbit), a
	// static-file server, or any third-party http.Handler. Unlike Get/Post/…,
	// which register a single endpoint, Mount owns the whole subtree below
	// pattern. A request for the bare pattern without a trailing slash (e.g.
	// GET /admin) is 307-redirected to the canonical pattern/ (GET /admin/).
	// Module-level and With/Group middleware still wrap the mounted handler.
	Mount(pattern string, h http.Handler)
}

Router is the routing surface a module receives via `ModuleSpec.Routes(r Router)`. It is defined in `pkg/nucleus` (not as an alias for `*router.Router`) so that modules do not take a hard import on `pkg/router`. The framework constructs the implementation in `Start()`; module code never instantiates a `Router` itself.

Three coexisting styles are supported per ADR-010 §7:

  • Flat declarative: `r.Get("/articles", ListArticles)` — small or audit-sensitive modules.
  • REST resource: `r.Resource("/tickets", controller{}, nucleus.Methods(Index, Show, Create))` — CRUD modules. Methods to register are passed explicitly via a variadic argument; the framework does not discover methods via reflection. Adding a `Patch` method to the controller does not silently register a PATCH route.
  • Nested groups: `r.Group("/admin", func(g Router) { ... })` — areas with nested URL hierarchy and inherited middleware. Middleware composes additively at every group level.

Mixing the three styles within the same module is supported.

type Runtime

type Runtime interface {
	// DB returns the managed `*sql.DB` for the module's database alias
	// (the module's `DefaultDB`, or the application default when unset).
	// It returns nil only when no database is configured for that alias —
	// a misconfiguration the module should surface as an OnStart error.
	DB() *sql.DB

	// DBForRequest resolves the managed `*sql.DB` for the request's resolved
	// scope: the tenant's isolated database when multi-tenant resolution is
	// active (`multitenant.*`), the site's database under multi-site, and the
	// application default otherwise. It mirrors
	// `(*app.App).DatabaseForRequest` semantics — including the
	// tenant-isolation-violation error when an unresolvable tenant would
	// otherwise fall through to a shared database under
	// `multitenant.require_isolated_db` — so module handlers in multi-tenant
	// applications should prefer it over DB(), which is bound to one static
	// alias for the whole module lifetime.
	DBForRequest(r *http.Request) (*sql.DB, error)

	// AutoMigrate synchronises the schema for the given models. NOTE: unlike
	// DB(), it does NOT scope to the module's bound DefaultDB alias — each
	// model is migrated against the database alias declared in its own
	// metadata (defaulting to the application default). It is a development
	// convenience; production deployments should prefer explicit SQL
	// migrations (`nucleus migrate up`), consistent with the SPEC's
	// SQL-first stance.
	AutoMigrate(models ...any) error

	// ApplyModuleMigrations applies THIS module's embedded migrations
	// (`Module.Migrations`) against the module's bound database alias,
	// through the real migration pipeline: the module-scoped ledger
	// (storage IDs prefixed `<module>/`, ADR-010 §16) with checksum
	// tracking — unlike AutoMigrate, which bypasses both. Already-applied
	// migrations are skipped, so the call is idempotent across restarts.
	//
	// This is a DELIBERATE call — the framework never invokes it (ADR-013
	// §R1: application boot never mutates the schema on its own; ADR-022
	// wires the field to this explicit call). A module that wants its
	// embedded migrations live from day one calls it in OnStart; an
	// application that wants operator-controlled schema changes leaves it
	// out and ships the SQL through `nucleus migrate up` instead. Like the
	// CLI, it runs to completion once started. It errors when the module
	// declares no Migrations, on an unbacked runtime, and on any
	// migration failure.
	ApplyModuleMigrations() error

	// Logger returns the application's structured logger. It is never nil;
	// callers always receive at least `slog.Default()`.
	Logger() *slog.Logger

	// Session returns the application's session manager — the same
	// instance whose LoadAndSave middleware the framework mounts on every
	// request, so handlers can already read and write session values
	// through the request context. Modules need the manager itself for the
	// operations that go beyond get/put: `RenewToken` after a successful
	// login (session-fixation defence), `Destroy`/`Invalidate` on logout,
	// and flash messaging. The manager is constructed unconditionally by
	// `app.New`; Session returns nil only on an unbacked runtime.
	Session() *auth.SessionManager

	// Authorizer returns the application's RBAC enforcer (ADR-004) — the
	// same instance behind the framework's default-deny middleware and the
	// admin panel. Modules use it to mount `RequireRole` middleware on
	// their routes, manage role groupings (`AddRole`/`RemoveRole`), or
	// audit live policy through the read-only forwarders (`GetPolicy`,
	// `GetGroupingPolicy`, `GetAllRoles`). Returns nil on an unbacked
	// runtime AND when the RBAC subsystem was not attached (an app built
	// with `app.WithoutDefaults()`) — guard accordingly.
	//
	// Mutations (`AddPolicy`/`Deny`/`AddRole`) act on the live in-memory
	// ruleset only: the policy file is read once at startup and runtime
	// changes do not persist across restarts.
	Authorizer() *authz.Enforcer

	// Mailer returns the application's outbound mail sender — the same
	// instance the framework built from the `mail_*` config and wrapped
	// with the health check and circuit breaker. Modules send through it
	// (e.g. from a signal handler or a task) rather than constructing their
	// own sender, which would bypass that lifecycle. Returns nil on an
	// unbacked runtime AND when the mail subsystem was not attached
	// (`app.WithoutDefaults()`) — guard accordingly. When attached, the
	// default driver is a no-op sender, so a non-nil Mailer is safe to call
	// even when no SMTP is configured.
	Mailer() mail.Sender

	// Storage returns the application's object store — the same instance the
	// framework built from the `storage_*` config, with its background
	// cleaner and circuit breaker. Modules Put/Get/SignedURL through it for
	// uploads and generated artifacts (report exports, etc.) instead of
	// opening their own store. Returns nil on an unbacked runtime AND when
	// the storage subsystem was not attached (`app.WithoutDefaults()`).
	Storage() storage.Store

	// JWT returns the application's JWT manager — the same instance the
	// framework built from `jwt_secret` / `jwt_keys[]` (and whose JWKS the
	// framework auto-mounts for asymmetric keys). Modules mint bearer tokens
	// (`Generate`) and validate them (`Validate`, or mount `Middleware` /
	// `OptionalJWTMiddleware`) through it instead of constructing their own
	// manager from a duplicated secret.
	//
	// Returns nil on an unbacked runtime AND when no signing material is
	// configured (`App.JWT` is nil) — a read-only service that only consumes
	// JWTs minted by an external IdP may legitimately leave it unset, so guard
	// accordingly.
	//
	// Treat the returned manager as read-only from request handlers: its
	// `RotateKey` / `RemoveKey` methods mutate the shared keyset for every
	// concurrent caller (and the framework's JWKS endpoint), so key lifecycle
	// is an operator concern, not a per-request module call — the same posture
	// as `Authorizer()`'s in-memory policy mutations.
	JWT() *auth.JWTManager

	// Models returns the application's model registry — the same instance the
	// framework registers every mounted module's `Models()` into. A module that
	// hosts a generic data UI (enumerate models, run CRUD over arbitrary models)
	// reads it; ordinary modules that only use their own typed models do not need
	// it. Returns nil on an unbacked runtime. The registry is process-wide and
	// shared — treat schema mutations (`UpdateFieldMeta`) as an operator concern,
	// not a per-request call.
	Models() *model.Registry

	// Databases returns a snapshot of every configured managed database handle,
	// keyed by alias (the application default included), unwrapped to `*sql.DB`.
	// Unlike `DB()` — bound to the module's single alias — this exposes all
	// handles, for a module that browses across databases (a data console over a
	// multi-database or multi-tenant topology). The returned map is a copy:
	// mutating it does not affect the framework's registry, and the handles
	// remain framework-owned (a module must NOT close them). Returns nil on an
	// unbacked runtime; an alias whose handle cannot be unwrapped is omitted.
	//
	// It is NOT scoped to the module's own alias or the request's tenant — every
	// configured handle is returned, so the caller owns any tenant-isolation
	// policy. Intended for a trusted, first-party admin module (orbit).
	Databases() map[string]*sql.DB

	// DatabaseHandle returns the framework's managed *db.DB wrapper for the
	// default database. It is the engine-aware handle a deep module needs for
	// operations the raw *sql.DB cannot do — `db.NewMigrator`'s dialect-aware DDL,
	// and `Engine()`/`System()` for dialect detection. Most modules want DB() (the
	// *sql.DB) instead. Returns nil on an unbacked runtime or when no default
	// database is configured (e.g. app.WithoutDefaults()). The handle is
	// framework-owned — a module must NOT Close it.
	DatabaseHandle() *db.DB

	// DatabaseHandles returns a snapshot of every managed *db.DB wrapper keyed by
	// alias — the engine-aware counterpart to Databases() (which returns *sql.DB)
	// — for a module that needs per-database dialect/migration capability across a
	// multi-database topology (e.g. orbit's admin). The map is freshly allocated;
	// the handles remain framework-owned (do NOT Close them). Nil on an unbacked
	// runtime.
	DatabaseHandles() map[string]*db.DB

	// Observability returns a first-party view of the framework's in-process
	// event bus, for a module that renders a live activity feed (orbit's live
	// SQL/HTTP view). It emits nucleus-owned event values through EventBus, so
	// the module never touches the lower-level pkg/observability surface and is
	// freed from its pooled-event Release discipline. Returns nil on an unbacked
	// runtime or when no bus is attached.
	Observability() EventBus

	// AuthChain returns the ordered authentication chain built from
	// auth_backends, or nil when none is declared. A module that owns a
	// login route — an admin panel, a custom sign-in page — authenticates
	// through this rather than reaching for the user table itself, so the
	// operator's declared order (directory first, local account second)
	// applies to every entry point in the application, not only the ones
	// the framework happens to own.
	AuthChain() *auth.Chain

	// Outbox returns the application's managed transactional outbox — the
	// same instance the framework built from the `outbox.*` config, whose
	// dispatcher and bridges are already running. A module enqueues events
	// through it (Enqueue, or EnqueueTx inside its own transaction for the
	// transactional guarantee) instead of writing to the outbox table by
	// hand. Returns nil on an unbacked runtime AND when the outbox is
	// disabled (`outbox.enabled: false`) — guard accordingly (NF-13).
	Outbox() *outbox.ManagedOutbox

	// Tasks returns the shared task manager of the module-jobs runtime —
	// the handle a module uses to enqueue ONE-OFF background tasks
	// (EnqueueJSON and friends) beyond the cron jobs it declared in
	// ModuleSpec.Jobs (NF-13). The provider is the configured
	// `jobs_provider` (memory or asynq).
	//
	// Availability: the manager exists once the jobs runtime has started —
	// which happens when at least one module registered a job, OR when
	// `jobs_provider` names a broker-backed provider (asynq) even with no
	// cron jobs — configuring a broker is the opt-in for enqueue-only
	// applications (the in-process memory provider is the framework
	// default, so it cannot double as that signal and is only built when
	// jobs exist). The runtime starts AFTER every module's OnStart,
	// so Tasks() returns nil inside OnStart — resolve it lazily from
	// request handlers or job closures, and treat nil as "no jobs runtime
	// configured". A handler for a one-off task type is registered through
	// the same manager's HandleFunc (both in-tree providers accept
	// registration at any time); register before the first enqueue of that
	// type, or the provider treats the early deliveries as failures per
	// its retry policy.
	Tasks() tasks.Manager
}

Runtime is the handle a module receives in its OnStart and OnShutdown lifecycle hooks. It is a thin, stable façade over the running application container, exposing the managed resources a module needs — the shared database pool, schema migration, and the structured logger — without leaking the full `*app.App` surface onto the module contract.

Modules MUST use `rt.DB()` instead of opening their own `*sql.DB`. The returned handle is owned by the framework: it draws from the configured connection pool, participates in the framework's startup/shutdown lifecycle (the framework closes it — a module must NOT), and honours the application's database configuration. The handle is bound to the module's declared `DefaultDB` alias; a module that leaves `DefaultDB` empty receives the application's default database.

Runtime is implemented by the framework, not by users. New methods may therefore be added in future minor versions without breaking module authors (who only ever consume the interface).

type SQLEvent

type SQLEvent struct {
	EmittedAt time.Time
	NodeID    string
	ModelName string
	Operation string
	Query     string
	// Args are the bound arguments AFTER the observability layer's emit-time
	// sanitization: string and []byte values are replaced with a "type(len):***"
	// marker, but numeric, bool, time.Time and nil args are kept verbatim (so a
	// "WHERE id = ?" key appears as e.g. "42"). Operator-only; see EventBus.
	Args     []string
	Duration time.Duration
	Err      string
	// RowsAffected is the driver-reported row count for exec-style
	// operations (INSERT/UPDATE/DELETE); 0 means "not reported" —
	// SELECT paths and drivers without support. Additive in v1.1.0.
	RowsAffected int64
	RequestID    string
	TraceID      string
	UserID       string
}

SQLEvent is a detached, first-party copy of a SQL-statement observability event. All fields are plain values the consumer owns; there is no Release obligation.

type ServiceRegistration

type ServiceRegistration struct {
	Name   string
	Run    func(context.Context) error
	Health func(context.Context) error
}

ServiceRegistration declares a long-running background goroutine the framework should manage alongside the HTTP listener. `Run` receives a context that the framework cancels at shutdown; the function must return when its context is cancelled.

`Health` is optional. When set (and Name is non-empty), the framework surfaces it in /healthz as the check `service:<Name>`: a nil return is healthy, an error marks the check unhealthy and flips the endpoint to 503. Keep it cheap and respect the ctx deadline — it runs on every /healthz request alongside the dependency probes.

type Shower

type Shower interface{ Show(*Context) error }

Shower handles GET /resource/{id}.

type Updater

type Updater interface{ Update(*Context) error }

Updater handles PUT /resource/{id}.

type WebhookRegistry

type WebhookRegistry interface {
	Register(path string, spec WebhookSpec) error
}

WebhookRegistry is the surface a module's Webhooks closure receives to register inbound webhook handlers. Each registration mounts a real route at `<webhooks_prefix>/<module-name><path>` (webhooks_prefix defaults to "/webhooks") on the application router. When CSRF protection is enabled, the webhook prefix is exempted automatically — webhooks authenticate by signature, not by CSRF token. Registration errors — empty path, non-canonical path (one that path.Clean would rewrite: "." or ".." segments, duplicate or trailing slashes), nil handler, duplicate path, or a TimestampTolerance that is negative or lacks a Secret — fail application boot.

type WebhookSpec

type WebhookSpec struct {
	// Handler serves the request after the framework's checks (method,
	// body cap, signature) pass. Required. The request body has already
	// been read for signature verification and is replayed, so Handler
	// reads it as usual.
	Handler http.HandlerFunc

	// Secret, when non-empty, requires each request to carry a valid
	// HMAC-SHA256 signature of the raw request body in the
	// X-Nucleus-Signature header, formatted "sha256=<hex>" (see
	// SignWebhookBody). Requests with a missing or invalid signature
	// are rejected with 401 before Handler runs. When empty, no
	// signature is checked and the framework WARNs once at boot —
	// Handler must then authenticate the caller itself.
	//
	// Declared limit — no anti-replay: the signature authenticates
	// content, not freshness or uniqueness. A captured signed request
	// verifies again if re-sent verbatim. Handlers whose effects are not
	// idempotent should deduplicate by an event ID carried in the
	// payload; setting TimestampTolerance additionally narrows the
	// replay window to that tolerance (dedup is still what closes it
	// within the window).
	Secret string

	// Methods restricts the accepted HTTP methods. Default: POST only.
	// Other methods receive 405 with an Allow header.
	Methods []string

	// MaxBytes caps the request body size; larger bodies are rejected
	// with 413. Default: 1 MiB.
	MaxBytes int64

	// TimestampTolerance, when positive, switches the signature check to
	// the timestamped scheme: each request must carry its send time as
	// decimal Unix seconds in the X-Nucleus-Timestamp header, the time
	// must lie within ±TimestampTolerance of the receiver's clock, and
	// the signature must cover `<timestamp>.<body>` — senders use
	// SignWebhookBodyWithTimestamp, which returns both header values.
	// Missing, malformed, out-of-tolerance, or unsigned timestamps are
	// rejected with 401 before Handler runs.
	//
	// Zero (the default) keeps the compatible body-only scheme of
	// SignWebhookBody with no timestamp requirement. Opt-in because it
	// changes what senders must sign; 5m is a reasonable tolerance for
	// senders with NTP-synced clocks. Setting it without a Secret is a
	// registration error (the timestamp is only trustworthy signed), as
	// is a negative value.
	TimestampTolerance time.Duration
}

WebhookSpec describes one inbound webhook a module registers through WebhookRegistry.Register.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL