app

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: 49 Imported by: 0

Documentation

Overview

Package app provides the application configuration and bootstrap for Nucleus. Configuration is loaded from multiple sources with increasing precedence: struct defaults < YAML file < environment variables (prefix NUCLEUS_).

config_validate_layers.go implements ADR-010 §2 layers 3 and 4 on the application Config: field-semantic validation (ranges, enums, parseable durations) and config-level referential validation (cross-field rules). Layers 1 (syntactic) and 2 (schema / unknown-fields) ship in config.go and config_validate.go.

These layers were born in pkg/nucleus and ran only on its two surfaces (FromConfigFile and the direct-struct Run) — the CLI's LoadConfig skipped them, so `log_level: verbose` failed `go run .` but sailed through every `nucleus <cmd>`: the "same file, two verdicts" class the DX audit named (DX-13 closed it for unknown keys; this closes layers 3–4). They live here so every config consumer — builder, direct struct, and all 38 CLI commands — applies the same verdict; pkg/nucleus re-exports the error sentinels for compatibility.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNilConfig indicates New was called with a nil config pointer.
	ErrNilConfig = errors.New("config is nil")
	// ErrNilApp indicates a method was called on a nil *App receiver.
	ErrNilApp = errors.New("app is nil")
	// ErrNotInitialized indicates required app dependencies are missing.
	ErrNotInitialized = errors.New("app is not fully initialized")
	// ErrServerAlreadyRunning indicates Run was invoked while the server is already running.
	ErrServerAlreadyRunning = errors.New("server is already running")
	// ErrModelsRegistryNotInitialized indicates model registration was attempted before models setup.
	ErrModelsRegistryNotInitialized = errors.New("models registry is not initialized")
	// ErrDatabaseAliasNotFound indicates an unknown database alias was requested.
	ErrDatabaseAliasNotFound = errors.New("database alias not found")
	// ErrTenantIsolationViolation indicates tenant routing resolved to a shared DB alias.
	ErrTenantIsolationViolation = errors.New("tenant isolation violation")
)
View Source
var ErrInvalidConfigReference = errors.New("nucleus: invalid configuration reference")

ErrInvalidConfigReference is returned when a configuration value is individually valid but inconsistent with another related key (ADR-010 §2 layer 4). The wrapped message names both keys and the rule they violate.

View Source
var ErrInvalidConfigValue = errors.New("nucleus: invalid configuration value")

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). The wrapped message names the offending key, its value, and the accepted set or range.

Functions

func ApplyProfile

func ApplyProfile(cfg *Config) error

ApplyProfile applies the named configuration preset (DX-23). The "dev" profile swaps every backing-service selection for its no-dependency counterpart so a realistic production config boots with zero external services: in-memory sessions and jobs, local filesystem storage, the no-op mailer, and a SQLite database (an already-SQLite URL is kept so the profile never moves an existing dev database). Extra database aliases — replicas, analytics — are dropped with the same rationale. Exported because the fluent loader (pkg/nucleus) produces its Config by its own merge and must apply the same preset semantics.

func ContractConfigKeyPatterns

func ContractConfigKeyPatterns() []string

ContractConfigKeyPatterns returns a sorted set of config key patterns exposed by the runtime configuration contract.

The returned keys are intended for compatibility guardrails. Map-valued subtrees are represented as wildcard patterns: - databases.<alias>.* - multisite.sites.<site>.* - multitenant.tenants.<tenant>.*

func DatabaseAliasFromContext

func DatabaseAliasFromContext(ctx context.Context) string

DatabaseAliasFromContext returns the DB alias selected for the request.

func NormalizeRuntimeConfig

func NormalizeRuntimeConfig(cfg *Config)

NormalizeRuntimeConfig applies the framework's runtime-config normalisations (database alias canonicalisation, multi-site / multi-tenant resolver normalisation) to cfg in place. `app.LoadConfig` calls this internally before returning; callers that bypass `LoadConfig` (most notably the multi-file loader in `pkg/nucleus.FromConfigFile`) need to call this so they produce a `*Config` indistinguishable from the env-var path. Safe to call with cfg == nil (no-op).

func QuickStart

func QuickStart(fn func(a *App) error, opts ...Option)

QuickStart provides a "standard" entry point for enterprise applications. It handles configuration loading, app initialization, signal handling for graceful shutdown, and error reporting.

Example:

func main() {
    app.QuickStart(func(a *app.App) error {
        a.Router.Get("/", func(c *router.Context) error {
            return c.String(200, "Hello Nucleus")
        })
        return nil
    })
}

func SiteFromContext

func SiteFromContext(ctx context.Context) string

SiteFromContext returns the resolved site name or empty when unavailable.

func TenantFromContext

func TenantFromContext(ctx context.Context) string

TenantFromContext returns the resolved tenant id or empty when unavailable.

func ValidateReferential

func ValidateReferential(cfg *Config) error

ValidateReferential applies ADR-010 §2 layer-4 cross-field checks to a fully-merged config. Like layer 3 it treats empty/zero as "use the default" — a rule fires only when the governing key is explicitly set to the value that makes the dependent key mandatory. It relies on ValidateSemantics having run first (it does not re-validate field shapes). The module half of layer 4 — Requires() against configured database aliases — lives in pkg/nucleus, where the module set exists.

func ValidateSemantics

func ValidateSemantics(cfg *Config) error

ValidateSemantics applies ADR-010 §2 layer-3 checks to a fully-merged config. Empty strings and zero numerics are accepted: they denote "use the framework default" (defaults are applied by app.New), so a zero-value or partial config passes — only an explicitly wrong value fails.

Types

type App

type App struct {
	Config     *Config
	Logger     *slog.Logger
	Router     *router.Router
	DB         *db.DB
	DBs        map[string]*db.DB
	Mailer     mail.Sender
	Session    *auth.SessionManager
	JWT        *auth.JWTManager
	Models     *model.Registry
	Authorizer *authz.Enforcer
	Storage    storage.Store
	// AuthChain is the ordered authentication chain built from
	// auth_backends. Nil when the list is empty — an application with
	// nothing to authenticate against says so by leaving it unset rather
	// than carrying an empty chain that always rejects.
	AuthChain *auth.Chain

	// AuthFederated is the configured browser-redirect identity providers,
	// or nil when none are declared. It is separate from AuthChain because
	// a federated flow has no credentials to hand to a chain: the user
	// leaves, comes back, and only then is there an identity.
	AuthFederated *auth.FederatedSet
	Outbox        *outbox.ManagedOutbox
	Templates     *template.Template

	// I18n resolves message keys against the compiled catalogs found under
	// `locales_path` (the JSON bundles `nucleus compilemessages` writes),
	// with `default_locale` as the fallback. Non-nil only when at least one
	// compiled catalog was found at startup; in that case the Accept-Language
	// negotiation middleware is mounted on the Router and handlers can
	// translate via c.T(...) / i18n.T(ctx, ...). See pkg/i18n.
	I18n *i18n.Translator

	// Observability is the in-process event bus for HTTP, SQL, session and
	// custom events. It is always non-nil after app.New returns.
	// Subscribers (such as the orbit admin module) attach to it directly.
	// See pkg/observability for the full ownership model.
	Observability *observability.Bus

	// SessionRecorder produces session-change events on the Observability
	// bus. It is used by the session manager middleware below.
	SessionRecorder *hooks.SessionRecorder
	// contains filtered or unexported fields
}

App is the main Nucleus application container. It wires the minimum runtime dependencies (config, logger, router, DB, and model registry).

By default, app.New(cfg) initializes all subsystems (storage, mail, authz). Use app.WithoutDefaults() to initialize only core, then add extensions explicitly.

func Bootstrap

func Bootstrap(configPath string, opts ...Option) (*App, error)

Bootstrap is a high-level helper that loads the configuration from a file and initializes a new App container with default settings. It simplifies the typical main.go boilerplate into a single call.

func New

func New(cfg *Config, opts ...Option) (*App, error)

New creates an application container with default wiring.

When called without options, New initializes all subsystems (storage, mail, authz) — identical to pre-extension behavior.

Use WithoutDefaults() for a lightweight core-only app:

a, err := app.New(cfg, app.WithoutDefaults())

Use WithExtensions() to selectively add subsystems:

a, err := app.New(cfg,
    app.WithoutDefaults(),
    app.WithExtensions(myExtension()),
)

func (*App) AutoMigrate

func (a *App) AutoMigrate(models ...any) error

AutoMigrate synchronizes the database schema with the provided model definitions. Supported dialects: SQLite, PostgreSQL, MySQL, MSSQL, and Oracle. Unknown engines return ErrAutoMigrate — use explicit SQL migration files plus `nucleus migrate` instead (see `website/docs/getting-started/quickstart.md` for the multi-driver path). It extracts metadata from models and executes dialect-aware `CREATE TABLE` statements, idempotent where the engine supports it (`CREATE TABLE IF NOT EXISTS` on sqlite/postgres/mysql, `IF OBJECT_ID … IS NULL` on MSSQL, PL/SQL ORA-00955 swallow on Oracle). Existing tables are not modified by AutoMigrate — schema evolution still requires migrations.

func (*App) Database

func (a *App) Database(alias string) (*db.DB, error)

Database resolves a database handle by alias. Empty alias means default.

func (*App) DatabaseForRequest

func (a *App) DatabaseForRequest(r *http.Request) (*db.DB, error)

DatabaseForRequest returns the DB selected for the current request scope. If no request scope is available, the default DB alias is used.

func (*App) DefaultDB

func (a *App) DefaultDB() *sql.DB

DefaultDB returns the primary database connection.

func (*App) DefaultDatabaseAlias

func (a *App) DefaultDatabaseAlias() string

DefaultDatabaseAlias returns the active default database alias.

func (*App) MountOpenAPIHandler

func (a *App) MountOpenAPIHandler(pattern string, handler http.Handler) error

MountOpenAPIHandler mounts a JSON OpenAPI document endpoint exactly once per path, served by any stdlib http.Handler — typically `openapi.Handler(provider)` for a generated document factory. This is the stdlib-first replacement for MountOpenAPI (DEP-2026-008).

func (*App) OnShutdown

func (a *App) OnShutdown(fn func(context.Context) error)

OnShutdown registers a callback executed during shutdown in reverse order.

func (*App) RegisterHealthProbe

func (a *App) RegisterHealthProbe(name string, fn func(context.Context) error)

RegisterHealthProbe adds a caller-owned check to /healthz under the given name. The nucleus runtime uses it to surface each ServiceRegistration.Health as "service:<name>"; applications may register their own domain probes the same way. Call before the server starts serving — registration is not synchronized with in-flight /healthz requests.

func (*App) RegisterModel

func (a *App) RegisterModel(m interface{}, cfg ...model.ModelConfig) error

RegisterModel registers a model in the shared model registry.

func (*App) Run

func (a *App) Run(ctx context.Context) error

Run starts the HTTP server and blocks until context cancellation or SIGINT/SIGTERM.

func (*App) Shutdown

func (a *App) Shutdown(ctx context.Context) error

Shutdown gracefully stops the HTTP server (if started) and runs shutdown hooks.

type AzureStorageSpec

type AzureStorageSpec struct {
	AccountName     storage.CredentialSource `koanf:"account_name"`
	AccountKey      storage.CredentialSource `koanf:"account_key"`
	Container       string                   `koanf:"container"`
	PublicContainer string                   `koanf:"public_container"`
}

AzureStorageSpec mirrors pkg/storage.AzureConfig.

type BridgeConfig

type BridgeConfig struct {
	Name   string                 `koanf:"name"`
	Type   string                 `koanf:"type"` // kafka, webhook, rabbitmq
	Config map[string]interface{} `koanf:"config"`
}

BridgeConfig configures an external message bridge (Kafka, Webhook, RabbitMQ, etc.).

type CircuitBreakerSpec

type CircuitBreakerSpec struct {
	// Enabled turns on circuit-breaker wrapping for the package.
	Enabled bool `koanf:"enabled"`

	// FailureThreshold is the number of consecutive failures required
	// to trip the breaker open.
	FailureThreshold int `koanf:"failure_threshold"`

	// Cooldown is the duration the breaker stays open before admitting
	// half-open probes.
	Cooldown time.Duration `koanf:"cooldown"`

	// HalfOpenMaxConcurrent caps in-flight probes in the half-open
	// state.
	HalfOpenMaxConcurrent int `koanf:"half_open_max_concurrent"`
}

CircuitBreakerSpec is the koanf-bindable shape for the optional circuit breaker wrapping mail and storage. The same struct backs `mail_circuit_breaker.*` and `storage.circuit_breaker.*` config keys.

Defaults applied by DefaultConfig are Enabled=true, FailureThreshold=5, Cooldown=30s, HalfOpenMaxConcurrent=1.

type CleanupStorageSpec

type CleanupStorageSpec struct {
	Enabled  bool   `koanf:"enabled"`
	Interval string `koanf:"interval"`
	Prefix   string `koanf:"prefix"`
	MaxAge   string `koanf:"max_age"`
}

CleanupStorageSpec mirrors pkg/storage.CleanupConfig.

type Config

type Config struct {
	// Server
	Host         string        `koanf:"host"`
	Port         int           `koanf:"port"`
	ReadTimeout  time.Duration `koanf:"read_timeout"`
	WriteTimeout time.Duration `koanf:"write_timeout"`
	IdleTimeout  time.Duration `koanf:"idle_timeout"`
	// RequestTimeout bounds a handler: past it the client gets a 503 and the
	// handler's context is cancelled. Zero means the default (30s); a
	// negative value disables the timeout for every route. Until 1.24 the
	// request timeout silently reused read_timeout and could not be turned
	// off (NU-6). Streaming routes are better served by TimeoutExemptPaths.
	RequestTimeout time.Duration `koanf:"request_timeout"`
	// TimeoutExemptPaths are URL path prefixes served without the request
	// timeout and with the raw response writer (Flush and Hijack work):
	// server-sent events, long polls, large downloads. Requests whose
	// Accept header asks for text/event-stream are exempt without listing.
	TimeoutExemptPaths []string `koanf:"timeout_exempt_paths"`

	// TLS configuration (optional — empty disables HTTPS)
	TLSCertFile string `koanf:"tls_cert_file"`
	TLSKeyFile  string `koanf:"tls_key_file"`

	// Database
	DatabaseDefault string                    `koanf:"database_default"`
	Databases       map[string]DatabaseConfig `koanf:"databases"`

	// Multi-site and multi-tenant routing.
	MultiSite   MultiSiteConfig   `koanf:"multisite"`
	MultiTenant MultiTenantConfig `koanf:"multitenant"`

	// Redis (optional — empty disables Redis-backed features)
	RedisURL string `koanf:"redis_url"`

	// Auth
	// AuthBackends is the ORDERED list of authentication backends the
	// login path consults, by registered name (auth.RegisterBackend).
	//
	// Order is the feature, not a detail: `[ldap, local]` means the
	// directory answers first and the application's own user table is what
	// still works the morning the directory does not. A backend that
	// cannot reach its source is skipped rather than treated as a
	// rejection, which is what makes that break-glass account usable.
	//
	// Empty means the chain is not built. An application with no user
	// provider and no directory has nothing to authenticate against, and
	// saying so with an empty list is clearer than inventing a default.
	AuthBackends []string `koanf:"auth_backends"`

	// AuthFederated declares the browser-redirect identity providers —
	// OIDC, SAML — an operator wants sign-in buttons for.
	//
	// Each entry names an INSTANCE and the registered provider type that
	// implements it, and reads its settings from `auth.<name>.*`, the same
	// subtree a credential backend uses:
	//
	//	public_base_url: https://app.example.com
	//	auth_federated:
	//	  - name: corp
	//	    provider: oidc
	//	    display_name: Corp SSO
	//	  - name: partners
	//	    provider: oidc
	//	auth:
	//	  corp:
	//	    issuer: https://login.corp.example/
	//	  partners:
	//	    issuer: https://idp.partners.example/
	//
	// Instance and type are separate names because two identity providers
	// of the same protocol is the ordinary case, and a registry keyed by
	// type alone would have made the second one impossible to express.
	//
	// This list is independent of AuthBackends: a federated flow has no
	// credentials to hand to a chain, so it is not a link in one. An
	// application usually wants both — a directory or local table for
	// break-glass, and an identity provider for everyone else.
	AuthFederated []auth.FederatedInstance `koanf:"auth_federated"`

	// PublicBaseURL is the address the BROWSER reaches this application
	// at, without a trailing slash. Federated sign-in needs it because the
	// callback URL an operator registers with their identity provider has
	// to be the one this application will be listening on, and the address
	// the process binds is frequently not it (a reverse proxy, a
	// container port). Required only when AuthFederated is non-empty.
	PublicBaseURL string `koanf:"public_base_url"`

	// HTTPInterceptors is the ORDERED list of registered request
	// interceptors to wrap the router in, outermost first.
	//
	// Order is the behaviour rather than a detail — authentication before
	// rate limiting and rate limiting before authentication are different
	// systems — so an interceptor is not merely enabled here, it is
	// placed, the same way auth_backends places a backend.
	//
	// Settings live under `interceptors.<name>.*`, mirroring how
	// auth_backends pairs with auth.<name>.*: the list orders, the
	// subtree configures.
	//
	//	http_interceptors: [audit, tenant-guard]
	//	interceptors:
	//	  audit:
	//	    sink: stdout
	//
	// A name nobody registered fails at BOOT, naming what is registered: a
	// typo in a list of request interceptors must not resolve to one fewer
	// protection, quietly.
	HTTPInterceptors []string `koanf:"http_interceptors"`

	// InterceptorConfig maps a registered interceptor name to its
	// `interceptors.<name>.*` subtree. Populated by the loader; not a
	// key an operator writes.
	InterceptorConfig map[string]map[string]any `koanf:"-"`

	JWTSecret string        `koanf:"jwt_secret"`
	JWTExpiry time.Duration `koanf:"jwt_expiry"`
	JWTIssuer string        `koanf:"jwt_issuer"`
	// JWTAudience, when set, is stamped into every token Generate mints and
	// required of every token Validate accepts; empty leaves aud unchecked.
	JWTAudience     string        `koanf:"jwt_audience"`
	JWTKeys         []JWTKeySpec  `koanf:"jwt_keys"`
	JWTCurrentKID   string        `koanf:"jwt_current_kid"`
	SessionLifetime time.Duration `koanf:"session_lifetime"`
	SessionStore    string        `koanf:"session_store"`
	SessionRedisURL string        `koanf:"session_redis_url"`
	SessionTable    string        `koanf:"session_table"`

	// Session cookies
	//
	// SessionCookieName supports the "__Host-" / "__Secure-" cookie
	// prefixes (recommended over HTTPS): App.New validates the prefix
	// preconditions at startup — __Host- requires session_cookie_secure,
	// path "/" and no domain; __Secure- requires session_cookie_secure —
	// and fails loud instead of issuing a cookie every browser would
	// silently drop.
	SessionCookieName   string `koanf:"session_cookie_name"`
	SessionCookieDomain string `koanf:"session_cookie_domain"`
	SessionCookiePath   string `koanf:"session_cookie_path"`
	// SessionCookieSecure sets the session cookie's Secure attribute.
	// Default true (secure-by-default, SPEC §2.4): the session cookie
	// refuses to ride over plain HTTP. Local development over http:// must
	// opt out with `session_cookie_secure: false`. Mirrors the CSRF cookie
	// posture (ADR-008: Secure by default, explicit opt-out).
	SessionCookieSecure   bool          `koanf:"session_cookie_secure"`
	SessionCookieSameSite string        `koanf:"session_cookie_samesite"`
	SessionIdleTimeout    time.Duration `koanf:"session_idle_timeout"`
	// SessionMemcachedServers is the host:port list for `session_store:
	// memcached`.
	SessionMemcachedServers []string `koanf:"session_memcached_servers"`
	SessionRedisPrefix      string   `koanf:"session_redis_prefix"`

	// RBAC
	//
	// RBACPolicyFile is the path to the Casbin RBAC CSV policy file. The
	// deprecated admin_rbac_policy_file alias was removed in v0.12.0
	// (DEP-2026-004; see MA-2026-004 for the one-line rename).
	RBACPolicyFile string `koanf:"rbac_policy_file"`

	// Mail
	MailDriver string `koanf:"mail_driver"`
	SMTPHost   string `koanf:"smtp_host"`
	SMTPPort   int    `koanf:"smtp_port"`
	SMTPUser   string `koanf:"smtp_user"`
	SMTPPass   string `koanf:"smtp_pass"`
	MailFrom   string `koanf:"mail_from"`

	// MailCircuitBreaker, when Enabled, wraps mail.Sender.Send calls
	// with a pkg/circuit breaker. Healthy (the SMTP HELO probe used by
	// /healthz) bypasses the breaker so a recovering dependency can
	// still be observed while Send is short-circuited.
	MailCircuitBreaker CircuitBreakerSpec `koanf:"mail_circuit_breaker"`

	// Observability
	LogLevel     string `koanf:"log_level"`
	LogFormat    string `koanf:"log_format"`
	OTLPEndpoint string `koanf:"otlp_endpoint"`
	MetricsPath  string `koanf:"metrics_path"`

	// SQLDriverInstrumentation wraps the database/sql driver so that direct
	// db.QueryContext/ExecContext statements — the ones that bypass
	// model.CRUD (outbox dispatch, SQL session stores, migrations, schema
	// drift, and any raw SQL an app runs) — also reach the observability
	// bus's live SQL feed. Statements issued through model.CRUD are already
	// on the feed and are not double-recorded. Default false: without it the
	// feed shows only CRUD traffic (the historical behaviour) and the driver
	// is not wrapped, so there is zero hot-path cost. Enabling it adds a
	// small per-direct-statement cost; the expensive work (sanitize + emit)
	// still runs only when a subscriber is attached.
	SQLDriverInstrumentation bool `koanf:"sql_driver_instrumentation"`

	// MetricsPath is seeded into the bootstrap allow-list (ADR-004) and so
	// answers without authorization. Default true — the historical
	// behaviour, matching the common "scraper on a private network" setup.
	// Set false to put /metrics behind the default-deny RBAC enforcer:
	// grant your scraper access with a policy on the metrics path (e.g.
	// `p, metrics-scraper, /metrics, *` plus JWT auth), or keep the
	// endpoint private at the network layer instead. Note that metric
	// VALUES are operational data; if your metrics carry anything
	// sensitive, flip this off or firewall the path.
	MetricsPublic bool `koanf:"metrics_public"`

	// LogRedactExtraKeys are additional log attribute keys whose values
	// the structured logger redacts, on top of the built-in denylist
	// (observe.DefaultRedactedKeys). Use it for app-specific sensitive
	// fields. There is intentionally no config key to *disable*
	// redaction — that requires an explicit code-level opt-out via
	// observe.NewLoggerWithRedaction. See ADR-007.
	LogRedactExtraKeys []string `koanf:"log_redact_extra_keys"`

	// Security — CSRF
	//
	// CSRFEnabled mounts the router's CSRF middleware (router.WithCSRF) on
	// the default stack: origin verification via Sec-Fetch-Site with the
	// double-submit token as fallback. Default false — CSRF protection is
	// opt-in because it only makes sense for cookie/session-authenticated
	// HTML apps; a pure Bearer-token API does not need it. The mvc scaffold
	// enables it; enable it in any app that authenticates browsers with the
	// session cookie.
	CSRFEnabled bool `koanf:"csrf_enabled"`
	// CSRFExemptPaths are URL path prefixes excluded from CSRF validation
	// (e.g. "/api/" for Bearer-only subtrees, or webhook receivers that
	// authenticate by signature).
	CSRFExemptPaths []string `koanf:"csrf_exempt_paths"`

	// CSRFInsecureCookie disables the Secure attribute on the CSRF cookies —
	// a development-only opt-out mirroring session_cookie_secure: false. The
	// default (Secure) makes the double-submit flow unreachable for plain
	// HTTP non-browser clients (Go's cookiejar over http://127.0.0.1), while
	// browsers special-case localhost. Never enable it in production.
	CSRFInsecureCookie bool `koanf:"csrf_insecure_cookie"`

	// Security
	RateLimitRequests int           `koanf:"rate_limit_requests"`
	RateLimitWindow   time.Duration `koanf:"rate_limit_window"`
	RateLimitBurst    int           `koanf:"rate_limit_burst"`
	RateLimitByRoute  bool          `koanf:"rate_limit_by_route"`
	RateLimitByRole   bool          `koanf:"rate_limit_by_role"`

	// TrustedProxies is the allow-list of upstream proxy addresses (IPs or
	// CIDR ranges) whose X-Forwarded-For / X-Real-IP headers are honored. An
	// empty list (the default) DENIES all forwarding headers: r.RemoteAddr —
	// the immediate peer — is used as the client IP for logging and rate
	// limiting. This prevents header-spoofed rate-limit evasion and audit-log
	// poisoning. Set it to your load balancer / reverse-proxy addresses (e.g.
	// ["10.0.0.0/8"]) when Nucleus runs behind one.
	TrustedProxies []string `koanf:"trusted_proxies"`

	// StorageProviderConfig carries the `storage.<provider>.*` subtree of a
	// REGISTERED third-party provider. It is not part of the schema — the
	// framework cannot know the shape of a backend it has never seen — so
	// it is captured raw at load time and handed to the provider, which
	// binds it into its own typed struct with Config.BindProvider.
	//
	// Without this a registry that let you plug a backend in did not let
	// you configure it, which is one step short of useful.
	StorageProviderConfig map[string]any `koanf:"-" json:"-" yaml:"-"`

	// AuthBackendConfig carries the `auth.<backend>.*` subtree of each
	// REGISTERED authentication backend named in AuthBackends, keyed by
	// backend name. Same reason and same shape as StorageProviderConfig:
	// the framework cannot know what a directory backend needs to reach
	// its directory, so the subtree is captured raw at load time and the
	// backend binds it into its own typed struct with
	// auth.BackendConfig.Bind.
	//
	// It is a map and not one subtree because the chain is ORDERED and can
	// hold several backends at once — `[ldap, local]` configures two.
	AuthBackendConfig map[string]map[string]any `koanf:"-" json:"-" yaml:"-"`

	// CORSOrigins is the allow-list of origins permitted by the CORS
	// middleware. An empty list (the default) DENIES cross-origin requests —
	// no CORS headers are emitted (security-by-default, completed at v1.0.0
	// per ADR-013 R4 / DEP-2026-007). A non-empty list restricts CORS to
	// exactly these origins; the historical allow-all behavior is the
	// explicit opt-in `["*"]` (`Access-Control-Allow-Origin: *` for
	// credential-less requests). Key reference:
	// `docs/reference/CONFIG_KEY_REGISTRY.md`.
	CORSOrigins []string `koanf:"cors_origins"`
	// CORSAllowCredentials controls whether the CORS middleware emits
	// `Access-Control-Allow-Credentials: true`. It is only honored when
	// CORSOrigins is non-empty: per the Fetch standard, credentials cannot be
	// combined with the `*` wildcard, so the allow-all default never sets it.
	CORSAllowCredentials bool `koanf:"cors_allow_credentials"`

	// i18n
	DefaultLocale string `koanf:"default_locale"`
	LocalesPath   string `koanf:"locales_path"`

	// Static files
	StaticPrefix string `koanf:"static_prefix"`
	StaticRoot   string `koanf:"static_root"`

	// Storage (unified config; the legacy flat storage_driver/storage_path
	// keys were removed in v0.12.0 — DEP-2026-005, MA-2026-005)
	Storage StorageConfig `koanf:"storage"`

	// Outbox (transactional outbox pattern)
	// When enabled, the outbox provides reliable message delivery through
	// a SQL-backed table with support for external bridges (Kafka, webhooks, etc.)
	Outbox OutboxConfig `koanf:"outbox"`

	// Jobs — module background jobs (pkg/nucleus ModuleSpec.Jobs).
	//
	// JobsProvider selects the pkg/tasks provider that executes them:
	// "memory" (default; in-process scheduler and workers, jobs are lost on
	// restart) or "asynq" (Redis-backed, durable, requires JobsRedisURL).
	JobsProvider string `koanf:"jobs_provider"`
	// JobsRedisURL is the Redis connection URL for the asynq jobs provider
	// (e.g. "redis://localhost:6379/0"). Required when jobs_provider is
	// "asynq"; ignored by the memory provider.
	JobsRedisURL string `koanf:"jobs_redis_url"`
	// JobsConcurrency is the number of concurrent job workers. 0 uses the
	// provider default.
	JobsConcurrency int `koanf:"jobs_concurrency"`
	// JobsSchedulerLock (default true) runs the asynq scheduler under
	// leader election over a Redis lock (SET NX + TTL), so that with
	// multiple replicas exactly ONE process ticks the cron entries — each
	// replica used to start its own scheduler and every job fired once per
	// replica (NF-1). Workers run on every replica either way; only the
	// scheduler is elected. Set false to opt out (single-replica
	// deployments that prefer zero extra Redis traffic, or an external
	// scheduler of record) — the boot log then WARNs about the
	// duplication. Ignored by the memory provider (in-process by nature).
	JobsSchedulerLock bool `koanf:"jobs_scheduler_lock"`

	// WebhooksPrefix is the URL prefix under which module webhook routes
	// (pkg/nucleus ModuleSpec.Webhooks) are mounted:
	// <prefix>/<module-name><path>. Default "/webhooks". When CSRF
	// protection is enabled the framework exempts this prefix
	// automatically — webhooks authenticate by signature, not CSRF token.
	WebhooksPrefix string `koanf:"webhooks_prefix"`

	// Templates
	TemplatesDir string `koanf:"templates_dir"`

	// Environment
	Env   string `koanf:"env"`
	Debug bool   `koanf:"debug"`

	// Profile applies a named preset over the loaded configuration (DX-23).
	// Supported: "dev" — swap every backing-service selection for its
	// no-dependency counterpart (SQLite database, in-memory sessions and
	// jobs, local filesystem storage, no-op mailer) so the SAME config file
	// boots with zero external services. Empty means no preset. Unknown
	// values fail config load.
	Profile string `koanf:"profile"`

	// StateDir is the local directory under which the framework persists
	// machine-local artefacts. Default: "./.nucleus-state". Override with the
	// NUCLEUS_STATE_DIR environment variable.
	StateDir string `koanf:"state_dir"`
}

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a copy of the framework default configuration.

func LoadConfig

func LoadConfig(path ...string) (*Config, error)

LoadConfig loads configuration from multiple sources with increasing precedence: 1. Struct defaults 2. YAML file (optional — path argument or "nucleus.yml" in current directory) 3. Environment variables with prefix NUCLEUS_

If no path is provided and "nucleus.yml" does not exist, only defaults and env vars are used.

func (*Config) Addr

func (c *Config) Addr() string

Addr returns the host:port address string for the server.

func (*Config) DatabaseAliases

func (c *Config) DatabaseAliases() []string

DatabaseAliases returns configured database aliases with non-empty URLs.

func (*Config) DatabaseByAlias

func (c *Config) DatabaseByAlias(alias string) (DatabaseConfig, bool)

DatabaseByAlias returns one resolved database config.

func (*Config) DeclaredProviders

func (c *Config) DeclaredProviders() providerns.Declared

DeclaredProviders is what the validators need from this file to apply the exemption rule. Both configuration paths build it the same way, which is what stops "the same file, two verdicts" from happening a fourth time.

func (*Config) DefaultDatabase

func (c *Config) DefaultDatabase() DatabaseConfig

DefaultDatabase returns the resolved primary database config.

func (*Config) DefaultDatabaseAlias

func (c *Config) DefaultDatabaseAlias() string

DefaultDatabaseAlias returns the configured primary database alias.

func (*Config) IsDev

func (c *Config) IsDev() bool

IsDev returns true if the environment is "development".

func (*Config) IsProd

func (c *Config) IsProd() bool

IsProd returns true if the environment is "production".

func (*Config) ToStorageConfig

func (c *Config) ToStorageConfig() storage.Config

toStorageConfig converts the app Config to storage.Config. ToStorageConfig renders the storage section as the storage package's own Config, including the raw subtree of a registered third-party provider. Exported so a caller assembling storage outside app.New — a test, an embedder — gets exactly what the framework would build.

type DatabaseConfig

type DatabaseConfig struct {
	URL         string        `koanf:"url"`
	MaxOpen     int           `koanf:"max_open"`
	MaxIdle     int           `koanf:"max_idle"`
	MaxLifetime time.Duration `koanf:"max_lifetime"`
}

DatabaseConfig describes one named database connection under databases.<alias>.

type Extension

type Extension interface {
	// Name returns a human-readable identifier for this extension (e.g. "admin", "storage").
	Name() string

	// Attach initializes the extension and wires it into the App.
	//
	// It receives the fully constructed core App and may:
	//   - READ the framework services it needs (Config, Logger, Router, DB,
	//     DBs, Models, Session, JWT, Authorizer, Mailer, Storage,
	//     Observability);
	//   - mount HTTP routes and register middleware through Router;
	//   - hold on to whatever it needs for its own lifetime.
	//
	// It may NOT reassign the framework's own fields on App. That used to
	// be documented as permitted — "or set fields on App" — and it was a
	// blank cheque: whatever an extension reached for became API in
	// practice while being covered by no contract, so nothing could be
	// promised about it from one version to the next. No extension ever
	// used it (the one in-tree consumer reads five fields and writes
	// none), so the permission cost stability and bought nothing.
	//
	// What an extension may rely on is frozen in
	// contracts/baseline/extension_surface.txt: adding or removing a field
	// there is a deliberate, reviewed act, which is what makes it possible
	// to promise anything to a plugin author at all.
	Attach(a *App) error

	// Shutdown releases resources held by this extension.
	// Called in reverse registration order during App.Shutdown.
	Shutdown(ctx context.Context) error
}

Extension is the interface that subsystems implement to register themselves with the App container during initialization. Extensions are attached after the core components (config, logger, router, DB, sessions, models) are initialized but before the HTTP server starts.

The lifecycle is:

  1. app.New(cfg) initializes core components.
  2. Each Extension.Attach(a) is called in registration order.
  3. app.Run(ctx) starts the HTTP server.
  4. On shutdown, Extension.Shutdown(ctx) is called in reverse order.

type GCSStorageSpec

type GCSStorageSpec struct {
	Bucket            string                   `koanf:"bucket"`
	CredentialsSource storage.CredentialSource `koanf:"credentials"`
	PublicBucket      string                   `koanf:"public_bucket"`
}

GCSStorageSpec mirrors pkg/storage.GCSConfig.

type HealthzCheck

type HealthzCheck struct {
	Name      string `json:"name"`
	Status    string `json:"status"`
	Message   string `json:"message,omitempty"`
	LatencyMS int64  `json:"latency_ms,omitempty"`
}

HealthzCheck is one entry in the /healthz response, reporting on a single dependency. `status` is `healthy` or `unhealthy`; degraded states are promoted to `unhealthy` so external probes (Kubernetes, ELB) treat the response uniformly.

type HealthzResponse

type HealthzResponse struct {
	Status    string         `json:"status"`
	CheckedAt string         `json:"checked_at"`
	Checks    []HealthzCheck `json:"checks"`
}

HealthzResponse is the JSON body returned by /healthz.

type JWTKeySpec

type JWTKeySpec struct {
	KID       string `koanf:"kid"`
	Algorithm string `koanf:"algorithm"`
	SecretEnv string `koanf:"secret_env"`
	PemPath   string `koanf:"pem_path"`
	PemEnv    string `koanf:"pem_env"`
}

JWTKeySpec describes one key in the JWT keyset constructed by App.New. Operators populate this slice via `auth.jwt_keys` in nucleus.yml and nominate the current signing key via `auth.jwt_current_kid`. The key material itself follows the `CredentialSource` pattern already used by pkg/storage: PEM files and secrets stay out of tracked YAML and load from `*_env` / `*_path` references.

Supported algorithms and their material fields:

  • HS256 — set `SecretEnv` (the named environment variable holds the shared HMAC secret).
  • RS256 — set exactly one of `PemPath` / `PemEnv` (RSA private key, PKCS#1 or PKCS#8 PEM).
  • ES256 — set exactly one of `PemPath` / `PemEnv` (ECDSA P-256 private key, SEC1 or PKCS#8 PEM). Only the P-256 curve is accepted; see ADR-005.

`SecretEnv` reads the named environment variable; `PemPath` reads a file from disk; `PemEnv` reads PEM bytes from an environment variable (suitable for Kubernetes secrets mounted as env vars).

type LocalStorageSpec

type LocalStorageSpec struct {
	Path string `koanf:"path"`
}

LocalStorageSpec mirrors pkg/storage.LocalConfig.

type MultiSiteConfig

type MultiSiteConfig struct {
	Enabled     bool                  `koanf:"enabled"`
	DefaultSite string                `koanf:"default_site"`
	Sites       map[string]SiteConfig `koanf:"sites"`
}

MultiSiteConfig describes host-based site resolution.

type MultiTenantConfig

type MultiTenantConfig struct {
	Enabled               bool                    `koanf:"enabled"`
	Resolver              string                  `koanf:"resolver"` // subdomain|header
	Header                string                  `koanf:"header"`
	DefaultTenant         string                  `koanf:"default_tenant"`
	RequireIsolatedDB     bool                    `koanf:"require_isolated_db"`
	DatabaseAliasTemplate string                  `koanf:"database_alias_template"`
	Tenants               map[string]TenantConfig `koanf:"tenants"`

	// RequireTenantStorage makes storage operations FAIL when the context
	// carries no tenant, instead of silently degrading to the shared
	// (unprefixed) key space — the trap a background job without request
	// scope falls into (NF-12). Off by default for compatibility: without
	// it, a multi-tenant application still gets a one-shot WARN the first
	// time an operation degrades.
	RequireTenantStorage bool `koanf:"require_tenant_storage"`
}

MultiTenantConfig describes tenant resolution and tenant->database mapping.

type OpError

type OpError struct {
	Op  string
	Err error
}

OpError wraps an application error with operation context while preserving unwrapping semantics for errors.Is / errors.As.

func (*OpError) Error

func (e *OpError) Error() string

func (*OpError) Unwrap

func (e *OpError) Unwrap() error

type Option

type Option func(*appOptions)

Option configures the App during construction via app.New(cfg, opts...).

func WithExtensions

func WithExtensions(exts ...Extension) Option

func WithOpenAuthz

func WithOpenAuthz() Option

WithOpenAuthz disables the default-deny RBAC middleware mounted by App.New (see ADR-004). It switches off authorization ONLY: authentication still runs — a configured JWT manager decodes bearers in open mode too, so handlers and request interceptors keep seeing the caller's identity via auth.ClaimsFromContext (QCD-FW-25). Use only for early development, internal tooling, or demos where unauthenticated access is acceptable. The option emits a startup WARN log so the choice is visible in operational telemetry. There is no `Config.OpenAuthz` config key on purpose — opting out requires touching code and surfaces in PR review.

func WithTemplateFuncs

func WithTemplateFuncs(funcs template.FuncMap) Option

WithTemplateFuncs registers template functions available to every template app.New parses from templates_dir (QCD-FW-9). Order of operations at startup: registered functions → recursive parse of templates_dir → SetHTMLTemplates on the router. Without this, every piece of presentation logic (date formats, percentages, pagination URLs) has to be precomputed in Go and passed through the data map. Repeated options merge; a later registration of the same name wins.

a, err := app.New(cfg, app.WithTemplateFuncs(template.FuncMap{
    "fecha": func(t time.Time) string { return t.Format("02/01/2006") },
}))

func WithTemplates

func WithTemplates(base *template.Template) Option

WithTemplates injects a prebuilt *template.Template as the BASE the startup loader parses templates_dir into (QCD-FW-9): templates and {{define}} blocks already present on the base stay available, and files from templates_dir are added on top under their relative-path names — enabling wrapping layouts and programmatic templates. Functions from WithTemplateFuncs are applied to the base before parsing. When templates_dir has no templates, the base itself (if it has any parsed templates) is still wired into the router.

func WithTemplatesFS

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

WithTemplatesFS parses every `.html` file of an fs.FS into the template namespace at startup, each registered under `<prefix>/<path>` (or its bare slash-path when prefix is empty) — the fs.FS counterpart of templates_dir, for templates embedded in the binary (`embed.FS`).

Unlike WithTemplates (which replaces the base), WithTemplatesFS ACCUMULATES: each call adds a source, applied in registration order. Load order on a name collision is last-parse-wins: the WithTemplates base first, then every FS source, then templates_dir — so the host's on-disk files always override an embedded source's. Functions from WithTemplateFuncs are applied before any parse, so FS templates see them. `nucleus.Run` feeds each mounted module's `Module.Templates` through this option under the module's name.

func WithUserProvider

func WithUserProvider(provider auth.UserProvider) Option

WithExtensions registers one or more extensions to be attached during app.New().

Example:

a, err := app.New(cfg,
    app.WithExtensions(
        admin.Extension(),
        storage.Extension(storageCfg),
    ),
)

WithUserProvider registers the application's own user table as an authentication backend, under the name "local" unless overridden.

It is what finally connects auth.UserProvider — the interface that has described how to reach your users since v0.x, frozen in the contract and called by nothing — to the login path. An application that authenticates only against a directory simply does not call this.

func WithUserProviderNamed

func WithUserProviderNamed(name string, provider auth.UserProvider) Option

WithUserProviderNamed is WithUserProvider with an explicit backend name, for an application that wants its table to appear in the chain as something other than "local".

func WithoutDefaults

func WithoutDefaults() Option

WithoutDefaults disables automatic initialization of the default extensions (admin, storage, mail, authz). When used, only the core components are initialized and the caller must explicitly register desired extensions via WithExtensions.

This is useful for lightweight API services that don't need the admin panel, file storage, or RBAC enforcement.

type OutboxConfig

type OutboxConfig struct {
	Enabled       bool           `koanf:"enabled"`
	TableName     string         `koanf:"table_name"`
	LeaseDuration time.Duration  `koanf:"lease_duration"`
	MaxRetries    int            `koanf:"max_retries"`
	RetryBackoff  time.Duration  `koanf:"retry_backoff"`
	Bridges       []BridgeConfig `koanf:"bridges"`

	// LeaseOwner identifies THIS instance in the outbox lease rows
	// (QCD-FW-5). Empty (the default) derives a per-instance identifier
	// from the hostname and pid — every process used to share the literal
	// "nucleus-app", which made lease rows untraceable and let co-tenant
	// processes lease messages interchangeably. Set it explicitly for a
	// stable identity (e.g. a k8s pod name).
	LeaseOwner string `koanf:"lease_owner"`

	// MissingRoutePolicy controls what a dispatcher does with a leased
	// message whose topic has no registered bridge (QCD-FW-5):
	// "error" (default) fails the message; "ignore" releases it for the
	// instance that can deliver it — required in a deliberately
	// heterogeneous fleet where not every process registers every bridge.
	MissingRoutePolicy string `koanf:"missing_route_policy"`
}

OutboxConfig configures the transactional outbox pattern for reliable message delivery.

type RequestScope

type RequestScope struct {
	Host          string
	Site          string
	Tenant        string
	DatabaseAlias string
}

RequestScope captures site/tenant/database routing decisions for one request.

func RequestScopeFromContext

func RequestScopeFromContext(ctx context.Context) (RequestScope, bool)

RequestScopeFromContext returns the resolved request scope when available.

type S3StorageSpec

type S3StorageSpec struct {
	Endpoint        string                   `koanf:"endpoint"`
	Bucket          string                   `koanf:"bucket"`
	Region          string                   `koanf:"region"`
	AccessKeyID     storage.CredentialSource `koanf:"access_key_id"`
	SecretAccessKey storage.CredentialSource `koanf:"secret_access_key"`
	SessionToken    storage.CredentialSource `koanf:"session_token"`
	UsePathStyle    bool                     `koanf:"use_path_style"`
	PublicBucket    string                   `koanf:"public_bucket"`
	// CreateBucketIfMissing provisions the bucket(s) at startup when they
	// do not exist yet (QCD-FW-2). Opt-in; without it a missing bucket
	// still fails app.New loudly.
	CreateBucketIfMissing bool `koanf:"create_bucket_if_missing"`
}

S3StorageSpec mirrors pkg/storage.S3Config key-for-key and TYPE-for-type (TestStorageConfigMirrorParity walks both recursively). Credentials are storage.CredentialSource — `access_key_id: "literal"` still binds (the config decoder promotes a plain string to {value: …}), and the `env_var`/`file`/`secret_manager` shapes the README promises are now actually loadable. QCD-FW-4's lesson: a mirror field missing or mis-typed here makes a documented key silently unreachable.

type SiteConfig

type SiteConfig struct {
	Hosts                       []string `koanf:"hosts"`
	Database                    string   `koanf:"database"`
	TenantDatabaseAliasTemplate string   `koanf:"tenant_database_alias_template"`
}

SiteConfig maps host patterns to a logical site and default DB alias. Host patterns support exact hosts and wildcard prefix patterns (*.example.com).

type StorageConfig

type StorageConfig struct {
	// Default visibility for new objects (private|public).
	DefaultVisibility string `koanf:"default"`

	// Provider selects the storage backend (s3|gcs|azure|local).
	Provider string `koanf:"provider"`

	// PublicPaths maps public URL paths to storage key prefixes.
	PublicPaths map[string]string `koanf:"public_paths"`

	// PublicURLBase is the base URL for public objects (CDN or direct provider).
	PublicURLBase string `koanf:"public_url_base"`

	// S3 configuration
	S3 S3StorageSpec `koanf:"s3"`

	// GCS configuration
	GCS GCSStorageSpec `koanf:"gcs"`

	// Azure configuration
	Azure AzureStorageSpec `koanf:"azure"`

	// Local configuration (development only)
	Local LocalStorageSpec `koanf:"local"`

	// Cleanup config
	Cleanup CleanupStorageSpec `koanf:"cleanup"`

	// CircuitBreaker, when Enabled, wraps remote storage operations
	// (Put/Get/Delete/Exists/List/SignedURL/Copy) with a pkg/circuit
	// breaker. The local provider is never wrapped. PublicURL is
	// pass-through (pure string composition).
	CircuitBreaker CircuitBreakerSpec `koanf:"circuit_breaker"`
}

StorageConfig is the unified storage configuration.

type TenantConfig

type TenantConfig struct {
	Site     string `koanf:"site"`
	Database string `koanf:"database"`
}

TenantConfig allows explicit site and database alias assignment for one tenant id.

Jump to

Keyboard shortcuts

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