framework

package
v0.22.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 62 Imported by: 0

Documentation

Overview

Package framework is the public surface of the GoFastr framework.

It contains the App spine — App, Plugin, Registry, lifecycle, typed hooks, the in-memory test harness — plus thin re-exports of every subpackage's public API so callers can keep writing framework.Entity, framework.NewCrudHandler, framework.AutoMigrate, etc.

The actual implementations live in subpackages:

  • framework/entity Entity model, columns, relations, validators
  • framework/crud HTTP CRUD handler, eager loading, includes, typed query, MCP tool generator
  • framework/hook HookRegistry + lifecycle constants
  • framework/event EventBus + Event types
  • framework/migrate AutoMigrate + DiffSchema + Dialect detection
  • framework/openapi EntityOpenAPI spec generator
  • framework/dsl ?dsl= query parser
  • framework/filter query-string filter & sort parsing
  • framework/pagination cursor + offset paging
  • framework/tenant multi-tenancy (TenantConfig, TenantMiddleware)
  • framework/softdelete soft-delete helpers
  • framework/access RBAC (Permission, Policy, RolePolicy)
  • framework/file FileField upload helpers
  • framework/cron in-process cron scheduler
  • framework/slowquery SlowQueryLogger DBExecutor wrapper
  • framework/db shared Executor + tx context primitives

Both surfaces are first-class: the facade re-exports give callers short, one-import access (`framework.Entity`, `framework.AutoMigrate`), while the narrow subpackages give plugin authors and codegen tools a precise dependency graph.

See framework/ARCHITECTURE.md for the layering rules, cycle-breaking interfaces, and the recipe for extracting a new subpackage.

Index

Constants

View Source
const (
	CaseCamel          = crud.CaseCamel
	CaseSnake          = crud.CaseSnake
	MaxBatchSize       = crud.MaxBatchSize
	MaxMultipartMemory = crud.MaxMultipartMemory
)
View Source
const (
	RelHasOne     = entity.RelHasOne
	RelHasMany    = entity.RelHasMany
	RelManyToOne  = entity.RelManyToOne
	RelManyToMany = entity.RelManyToMany
)
View Source
const (
	EntityCreated = event.EntityCreated
	EntityUpdated = event.EntityUpdated
	EntityDeleted = event.EntityDeleted
)
View Source
const (
	BeforeCreate = hook.BeforeCreate
	AfterCreate  = hook.AfterCreate
	BeforeUpdate = hook.BeforeUpdate
	AfterUpdate  = hook.AfterUpdate
	BeforeDelete = hook.BeforeDelete
	AfterDelete  = hook.AfterDelete
	BeforeList   = hook.BeforeList
	AfterList    = hook.AfterList
	BeforeGet    = hook.BeforeGet
	AfterGet     = hook.AfterGet
)
View Source
const (
	DialectPostgres = migrate.DialectPostgres
	DialectSQLite   = migrate.DialectSQLite
)

Variables

View Source
var (
	NewRolePolicy     = access.NewRolePolicy
	NewGrantStore     = access.NewGrantStore
	RequirePermission = access.RequirePermission
	GetPermissions    = access.GetPermissions
	WithPolicy        = access.WithPolicy
	WithRoles         = access.WithRoles
	// GetRoles reads the roles installed via WithRoles back out of the
	// request context — the reader half of the role-context seam, for
	// role-based UI branching.
	GetRoles = access.GetRoles
	// Can reports whether the request context carries a permission.
	Can = access.Can
	// AccessMiddleware installs the RBAC policy + roles into request context
	// so RequirePermission and EntityConfig.Access gates can resolve.
	AccessMiddleware = access.Middleware
)
View Source
var (
	NewCrudHandler         = crud.NewCrudHandler
	RegisterCrudRoutes     = crud.RegisterCrudRoutes
	RegisterCrudRoutesFunc = crud.RegisterCrudRoutesFunc
	MarshalEntity          = crud.MarshalEntity
	UnmarshalEntity        = crud.UnmarshalEntity
	IsNotFound             = crud.IsNotFound
	EagerLoad              = crud.EagerLoad
	RegisterEntityMCPTools = crud.RegisterEntityMCPTools
	WithServerWrites       = crud.WithServerWrites
	NewValidationError     = crud.NewValidationError
)
View Source
var (
	Define                 = entity.Define
	HasOne                 = entity.HasOne
	HasMany                = entity.HasMany
	BelongsTo              = entity.BelongsTo
	ManyToMany             = entity.ManyToMany
	NewStringColumn        = entity.NewStringColumn
	NewIntColumn           = entity.NewIntColumn
	NewFloatColumn         = entity.NewFloatColumn
	NewBoolColumn          = entity.NewBoolColumn
	NewTimestampColumn     = entity.NewTimestampColumn
	NewUUIDColumn          = entity.NewUUIDColumn
	NewValidationRegistry  = entity.NewValidationRegistry
	Required               = entity.Required
	Unique                 = entity.Unique
	Custom                 = entity.Custom
	FormatValidationErrors = entity.FormatValidationErrors
	And                    = entity.And
	Or                     = entity.Or
	Not                    = entity.Not
)
View Source
var (
	AutoMigrate                = migrate.AutoMigrate
	AutoMigrateContext         = migrate.AutoMigrateContext
	AutoMigratePlanContext     = migrate.AutoMigratePlanContext
	MigrateEntity              = migrate.MigrateEntity
	MigrateEntityDialect       = migrate.MigrateEntityDialect
	DiffSchema                 = migrate.DiffSchema
	ApplySchemaDiff            = migrate.ApplySchemaDiff
	ApplySchemaDiffWithOptions = migrate.ApplySchemaDiffWithOptions
	DetectDialect              = migrate.DetectDialect
	GenerateMigration          = migrate.GenerateMigration
	GeneratePlan               = migrate.GeneratePlan
	SnapshotFromRegistry       = migrate.SnapshotFromRegistry
	SnapshotFromPlan           = migrate.SnapshotFromPlan
	RenderMigrationFile        = migrate.RenderMigrationFile
	LoadSnapshot               = migrate.LoadSnapshot
	SaveSnapshot               = migrate.SaveSnapshot
)
View Source
var (
	NewMetrics        = middleware.NewMetrics
	MetricsMiddleware = middleware.MetricsMiddleware
	MetricsHandler    = middleware.MetricsHandler
	Tracing           = middleware.Tracing
)
View Source
var (
	DefaultTenantConfig = tenant.DefaultTenantConfig
	WithMultiTenant     = tenant.WithMultiTenant
	ApplyTenantFilter   = tenant.ApplyTenantFilter
	TenantMiddleware    = tenant.TenantMiddleware
	SetTenantID         = tenant.SetTenantID
	GetTenantID         = tenant.GetTenantID
	InjectTenantID      = tenant.InjectTenantID
)
View Source
var EntityOpenAPI = openapi.EntityOpenAPI
View Source
var NewEventBus = event.NewEventBus
View Source
var NewHookRegistry = hook.NewHookRegistry
View Source
var NewScheduler = cron.NewScheduler

Functions

func AppendAuditEvent added in v0.14.0

func AppendAuditEvent(ctx context.Context, db *sql.DB, table, entity, op, recordID, actorID string, diff map[string]any) error

AppendAuditEvent appends a non-CRUD audit row (e.g. an auth security event) to the audit table. It reuses the same row writer and the same control-byte sanitisation as the CRUD audit hooks, so a custom sink and the lifecycle hooks cannot drift apart. diff may be nil; an empty map is treated as nil so a no-detail event writes a NULL diff column rather than "{}".

Unlike the CRUD hooks this is NOT transactional by default: it writes through the plain pool unless a transaction is already on ctx (resolved via TxFromContext), because security events (login, 2FA, password reset) are not part of an entity write. The caller owns atomicity if it needs any.

func DBFromContext added in v0.3.2

func DBFromContext(ctx context.Context) (*sql.DB, bool)

DBFromContext returns the *sql.DB stamped onto ctx by an App with a DB configured. The second return value is false when no DB is present — e.g. a UI-only app, or a context that never passed through the App's request chain.

This is the package-portable alternative to the package-level handle idiom: a shared screen can call framework.DBFromContext(ctx) instead of closing over a global *sql.DB captured at main() time.

func DefaultMiddleware

func DefaultMiddleware(a *App) []router.Middleware

DefaultMiddleware is the framework's standard safety chain in canonical order:

recovery → request-id → [idempotency] → [i18n] → security headers → timeout

The optional entries are present when the App was configured with WithIdempotency / WithI18n; the timeout entry is omitted when AppConfig.DisableRequestTimeout is true.

Access logging is deliberately NOT in this list. battery/log owns structured access logging when registered, and ad-hoc apps that just want a basic line can add middleware.LoggingFn(app.Logger) themselves — having both fire produces duplicate entries with mismatched fields (`request` from the framework, `http.access` from the plugin).

Takes the App so the recovery middleware can route panics through app.Logger (late-binding) and the timeout reflects AppConfig.RequestTimeout. Pass nil only in tests; the recovery falls back to slog.Default and the timeout to 30s.

func EnsureAuditTable

func EnsureAuditTable(db *sql.DB, table string) error

EnsureAuditTable creates the audit_log table if it does not exist. Idempotent. Dialect-aware via the existing migrate.DetectDialect helper.

func GetAs

func GetAs[T any](bm *BatteryManager, name string) (T, error)

GetAs retrieves a battery by name and type-asserts it to T. Returns an error if the battery is not found or doesn't implement T.

func NewTypedQuery

func NewTypedQuery[T any](h *crud.CrudHandler) *crud.TypedQuery[T]

func OnAfterCreate

func OnAfterCreate[T any](app *App, name string, fn func(ctx context.Context, value *T) error)

OnAfterCreate registers a typed AfterCreate hook. The callback receives the just-inserted row (already includes server-generated fields like id). Mutations are not reflected — Create has already committed the row's shape; modifying the struct is harmless but pointless.

func OnAfterDelete

func OnAfterDelete(app *App, name string, fn func(ctx context.Context, id string) error)

OnAfterDelete registers a typed AfterDelete hook. Same shape as OnBeforeDelete.

func OnAfterGet

func OnAfterGet(app *App, name string, fn func(ctx context.Context, p *hook.GetPayload) error)

OnAfterGet registers a typed AfterGet hook. The callback receives *hook.GetPayload with Result populated — mutate the map in place to redact fields before the response is serialised.

func OnAfterList

func OnAfterList(app *App, name string, fn func(ctx context.Context, p *hook.ListPayload) error)

OnAfterList registers a typed AfterList hook. The callback receives the *hook.ListPayload with Results populated — mutate the slice in place to redact / drop rows.

func OnAfterUpdate

func OnAfterUpdate[T any](app *App, name string, fn func(ctx context.Context, value *T) error)

OnAfterUpdate registers a typed AfterUpdate hook receiving the post-update row.

func OnBeforeCreate

func OnBeforeCreate[T any](app *App, name string, fn func(ctx context.Context, value *T) error)

OnBeforeCreate registers a typed BeforeCreate hook on the entity named `name`. Mutations the callback makes to *T are reflected back into the pending body so the subsequent INSERT picks them up.

func OnBeforeDelete

func OnBeforeDelete(app *App, name string, fn func(ctx context.Context, id string) error)

OnBeforeDelete registers a typed BeforeDelete hook. The payload is the record id; no generic parameter needed.

func OnBeforeGet

func OnBeforeGet(app *App, name string, fn func(ctx context.Context, p *hook.GetPayload) error)

OnBeforeGet registers a typed BeforeGet hook. The callback receives *hook.GetPayload; ID is the request's path-value and AddWhere lets the host scope the lookup (mismatch → 404).

func OnBeforeList

func OnBeforeList(app *App, name string, fn func(ctx context.Context, p *hook.ListPayload) error)

OnBeforeList registers a typed BeforeList hook. The callback receives the framework's *hook.ListPayload directly so callers can append WHERE clauses (p.AddWhere) without type-asserting from any. Symmetric with OnBeforeCreate/OnBeforeUpdate.

func OnBeforeUpdate

func OnBeforeUpdate[T any](app *App, name string, fn func(ctx context.Context, value *T) error)

OnBeforeUpdate registers a typed BeforeUpdate hook. *T is sparse — it holds whatever the caller sent, not the full row.

func PluginGetAs added in v0.3.2

func PluginGetAs[T any](pm *PluginManager, name string) (T, error)

PluginGetAs retrieves a plugin by name and type-asserts it to T. Returns an error if the plugin is not found or doesn't implement T.

The plugin-side mirror of GetAs (the battery variant): use it to reach a plugin's concrete type or an optional interface it satisfies without hand-writing the lookup-and-assert boilerplate.

logp, err := framework.PluginGetAs[*logplugin.Plugin](app.Plugins, "log")

func TxFromContext

func TxFromContext(ctx context.Context) (*sql.Tx, bool)

TxFromContext returns the active *sql.Tx from context when a CRUD handler has wrapped the operation in a transaction. Re-exports framework/db.TxFromContext for callers (typed hooks etc.) that import framework directly.

func WithDBContext added in v0.3.2

func WithDBContext(ctx context.Context, db *sql.DB) context.Context

WithDBContext returns a derived context carrying db. The App stamps this onto every request context (see App.DBContextMiddleware) so a screen's RenderCtx(ctx) / Load(ctx) can reach the same *sql.DB the framework holds without a package-level global handle.

Types

type ACPConfig added in v0.10.0

type ACPConfig struct {
	ProtocolVersion      string           // protocol.version (protocol.name is fixed "acp")
	APIBaseURL           string           // api_base_url
	Transports           []string         // supported transports
	CapabilitiesServices []map[string]any // capabilities.services
}

ACPConfig configures /.well-known/acp.json (agenticcommerce.dev).

type AccessControl

type AccessControl = entity.AccessControl

type AccessDeclaration added in v0.5.0

type AccessDeclaration = entity.AccessDeclaration

type AgentAuthBlock added in v0.10.0

type AgentAuthBlock struct {
	// Skill is the URL of the /auth.md manifest. Defaults to <base>/auth.md.
	Skill string
	// IdentityEndpoint registers/looks up an agent identity.
	IdentityEndpoint string
	// ClaimEndpoint claims a pending identity.
	ClaimEndpoint string
	// EventsEndpoint receives async notifications (e.g. revocation).
	EventsEndpoint string
	// IdentityTypesSupported defaults to ["anonymous","identity_assertion","service_auth"].
	IdentityTypesSupported []string
}

AgentAuthBlock is the agent_auth object advertised in the OAuth authorization-server metadata (WorkOS agentic-registration profile).

type AgentSkillEntry added in v0.10.0

type AgentSkillEntry struct {
	Name        string `json:"name"`
	Type        string `json:"type"` // "skill-md" (default) or "archive"
	Description string `json:"description"`
	URL         string `json:"url"`
	Digest      string `json:"digest"` // "sha256:<hex>" of the artifact at URL
}

AgentSkillEntry is one skill in the /.well-known/agent-skills/index.json. Mirrors the v0.2.0 discovery schema.

type App

type App struct {
	Registry *Registry

	MCP     *mcp.Server
	DB      *sql.DB
	Config  AppConfig
	Plugins *PluginManager
	Storage upload.Storage // optional; enables multipart on Image/File fields

	Batteries *BatteryManager
	// contains filtered or unexported fields
}

App is the top-level application container. It wires together the entity registry, router, MCP server, and database.

func NewApp

func NewApp(opts ...AppOption) *App

NewApp creates a new App with the given options. It initializes default Registry, Router, and MCP Server if not provided.

func NewUIHostApp

func NewUIHostApp(host Mountable, opts ...AppOption) *App

NewUIHostApp builds an App and mounts the given host on it in one call — the near-universal shape for SSR/UIHost apps, which otherwise repeat

app := framework.NewApp(opts...)
app.Mount(host)

host is any Mountable (typically a *uihost.Host). Returns the App for fluent chaining.

func (*App) AddCron

func (a *App) AddCron(s *cron.Scheduler) *App

AddCron registers a Scheduler with the app's lifecycle: it starts when Start runs and stops when Stop runs. Returns the App for chaining so users can wire several schedulers in one expression.

The stop side drains through StopContext so in-flight job goroutines are joined before shutdown proceeds — bounded by the drain deadline, so a job that ignores its (already-cancelled) context can't hang SIGTERM forever.

Worker-scoped: under RoleServe this is a no-op — neither the start hook nor the drainer is registered, so a serve-only shutdown never waits on a scheduler that was never started.

func (*App) AddQueue

func (a *App) AddQueue(q schedulerStartStop) *App

AddQueue registers any queue/worker that exposes Start(ctx) and Close(). The DBQueue from battery/queue satisfies this directly; in-memory and Redis variants can be wrapped.

Worker-scoped: under RoleServe this is a no-op — neither the start hook nor the Close hook is registered, so a serve-only shutdown never closes a queue it never started.

func (*App) CrudHandler

func (a *App) CrudHandler(name string) (*crud.CrudHandler, error)

CrudHandler returns a fully-wired in-process CRUD handler for a registered entity — the same handler shape the HTTP routes use (hooks, events, storage, JSON casing, registry). Use it to call CreateOne/UpdateOne/DeleteOne/ListAll directly, e.g. to compose several writes inside App.InTx (pass the InTx ctx so they join the same transaction). Returns an error if no entity is registered under name or the app has no DB.

func (*App) DBContextMiddleware added in v0.3.2

func (a *App) DBContextMiddleware() router.Middleware

DBContextMiddleware returns middleware that stamps the App's *sql.DB onto every request context so downstream handlers and screens can retrieve it via DBFromContext. When the App has no DB, the returned middleware is a pass-through.

The App installs this automatically as part of the default middleware chain when a DB is configured, so most apps never call this directly. It's exposed for apps that opt out of the default chain (WithoutDefaultMiddleware) and want to wire DB-in-context into their own chain.

func (*App) Entity

func (a *App) Entity(name string, config entity.EntityConfig) *App

Entity registers an entity with the given name and configuration. Returns the App for fluent chaining. Panics on any misconfiguration — convenient for static, hand-written declarations where a bad config is a programming error you want to fail fast on. For generated or untrusted configs (e.g. an AI-authored field, a dynamic schema) where one bad entity should not crash the process, use TryEntity, which returns the error.

func (*App) Events

func (a *App) Events() *event.EventBus

Events returns the application's event bus.

func (*App) ExportData added in v0.20.0

func (a *App) ExportData(ctx context.Context, dir string, opts ...ExportOption) error

ExportData dumps every entity's rows plus every registered battery table to a portable archive under dir: one <name>.ndjson per source plus a manifest.json. Reads are raw (all physical columns, all rows including soft-deleted), paged by primary-key keyset. See the package comment for the fidelity and SQL-safety contracts.

func (*App) ExportStatic added in v0.8.0

func (a *App) ExportStatic(ctx context.Context, dir, basePath string) error

ExportStatic renders the app to a folder of static HTML + assets using the SSG Builder (framework/static). It locates the mounted UIHost and renders every declared route to a directory-style file, plus all /__gofastr assets the runtime needs to boot — runtime.js, the split runtime modules (themeswitch, shortcut, copy, widgets, …), app.css, and per-component CSS — with query-free filenames so the output serves correctly from any static host (GitHub Pages, S3, …).

basePath is the URL subpath the site is served under (e.g. "/gofastr" for a GitHub Pages project site at https://user.github.io/gofastr/); pass "" for an apex deploy. When set, the builder prefixes every root-absolute asset/nav URL in the HTML and bakes the prefix into runtime.js so dynamically-loaded modules resolve under the mount path.

This is the native replacement for the wget mirror the Pages deploy used to ship: declaration-driven (no crawling), and it dumps the split modules the crawl baked a "?v=" into and 404'd.

Returns an error if no uihost.UIHost is mounted.

func (*App) Flags

func (a *App) Flags() *featureflag.Evaluator

Flags returns the app's feature-flag evaluator, creating one on first call. The default backing store is in-memory; for clustered deployments call SetFlagStore with a Redis- or DB-backed implementation BEFORE any caller invokes Flags — once the lazy default fires, subsequent SetFlagStore calls panic to avoid the silent race where some goroutines still hold a reference to the previous evaluator.

The evaluator is also installed as featureflag.Default() so package- level featureflag.Bool(ctx, "...") calls work from anywhere in the app.

func (*App) Group

func (a *App) Group(prefix string, opts ...routegroup.GroupOption) *routegroup.RouteGroup

Group creates a route group with the given prefix and optional configuration. The group supports its own middleware stack, access policy, OpenAPI tags, and MCP namespacing. Nested groups compose prefixes and middleware.

api := app.Group("/api")
api.Use(authMiddleware)
api.Get("/health", healthHandler)

admin := app.Group("/admin", routegroup.WithAccess(access.RequirePermission("admin:access")))
admin.Entity("settings", settingsConfig)

func (*App) GroupEntity

func (a *App) GroupEntity(g *routegroup.RouteGroup, name string, config entity.EntityConfig) *App

GroupEntity registers an entity with the given configuration inside a RouteGroup. CRUD routes mount at <group-prefix>/<entity-table>, MCP tools are namespaced under the group's MCPNamespace, and the OpenAPI tag reflects the group's OpenAPITag if set.

This is the group-scoped equivalent of App.Entity.

func (*App) HookRegistry

func (a *App) HookRegistry(entityName string) *hook.HookRegistry

HookRegistry returns (or creates) the hook registry for a named entity.

func (*App) ImportData added in v0.20.0

func (a *App) ImportData(ctx context.Context, dir string) error

ImportData restores an archive written by ExportData into the live database. It validates the WHOLE archive before writing a single row (missing manifest, unknown source, incompatible columns, checksum mismatch are all rejected up front), then writes every source inside a single transaction — rolling back on any error. Original ids/timestamps/owner/tenant and soft-deleted rows are preserved verbatim.

func (*App) InTx

func (a *App) InTx(ctx context.Context, fn func(ctx context.Context, tx *sql.Tx) error) error

InTx runs fn inside a database transaction opened on the App's DB. The inner context carries the *sql.Tx so any code path that calls TxFromContext (typed hooks, the various do* helpers, generated repo methods invoked via WithTx) participates atomically.

Convenience wrapper for callers that aren't already inside a CRUD hook — e.g. seeders, batch jobs, multi-entity write paths that need an explicit boundary. If fn returns an error, the tx rolls back and that error is returned unchanged.

func (*App) InitPlugins

func (a *App) InitPlugins() error

InitPlugins initializes all registered plugins and batteries by calling their Init(app) method. Plugins go first (registration order), then batteries (dependency-resolved order). Each module does everything it needs from inside Init — register routes, add middleware, register MCP tools, attach hooks, swap the logger, etc.

Idempotent: the first successful call latches an internal flag so any later call returns nil without re-running plugin Inits. This lets tests call InitPlugins() manually pre-Start without colliding with the implicit call inside Start.

func (*App) IsEnabled

func (a *App) IsEnabled(ctx context.Context, key string) bool

IsEnabled is a convenience wrapper that calls Flags().Bool with the supplied context. Use it from handler code that doesn't otherwise need a direct reference to the evaluator.

The context is expected to already carry an EvalContext (via featureflag.WithContext) when user/tenant gating matters; without one, the call still works but only the kill-switch (Flag.Enabled) and uniform rollout are consulted.

func (*App) JSONCasing

func (a *App) JSONCasing() crud.JSONCase

JSONCasing returns the configured JSON casing strategy. Defaults to CaseCamel if not explicitly set.

func (*App) Lifecycle

func (a *App) Lifecycle() *lifecycle.Lifecycle

Lifecycle returns the App's graceful-shutdown coordinator. Batteries and plugins use Lifecycle().RegisterDrainer / RegisterHealthChecker to participate in Shutdown beyond the simple OnStop hook.

func (*App) Logger

func (a *App) Logger() *slog.Logger

Logger returns the App-local *slog.Logger. Middleware and plugins should call this — not slog.Default() — so that a logging plugin can replace the destination without rewiring globals.

Always non-nil. NewApp seeds the App with a JSON-to-stderr logger that is independent of slog.Default; an unrelated slog.SetDefault elsewhere in the process does not redirect this App's logs.

func (*App) Modules added in v0.17.0

func (a *App) Modules() *ModuleManager

Modules returns the app's module manager, through which callers can list modules, query enabled state, and toggle modules at runtime.

func (*App) Mount

func (a *App) Mount(m Mountable) *App

Mount attaches a Mountable and registers its routes on the app's router immediately. The default middleware chain is already in place (committed during NewApp), so any handler the Mountable registers is wrapped with it. Returns the app for fluent chaining.

Mountables typically register a NotFound catch-all (e.g. a UI host that renders pages for any unrouted path), so call Mount AFTER any explicit routes you want to take precedence (entity CRUD, custom endpoints).

IMPORTANT — ordering with plugins/batteries: plugin.Init runs at App.Start (or InitPlugins), AFTER Mount has already registered the Mountable's routes. If a plugin's Init registers a more-specific route that overlaps with a Mountable's NotFound catch-all, the plugin's route still wins (ServeMux dispatches by specificity, not by registration order). But if a plugin registers a Mountable-style catch-all itself, it shadows any user routes added after Mount but before InitPlugins. Mount last unless you know what you're doing.

func (*App) Mountables

func (a *App) Mountables() []Mountable

Mountables returns the Mountables registered via Mount, in registration order. Batteries use it to discover a mounted UI host (type-asserting to *uihost.Host) so they can register screens on the host's render pipeline rather than spinning up a second host. Returns a copy — callers must not mutate the App's internal slice.

func (*App) MustCrudHandler

func (a *App) MustCrudHandler(name string) *crud.CrudHandler

MustCrudHandler is CrudHandler that panics on error — for app setup where a missing entity is a programming mistake.

func (*App) OnReady added in v0.5.0

func (a *App) OnReady(fn func(addr string)) *App

OnReady registers a function to run once the HTTP listener has bound successfully, just before the server begins accepting connections. The addr passed in is the listener's resolved address (a ":0" request arrives with the real port), so it is safe to print in a startup banner: every earlier phase — auto-migrate, seeds, plugin init, OnStart hooks, and the bind itself — has already succeeded. Hooks run in registration order and must not block.

func (*App) OnStart

func (a *App) OnStart(fn func(ctx context.Context) error) *App

OnStart registers a function to run once during App.Start, before the HTTP server begins accepting connections. The context passed in is cancelled when Stop is called, so workers should respect it.

Hooks run in registration order; the first to return a non-nil error aborts Start.

func (*App) OnStop

func (a *App) OnStop(fn func() error) *App

OnStop registers a function to run during App.Shutdown, after the HTTP server has shut down. Hooks run in reverse registration order — the last thing started is the first thing stopped. Internally the hook is wrapped as a lifecycle.Drainer so app-level cleanup and battery drains share one coordinator.

func (*App) OnStopFirst

func (a *App) OnStopFirst(fn func() error) *App

OnStopFirst registers an OnStop hook that runs LAST under the reverse-order Stop iteration. Useful for plugins (battery/log especially) that must outlive every other shutdown step: their close hook needs to fire AFTER every other OnStop has had a chance to emit log entries.

Without this, a user that registers app.OnStop BEFORE RegisterPlugin(log) gets the order inverted on reverse iteration — log's close runs first, the user's OnStop logs into closed sinks.

func (*App) Outbox added in v0.15.0

func (a *App) Outbox() *outbox.Outbox

Outbox returns the transactional event outbox, or nil when the app was built without WithOutbox. Use it to stage your own events inside an App.InTx transaction (Append), inspect delivery state (List), or replay dead rows.

func (*App) RegisterBattery

func (a *App) RegisterBattery(b Battery, deps ...string) *App

RegisterBattery registers a heavyweight, lifecycle-aware battery module (auth, search, cache, etc.) with the application. deps lists battery names that must be initialized before this one. Returns the App for chaining.

Batteries are initialized in dependency-resolved order during App.Start, before the HTTP server binds. Each battery's Init does whatever it needs by calling into the App (routes, middleware, hooks, MCP tools). Batteries that also implement BatteryLifecycle get their OnStart/OnStop fired by the App at the appropriate moment.

Example:

app.RegisterBattery(auth.New(auth.Config{...}), "search") // depends on search battery

func (*App) RegisterEntities

func (a *App) RegisterEntities(entities map[string]entity.EntityConfig) *App

RegisterEntities registers each (name, config) pair via App.Entity in alphabetical-by-name order. Sorting matters: Entity has order-sensitive side effects — router registration, MCP tool list order, OpenAPI tag emission — and Go's map iteration is randomised, so unsorted iteration would mean non-deterministic /openapi.json bytes across restarts (breaking ETag caching) and non-deterministic MCP tools/list responses. FK relations stay safe because AutoMigrate also topologically sorts.

Returns the App for fluent chaining.

app.RegisterEntities(map[string]entity.EntityConfig{
    "foods":  foodsConfig,
    "meals":  mealsConfig,
    "users":  usersConfig,
})

func (*App) RegisterModule added in v0.17.0

func (a *App) RegisterModule(m Module) *App

RegisterModule validates the name, registers the module as a battery (with deps = Manifest().DependsOn so topo-sort orders module init), and records the manifest in the module manager.

app.RegisterModule(myModule) // registers as battery + records manifest

func (*App) RegisterPlugin

func (a *App) RegisterPlugin(plugin Plugin) *App

RegisterPlugin registers a plugin with the application's plugin manager. Returns the App for fluent chaining.

Panics if InitPlugins has already run — plugins must be registered before App.Start (or the explicit InitPlugins call) so their Init fires. The panic is a clear contract violation rather than a silent no-op that would have the new plugin's routes / middleware vanish.

func (*App) RegisterReadiness

func (a *App) RegisterReadiness(name string, check func(ctx context.Context) error) *App

RegisterReadiness adds a readiness check. Names are not required to be unique — duplicates surface in the /readyz response as repeated rows, which is occasionally useful (the same backend probed at two layers).

Pass a timeout on the context if your check could hang; /readyz also applies an overall deadline, but per-check timeouts give better signal in the response.

func (*App) Role added in v0.16.0

func (a *App) Role() Role

Role returns the resolved process role (all/serve/worker). It is set once in NewApp and never changes afterward, so OnStart hooks and other setup code can gate their own background work on it — e.g. skipping an expensive warm-up that only makes sense when serving HTTP:

app.OnStart(func(ctx context.Context) error {
    if app.Role() == framework.RoleWorker {
        return nil // the worker process doesn't serve this cache
    }
    return warmRenderCache(ctx)
})

Plain OnStart hooks are role-agnostic and run in every role; worker- scoped registration (cron/queue/outbox) is gated internally.

func (*App) Router

func (a *App) Router() *router.Router

Router returns the App's *router.Router for advanced use (plugin authors, batteries that need to register routes with custom matching, sub-router construction). Application code should prefer the App-level helpers:

  • App.Use(mw) — register middleware (instead of Router().Use)
  • App.Get/Post/... — register routes (instead of Router().Handle)
  • App.Group(prefix) — sub-routes (instead of Router().Group)

Both forms are functionally equivalent — App.Use forwards to Router().Use — but the App-level surface is the canonical one in docs and examples.

Exposed as a method (rather than a field) so plugins and batteries can swap or wrap the router during Init without callers depending on direct field assignment.

func (*App) Routine

func (a *App) Routine(r migrate.Routine) *App

Routine registers a stored routine (function, procedure, trigger, or view) as a first-class migration object. Its Up runs on every boot (idempotent CREATE OR REPLACE) after tables are migrated, and `migrate generate` tracks it for reversible versioned migrations. Returns App for chaining.

func (*App) RunReadinessChecks added in v0.17.0

func (a *App) RunReadinessChecks(ctx context.Context) ReadinessResponse

RunReadinessChecks executes all registered readiness checks in parallel and returns the structured response. Exported so batteries (e.g. battery/setup's HealthStep) can run the same checks /readyz does without going through HTTP.

func (*App) RunWithSignals

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

RunWithSignals blocks until SIGINT or SIGTERM is received, then runs Shutdown. Returns Shutdown's error, or nil if ctx is cancelled before a signal arrives.

func (*App) SetFlagStore

func (a *App) SetFlagStore(s featureflag.Store) *App

SetFlagStore swaps the underlying store. Must be called before any caller has triggered the lazy default; a second SetFlagStore call (or any call after Flags() / IsEnabled has already been used) panics to avoid the silent race where stale references to the previous evaluator persist in handler closures.

Useful for tests that want a preconfigured store, or for production wiring that uses a persistent backend — wire it during NewApp setup, before any request runs.

func (*App) SetLogger

func (a *App) SetLogger(l *slog.Logger)

SetLogger replaces the App's logger. Atomic; safe to call concurrently with in-flight requests — atomic.Pointer.Store is race-free, and middleware reading via App.Logger() sees the new value on the next request.

Panics if l is nil — the App's logger is always non-nil; pass a discard logger (slog.New(slog.DiscardHandler)) to silence output.

func (*App) Shutdown

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

Shutdown gracefully stops the HTTP server, stops every registered battery in reverse dependency order, then runs each OnStop hook in reverse registration order. Matches net/http.Server.Shutdown's signature (takes a deadline ctx) but does the FULL lifecycle teardown. Safe to call multiple times — subsequent calls are no-ops.

Call this from your signal handler.

func (*App) Start

func (a *App) Start(addr string) error

Start starts the HTTP server on the given address. Auto-migrates tables, registers OpenAPI/Swagger, debug stats, applies the default middleware chain (unless disabled), and calls Mount on every attached Mountable.

Sets the process title to the app name for visibility in ps/Activity Monitor.

func (*App) T

func (a *App) T(ctx context.Context, key string, params ...map[string]any) string

T is a convenience wrapper around the app's Translator. Returns the bare key when no Translator has been wired via WithI18n.

The ctx is expected to already carry a Locale (the i18n middleware attaches one per request from Accept-Language); when it doesn't, the Translator's fallback locale is used.

func (*App) Table

func (a *App) Table(t migrate.Table) *App

Table registers a raw, non-entity table for migration only — no CRUD, no HTTP routes, no validation, no auto-injected columns. The table participates in auto-migrate, diffing, and generation alongside entities (including foreign keys that cross between the two). For users who want migration coverage of a table without the entity machinery. Returns App for chaining.

func (*App) Translator

func (a *App) Translator() *i18n.Translator

Translator returns the wired Translator, or nil if WithI18n was never called. Useful for handlers that need to drive locale-aware formatting beyond simple lookups.

func (*App) TryEntity

func (a *App) TryEntity(name string, config entity.EntityConfig) (err error)

TryEntity is the error-returning variant of Entity: it registers an entity and returns an error on any misconfiguration instead of panicking. It also recovers panics from deeper validation (e.g. an invalid TenantField) and converts them to errors, so a single bad config can never take down the process — the property an agent-driven authoring loop needs.

func (*App) Use

func (a *App) Use(mw ...router.Middleware) *App

Use appends middleware to the app's router chain. The default chain (installed by NewApp unless WithoutDefaultMiddleware is set) stays in place — Use adds to it, never silently replaces it. Plugins call Use from their Init to contribute middleware; router late-binding means these additions also wrap routes registered before the plugin loaded.

func (*App) View

func (a *App) View(v migrate.View) *App

View registers a database view — a virtual table built from other entities. The view is created on boot after its source tables (and tracked reversibly by `migrate generate`), and, when it declares Columns, it is also exposed through the ORM as a READ-ONLY entity: List/Get and the query layer work, but no write routes are registered. Returns App for chaining.

func (*App) WithAuditLog

func (a *App) WithAuditLog(cfg AuditConfig) *App

WithAuditLog enables audit logging on every entity registered on the app (or the subset named in cfg.Entities). Call AFTER Entity registrations.

Returns the app for fluent chaining. Panics if the audit table cannot be created — this is initialization-time work so loud failure is preferable to silent log loss.

func (*App) WithSeed added in v0.3.2

func (a *App) WithSeed(fn func(ctx context.Context) error) *App

WithSeed registers a seed function to run during App.Start AFTER auto-migration completes (so every entity table exists) and BEFORE the HTTP server accepts traffic. This is the documented fix for the common "no such table" footgun: calling db.Exec("INSERT …") from main() before Start() runs migrations fails, because the table isn't there yet.

Seed funcs run in registration order. The first to return a non-nil error aborts Start (the partial-startup teardown drains anything an earlier phase spawned). The context is the app's lifecycle context, so a long-running seed respects shutdown.

WithSeed composes with the per-entity EntityConfig.Seed callback: entity seeds (idempotent, ledger-tracked via _gofastr_seeded) run first as part of auto-migration's RunSeeds phase; WithSeed funcs run immediately after, in the order registered. Use WithSeed for cross-entity or app-level seed logic that doesn't belong to a single entity.

site := framework.NewApp(framework.WithDB(db))
site.Entity("foods", foodsConfig)
site.WithSeed(seedFoods) // runs after the foods table is migrated

Returns the App for fluent chaining.

type AppConfig

type AppConfig struct {
	Name           string        // application name
	JSONCase       crud.JSONCase // JSON key casing: "camelCase" (default) or "snake_case"
	DebugEndpoints bool          // opt-in for /.debug/* endpoints
	NoLLMMD        bool          // disable auto-generated /llm.md entity docs

	// PublicOpenAPI serves /openapi.json without the auth gate. By default
	// the spec is auth-gated (it enumerates every route), so a minimal app
	// returns 401 there — which surprised users following the quickstart
	// curl. Set true (or use WithPublicOpenAPI) when the spec is meant to be
	// public, e.g. a docs site or an internal API behind a network boundary.
	// The Swagger UI at /api/docs/ is always reachable; this only governs
	// the raw spec JSON.
	PublicOpenAPI bool

	// APIPrefix mounts every auto-CRUD entity route (list/get/create/update/
	// delete + _batch + _events + per-entity llm.md) under this path — e.g.
	// "/api" serves GET /api/posts instead of GET /posts. Empty (default)
	// keeps the bare entity-name mounts, so this is not a breaking change.
	// The generated OpenAPI spec expresses the prefix via its server URL.
	// GroupEntity routes are unaffected — a group owns its own prefix. MCP
	// tool names are unchanged.
	APIPrefix string

	// RequestTimeout caps the per-request wall-clock budget enforced by
	// the default middleware chain. Zero (default) installs a 30s cap.
	// Set a positive duration to override. To disable the timeout
	// middleware entirely, set DisableRequestTimeout — overloading
	// sign for "disable" is too easy to trip on (e.g. accidentally
	// subtracting two timestamps).
	RequestTimeout time.Duration

	// DisableRequestTimeout removes the Timeout middleware from the
	// default chain entirely. Useful for long-running uploads / SSE;
	// pair with per-handler ctx deadlines if you still need bounded
	// request lifetime.
	DisableRequestTimeout bool

	// ShutdownTimeout bounds the graceful drain that the default
	// SIGINT/SIGTERM handler runs via App.Shutdown. Zero installs the
	// 15s default. The drain stops accepting connections, waits for
	// in-flight requests, force-closes whatever remains at the deadline
	// (an open SSE stream never goes idle), then stops batteries and
	// runs OnStop hooks.
	ShutdownTimeout time.Duration

	// DisableSignalHandling opts out of the SIGINT/SIGTERM handler that
	// Start installs by default. Set it when the embedding process owns
	// signal handling and calls App.Shutdown (or RunWithSignals) itself.
	DisableSignalHandling bool
}

AppConfig holds application-level configuration.

type AppOption

type AppOption func(*App)

AppOption is a functional option for configuring an App.

func WithACP added in v0.10.0

func WithACP(cfg ACPConfig) AppOption

WithACP serves /.well-known/acp.json with the site's ACP discovery metadata.

func WithAPIPrefix

func WithAPIPrefix(prefix string) AppOption

WithAPIPrefix mounts auto-CRUD entity routes under prefix (e.g. "/api"). See AppConfig.APIPrefix. A leading slash is added and a trailing slash trimmed, so "api", "/api", and "/api/" all behave identically.

func WithAgentSkills added in v0.10.0

func WithAgentSkills(skills []AgentSkillEntry) AppOption

WithAgentSkills serves /.well-known/agent-skills/index.json enumerating the host's published Agent Skills (per the agent-skills-discovery-rfc). The host provides the entries (name/type/url/digest of each SKILL.md or archive it publishes); an empty list still satisfies the discovery check.

func WithAuthMD added in v0.10.0

func WithAuthMD(cfg AuthMDConfig) AppOption

WithAuthMD serves /auth.md and (when AgentAuth is set) merges an agent_auth block into the OAuth authorization-server metadata. Pair with WithOAuthAuthorizationServer for the agent_auth block to be emitted.

func WithConfig

func WithConfig(config AppConfig) AppOption

WithConfig sets the application config. It merges into whatever the granular options (WithAPIPrefix, WithPublicOpenAPI, WithName, …) have already set rather than replacing the struct wholesale, so option order doesn't silently discard config: every field WithConfig sets to a non-zero value wins; a zero field preserves the existing value. To turn a boolean back off, use the granular setter after WithConfig instead of relying on a zero-valued field.

TestWithConfigCoversEveryField pins the field list — extend this merge when adding an AppConfig field.

func WithDB

func WithDB(db *sql.DB) AppOption

WithDB sets the database connection.

func WithFanout added in v0.16.0

func WithFanout(f fanout.Fanout) AppOption

WithFanout attaches a cross-replica fanout (core/fanout.Fanout) to the app's real-time lane. The event bus is bridged — every locally-emitted event is mirrored to the other replicas and re-emitted on their buses, so entity `_events` SSE streams work regardless of which replica holds the connection — and any Mountable that supports SetFanout (a mounted UI host) gets its island manager wired the same way.

SEMANTICS: with a fanout attached the bus becomes a broadcast — every On/Subscribe handler fires on EVERY replica. That is correct for UI push and wrong for side effects; per-event work belongs on the durable lane (WithOutboxConsumer). Handlers that derive new events must gate on event.IsRemote(ctx). See the events doc, "Cross-replica fan-out".

The fanout is caller-owned: construct it before NewApp (e.g. framework/fanout.NewPostgres) and close it after Shutdown. The bridge itself is detached by Shutdown. Panics if f is nil.

func WithFileStorage

func WithFileStorage(s upload.Storage) AppOption

WithFileStorage sets the default upload.Storage used by CRUD handlers to persist files for Image and File entity fields when a multipart request arrives. Without this option, multipart requests on those fields fail with a clear error.

func WithI18n

func WithI18n(tr *i18n.Translator) AppOption

WithI18n installs a Translator and wires its locale-negotiation middleware into the default chain. Handlers downstream can call App.T(ctx, key, ...) for translated strings driven by the caller's Accept-Language. Also installed as i18n.Default() so the package- level i18n.T helper works from anywhere.

Panics when paired with WithoutDefaultMiddleware — register the middleware explicitly in your custom chain in that case.

func WithIdempotency

func WithIdempotency(cfg middleware.IdempotencyConfig) AppOption

WithIdempotency adds an Idempotency-Key middleware to the default chain. Pass middleware.IdempotencyConfig{} to take all defaults; the option is otherwise idiomatic-Go composition over the existing middleware.Idempotency primitive.

Has no effect when WithoutDefaultMiddleware is also set — wire your own chain explicitly in that case.

func WithLocaleResolver added in v0.19.0

func WithLocaleResolver(f func(*http.Request) (string, bool)) AppOption

WithLocaleResolver installs a custom locale resolver consulted BEFORE the X-Locale / Accept-Language headers during locale negotiation. Use it to make a stored per-user locale (e.g. a cookie set by a "change language" handler) win over the browser's Accept-Language.

Pair with i18n.CookieLocale for the common cookie case:

a := framework.NewApp(
    framework.WithI18n(tr),
    framework.WithLocaleResolver(i18n.CookieLocale("locale")),
)

Panics if used without WithI18n — locale resolution is meaningless without a translator/catalog to resolve against.

func WithLogger

func WithLogger(l *slog.Logger) AppOption

WithLogger sets the App's *slog.Logger. Same effect as calling App.SetLogger after NewApp; available as an option for symmetry. Panics if l is nil — the App's logger is always non-nil; pass a discard logger (slog.New(slog.DiscardHandler)) if you want to silence output.

func WithMCP added in v0.10.0

func WithMCP() AppOption

WithMCP exposes the app's MCP server at /mcp using the Streamable HTTP transport (POST JSON-RPC + GET Server-Sent Events), so a host doesn't have to hand-wire fwApp.Router().Handle("POST", "/mcp", fwApp.MCP). This is the agent-ready default: combined with uihost.WithAgentReady (which advertises /mcp via the agent card + Link headers) it makes the server's tools discoverable to MCP clients. Calling this AND manually mounting /mcp will panic with a route conflict — pick one.

func WithMCPIntrospection

func WithMCPIntrospection() AppOption

WithMCPIntrospection installs a set of MCP tools that expose the running App's structure to an agent: registered routes, plugins, batteries, app config, readiness status. Opt-in because:

  • The tools are read-only but cumulatively reveal a lot about the app's shape (every mounted route, every loaded module, every config field). Production apps that expose MCP to untrusted callers should weigh the disclosure surface before enabling.
  • The framework should stay zero-config-friendly: nothing should show up on the MCP server unless the operator asked for it.

Tools registered:

  • app_routes: list every (method, pattern) registered on the router.
  • app_plugins: list registered plugins (name only).
  • app_batteries: list registered batteries with deps + lifecycle status.
  • app_config: return the AppConfig snapshot (Name, JSONCase, timeouts…).
  • app_readiness: run every registered readiness check and report results.
  • framework_docs_list / framework_docs_get / framework_docs_search: expose the framework's markdown docs (embedded at build time, so they match the framework version this binary was built against — no GitHub fetch).

func WithMCPServer

func WithMCPServer(s *mcp.Server) AppOption

WithMCPServer sets a custom MCP server.

func WithMetrics

func WithMetrics() AppOption

WithMetrics enables HTTP request metrics (per-route counts, status classes, latency histograms) in the default middleware chain and mounts a Prometheus-format /metrics endpoint. The endpoint is unauthenticated by design (scrape it from inside your network / behind your ingress). Panics when paired with WithoutDefaultMiddleware — mount middleware.MetricsMiddleware and middleware.MetricsHandler yourself in that case.

func WithOAuthAuthorizationServer added in v0.10.0

func WithOAuthAuthorizationServer(cfg OAuthAuthorizationServerConfig) AppOption

WithOAuthAuthorizationServer serves /.well-known/oauth-authorization-server (RFC 8414). Use it when the app is an OAuth2/OpenID issuer so clients can discover endpoints + supported capabilities.

func WithOAuthProtectedResource added in v0.10.0

func WithOAuthProtectedResource(cfg OAuthProtectedResourceConfig) AppOption

WithOAuthProtectedResource serves /.well-known/oauth-protected-resource (RFC 9728) from cfg. Use it when the app exposes OAuth-token-protected resources so clients can discover how to obtain and present tokens.

func WithOutbox added in v0.15.0

func WithOutbox(opts ...outbox.Option) AppOption

WithOutbox enables the transactional event outbox (framework/outbox): CRUD lifecycle events are written to an outbox table INSIDE the same transaction as the entity write, and a relay goroutine (started by App.Start, drained on Shutdown) delivers each committed row to the declared durable consumers. This closes the crash window where a plain post-commit emit is lost, and makes delivery at-least-once per consumer — consumers that care must dedupe on Event.ID. Requires WithDB; NewApp panics otherwise.

The relay delivers ONLY to consumers declared via WithOutboxConsumer; it no longer publishes to the live event bus. The real-time lane (SSE EventStream, ephemeral On/Subscribe) is fed independently by EmitEvent, so the two lanes never duplicate. opts are forwarded to outbox.New (outbox.WithTable, outbox.WithPollInterval, …).

func WithOutboxConsumer added in v0.16.0

func WithOutboxConsumer(name, eventType string, handler event.EventHandler) AppOption

WithOutboxConsumer declares a durable outbox consumer. Requires WithOutbox (NewApp panics if the outbox isn't also enabled). name is a stable identity used to track per-consumer delivery across restarts/replicas; (eventType, name) must be unique. handler is invoked once per delivery with Event.ID set to the outbox row id (dedup key) — it must be idempotent (at-least-once delivery). A handler that errors or panics is retried with backoff and eventually dead-lettered independently of its sibling consumers (sibling isolation).

func WithPublicOpenAPI

func WithPublicOpenAPI() AppOption

WithPublicOpenAPI serves /openapi.json without the auth gate. Equivalent to setting AppConfig.PublicOpenAPI. Use when the spec is meant to be public (docs sites, internal APIs behind a network boundary).

func WithReadinessTimeout

func WithReadinessTimeout(d time.Duration) AppOption

WithReadinessTimeout overrides the per-request /readyz deadline. Default 5 seconds. Checks that don't return within the deadline are reported as errors with status "timeout".

func WithRole added in v0.16.0

func WithRole(r Role) AppOption

WithRole sets the process role explicitly, overriding the GOFASTR_ROLE env var. Panics in NewApp if r is not one of RoleAll / RoleServe / RoleWorker — matching how every other invalid option fails fast at construction rather than silently degrading.

func WithRouter

func WithRouter(r *router.Router) AppOption

WithRouter sets a custom router.

func WithSetup added in v0.17.0

func WithSetup(r SetupRunner) AppOption

WithSetup wires a first-run setup runner. When the runner reports incomplete setup at boot, Start either runs the steps headlessly (env provides every required value) or serves an interactive wizard until setup finishes — then atomically swaps to the real app router.

Overrides via the GOFASTR_SETUP env var:

  • "off": never enter setup mode (Start proceeds normally)
  • "force": enter setup mode even if Complete reports done (rescue)
  • invalid: Start fails loudly

func WithTracing

func WithTracing() AppOption

WithTracing enables the OpenTelemetry tracing middleware in the default chain. Each request runs in a span with method/route/status attributes. Spans no-op until you install a TracerProvider via otel.SetTracerProvider (e.g. an OTLP exporter), so this is safe to leave on. Panics when paired with WithoutDefaultMiddleware.

func WithUCP added in v0.10.0

func WithUCP(cfg UCPConfig) AppOption

WithUCP serves /.well-known/ucp with the site's UCP discovery metadata.

func WithVerboseReadiness

func WithVerboseReadiness() AppOption

WithVerboseReadiness opts in to including each check's error.Error() string in the /readyz JSON response. Default is to omit error text because /readyz is typically reachable without authentication and raw error strings frequently leak internal IPs, connection strings, or other infrastructure detail.

Turn this on only when the probes are scoped to a trusted listener or behind auth.

func WithWebBotAuth added in v0.10.0

func WithWebBotAuth(cfg WebBotAuthConfig) AppOption

WithWebBotAuth serves /.well-known/http-message-signatures-directory with the site's signing JWKS.

func WithoutAutoMigrate added in v0.15.0

func WithoutAutoMigrate() AppOption

WithoutAutoMigrate disables the entity auto-migration that App.Start otherwise runs before serving. Use it in deployments whose policy forbids unattended schema changes on boot: generate the entity DDL into versioned migration files instead (`gofastr migrate generate <name>`) and apply them as an explicit step (`gofastr migrate up`). Entity seeds still run at Start — they are idempotent data, not schema — which also means an entity WITH seeds fails Start fast when its table is missing, instead of the app serving against an unmigrated schema.

func WithoutDefaultMiddleware

func WithoutDefaultMiddleware() AppOption

WithoutDefaultMiddleware disables the default middleware chain (recovery, request-id, logging, security headers, timeout). Use this when you want full control over middleware composition via Use().

type ApplyOptions

type ApplyOptions = migrate.ApplyOptions

type AuditConfig

type AuditConfig struct {
	// Table is the destination table for audit rows. Defaults to "audit_log".
	Table string

	// Actor resolves the actor id (typically a user id) from the request
	// context. Return "" when no actor is attached (e.g. system writes).
	Actor func(context.Context) string

	// Entities restricts auditing to the named entities. Empty means audit
	// every registered entity.
	Entities []string

	// Redact, when non-nil, is called with the entity name and a
	// defensive copy of the row about to be serialised into the
	// `diff` column. Return either the modified input (safe to mutate
	// — the framework already copied it) or a fresh map. Either works.
	//
	// When nil, the framework applies a default sensitive-field scrub
	// (see defaultSensitiveSuffixes) — fields whose names look like
	// passwords, tokens, secrets, or keys are dropped from the diff so
	// a host that forgot to configure Redact doesn't accidentally
	// stream credentials into the audit log.
	//
	// Redact is invoked for AfterCreate, AfterUpdate, AND AfterDelete.
	// On delete the input is `map[string]any{"id": <record_id>}`; the
	// returned map's "id" value (if any) becomes the audit row's
	// record_id, letting hosts pseudonymise the natural key on delete
	// too. If the callback returns a map with no `id` key the framework
	// falls back to the original — silently erasing the record_id is
	// an audit-forensics erasure primitive we don't want to expose.
	//
	// Returning nil is equivalent to returning an empty map. A panic
	// inside Redact is recovered: the audit row is still written using
	// the original pre-redact value so audit coverage isn't lost to a
	// misbehaving callback.
	Redact func(entity string, row map[string]any) map[string]any
}

AuditConfig configures the audit log helper.

Audit rows are written via lifecycle hooks: AfterCreate, AfterUpdate, and AfterDelete. The hook fires inside the same transaction as the operation it audits, so partial writes are impossible — a rollback drops the audit row along with the change.

type AuthMDConfig added in v0.10.0

type AuthMDConfig struct {
	// Markdown is the /auth.md body — the procedural manifest agents read
	// (discover → register → claim → exchange → use → revoke). Host-authored.
	Markdown string

	// AgentAuth, when set, is merged as an `agent_auth` block into the
	// /.well-known/oauth-authorization-server document (requires
	// WithOAuthAuthorizationServer). Nil omits the block.
	AgentAuth *AgentAuthBlock
}

AuthMDConfig configures /auth.md and the agent_auth discovery block.

type Battery

type Battery interface {
	// Name returns the unique battery identifier. Used for dependency
	// resolution and logging.
	Name() string

	// Init wires the battery into the App. Called once during App.Start,
	// before lifecycle hooks fire and after any batteries this one
	// depends on have themselves initialized.
	Init(app *App) error
}

Battery is the interface for heavyweight, lifecycle-aware modules that extend a GoFastr application (auth, search, cache, queue, etc.).

A Battery is a Plugin (Init(*App) error is the same shape) plus two things a plain Plugin doesn't have:

  1. Dependency declarations. Pass dependency names at RegisterBattery time and the framework topologically sorts the Init order so dependents see their dependencies wired first.
  2. Structured start/stop. Implement BatteryLifecycle to get per-battery OnStart(ctx) / OnStop(ctx) hooks, distinct from the App-wide app.OnStart / app.OnStop. The framework runs them in dependency order on Start and reverse-dependency order on Stop.

If neither of those applies, prefer Plugin — there is less ceremony and the Plugin/Battery distinction stays meaningful only for the modules that genuinely need it.

From Init the battery does everything by calling into the App — register routes via app.Router, middleware via app.Use, swap the logger via app.SetLogger, MCP tools via app.MCP, hooks via app.HookRegistry(name).RegisterHook(...), and so on. There are no optional interfaces for routes / middleware / hooks / tools — Init owns it all.

type BatteryLifecycle

type BatteryLifecycle interface {
	Battery

	// OnStart is called after all batteries are initialized and before the
	// HTTP server begins accepting connections. The context is cancelled
	// when the app shuts down, so long-running workers should respect it.
	// The first error aborts startup.
	OnStart(ctx context.Context) error

	// OnStop is called during graceful shutdown, after the HTTP server has
	// stopped. Batteries stop in reverse dependency order (dependents
	// first, then their dependencies).
	OnStop(ctx context.Context) error
}

BatteryLifecycle is the optional interface for batteries that need to participate in the App's startup and shutdown sequence.

type BatteryManager

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

BatteryManager manages registered batteries, resolves dependencies, and orchestrates initialization and lifecycle.

func NewBatteryManager

func NewBatteryManager() *BatteryManager

NewBatteryManager creates a new BatteryManager.

func (*BatteryManager) All

func (bm *BatteryManager) All() []Battery

All returns all registered batteries in dependency-resolved order.

func (*BatteryManager) Get

func (bm *BatteryManager) Get(name string) (Battery, error)

Get retrieves a battery by name. Returns the Battery interface so callers can type-assert to the concrete type or any optional interface.

func (*BatteryManager) InitAll

func (bm *BatteryManager) InitAll(app *App) error

InitAll initializes all batteries in dependency order. Called during App.Start before the HTTP server binds.

func (*BatteryManager) Names

func (bm *BatteryManager) Names() []string

Names returns battery names in dependency-resolved order.

func (*BatteryManager) Register

func (bm *BatteryManager) Register(b Battery, deps ...string) error

Register adds a battery with optional dependency declarations. Deps lists battery names that must be initialized before this one. Returns an error on duplicate name or unknown dependency.

func (*BatteryManager) StartAll

func (bm *BatteryManager) StartAll(ctx context.Context) error

StartAll calls OnStart on batteries that implement BatteryLifecycle, in dependency order (dependencies first).

func (*BatteryManager) StopAll

func (bm *BatteryManager) StopAll(ctx context.Context) error

StopAll calls OnStop on batteries that implement BatteryLifecycle, in reverse dependency order (dependents first, then dependencies).

type BoolColumn

type BoolColumn = entity.BoolColumn

type Column

type Column = migrate.Column

type Condition

type Condition = entity.Condition

type CronJob

type CronJob = cron.CronJob

type CrudHandler

type CrudHandler = crud.CrudHandler

type DBExecutor

type DBExecutor = db.Executor

type DestructiveChangeError

type DestructiveChangeError = migrate.DestructiveChangeError

type Dialect

type Dialect = migrate.Dialect

type Endpoint

type Endpoint = entity.Endpoint

type Entity

type Entity = entity.Entity

type EntityConfig

type EntityConfig = entity.EntityConfig

type EntityDeclaration

type EntityDeclaration = entity.EntityDeclaration

type Event

type Event = event.Event

type EventBus

type EventBus = event.EventBus

type EventHandler

type EventHandler = event.EventHandler

type ExportOption added in v0.20.0

type ExportOption func(*exportConfig)

ExportOption configures ExportData.

func WithExportPageSize added in v0.20.0

func WithExportPageSize(n int) ExportOption

WithExportPageSize sets the keyset page size (rows per SELECT). The default (1000) bounds memory for large tables. Non-positive falls back to the default.

func WithExportTime added in v0.20.0

func WithExportTime(t time.Time) ExportOption

WithExportTime stamps the manifest's created_at with the given time instead of time.Now. Pass a fixed value when deterministic output matters (tests, reproducible archives). Zero means "use time.Now().UTC()".

type FieldDeclaration

type FieldDeclaration = entity.FieldDeclaration

type FloatColumn

type FloatColumn = entity.FloatColumn

type ForeignKey

type ForeignKey = migrate.ForeignKey

type GrantStore added in v0.20.0

type GrantStore = access.GrantStore

type GroupOption

type GroupOption = routegroup.GroupOption

GroupOption is re-exported for routegroup configuration.

type HookFunc

type HookFunc = hook.HookFunc

type HookGetPayload

type HookGetPayload = hook.GetPayload

type HookListPayload

type HookListPayload = hook.ListPayload

type HookRegistry

type HookRegistry = hook.HookRegistry

type HookType

type HookType = hook.HookType

type HookWhereClause

type HookWhereClause = hook.WhereClause

type InMemoryModuleStore added in v0.17.0

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

InMemoryModuleStore is the default store for apps without a DB. State is lost on restart — modules re-enable on every boot.

func NewInMemoryModuleStore added in v0.17.0

func NewInMemoryModuleStore() *InMemoryModuleStore

NewInMemoryModuleStore creates an empty in-memory store.

func (*InMemoryModuleStore) Load added in v0.17.0

func (s *InMemoryModuleStore) Load(_ context.Context) (map[string]bool, error)

Load returns the current enable/disable map (possibly empty).

func (*InMemoryModuleStore) SetEnabled added in v0.17.0

func (s *InMemoryModuleStore) SetEnabled(_ context.Context, name string, enabled bool) error

SetEnabled persists the enabled state in memory.

type IncludeNode

type IncludeNode = crud.IncludeNode

type Index

type Index = entity.Index

type IntColumn

type IntColumn = entity.IntColumn

type JSONCase

type JSONCase = crud.JSONCase

type ListOptions

type ListOptions = crud.ListOptions

type ListResponse

type ListResponse = crud.ListResponse

type Metrics

type Metrics = middleware.Metrics

type MigrationPlan

type MigrationPlan = migrate.Plan

type Module added in v0.17.0

type Module interface {
	Battery
	Manifest() ModuleManifest
}

Module is a Battery plus a manifest. Everything a module registers during Init (routes, entities, cron jobs, queue consumers, MCP tools) is attributed to the module, and a runtime enable/disable gate is enforced at dispatch time: disabled → its routes 404, its cron jobs and queue consumers skip, its MCP tools refuse.

type ModuleInfo added in v0.17.0

type ModuleInfo struct {
	Name           string
	Version        string
	Description    string
	DependsOn      []string
	MigrationGroup string
	Enabled        bool
	EntityCount    int
	RouteCount     int
	ToolCount      int
}

ModuleInfo is the introspection view of a registered module.

type ModuleManager added in v0.17.0

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

ModuleManager tracks registered modules, their enable/disable state, and the attribution of routes/tools/entities to modules. It is the runtime gate that disabled-module enforcement reads on every request.

func NewModuleManager added in v0.17.0

func NewModuleManager(db *sql.DB, f fanout.Fanout) *ModuleManager

NewModuleManager creates a manager backed by the appropriate store. When db is non-nil a SQLModuleStore is used; otherwise in-memory. If db is non-nil and the SQL store cannot be created (e.g. CREATE TABLE fails), the error is stored and surfaced by loadFromStore so boot fails closed — a deliberately disabled module must not silently come back enabled on a broken store.

func (*ModuleManager) Disable added in v0.17.0

func (mm *ModuleManager) Disable(ctx context.Context, name string) error

Disable persists the new state and flips the cache. Refuses (fail closed) if any currently-enabled module lists name in DependsOn — no cascade. Serialized by toggleMu alongside Enable.

func (*ModuleManager) Enable added in v0.17.0

func (mm *ModuleManager) Enable(ctx context.Context, name string) error

Enable persists the new state and flips the cache. Refuses if any of the module's DependsOn is disabled. The check-then-act sequence (dependency check → store write → cache flip) is serialized by toggleMu so concurrent toggles cannot interleave to a forbidden state.

func (*ModuleManager) Enabled added in v0.17.0

func (mm *ModuleManager) Enabled(name string) bool

Enabled is the hot-path read the dispatch gates use. A single map read under RLock; absent modules are enabled by default.

func (*ModuleManager) List added in v0.17.0

func (mm *ModuleManager) List() []ModuleInfo

List returns introspection info for every registered module.

type ModuleManifest added in v0.17.0

type ModuleManifest struct {
	// Version is an optional, informational version string.
	Version string

	// Description is a short, human-readable summary.
	Description string

	// DependsOn names other MODULES that must be enabled (and initialised)
	// before this one. RegisterModule forwards this to BatteryManager as
	// the battery dep list, so topo-sort orders module init.
	DependsOn []string

	// MigrationGroup defaults to the module name. It is an informational
	// pointer to the core/migrate group (#33) the module owns — the
	// framework does not enforce that it matches a registered migration
	// group, but the modules doc describes the tie-in.
	MigrationGroup string
}

ModuleManifest is the declarative metadata a Module carries alongside its Battery Init. Everything in it is informational — the framework uses DependsOn for battery init ordering (it doubles as the battery dep list), and the rest is surfaced through introspection.

type ModuleStore added in v0.17.0

type ModuleStore interface {
	Load(ctx context.Context) (map[string]bool, error)
	SetEnabled(ctx context.Context, name string, enabled bool) error
}

ModuleStore persists module enable/disable state across restarts. The default in-memory store is used when the app has no DB; an app built with WithDB gets the SQL store automatically.

type Mountable

type Mountable interface {
	Mount(*router.Router)
}

Mountable is anything that can register routes on the framework's router. UI hosts, admin panels, websocket pubsub layers, etc. all satisfy this interface and are attached via App.Mount.

type OAuthAuthorizationServerConfig added in v0.10.0

type OAuthAuthorizationServerConfig struct {
	Issuer                            string // REQUIRED: issuer identifier URL
	AuthorizationEndpoint             string
	TokenEndpoint                     string
	IntrospectionEndpoint             string
	UserinfoEndpoint                  string
	JwksURI                           string
	ScopesSupported                   []string
	ResponseTypesSupported            []string
	GrantTypesSupported               []string
	TokenEndpointAuthMethodsSupported []string
}

OAuthAuthorizationServerConfig configures /.well-known/oauth-authorization-server (RFC 8414). Relevant when the host acts as an OAuth2/OpenID issuer (battery/auth is a client by default, so this is opt-in).

type OAuthProtectedResourceConfig added in v0.10.0

type OAuthProtectedResourceConfig struct {
	// Resource is the protected resource's identifier: an absolute https
	// URL with no fragment (a query component is discouraged but allowed).
	// REQUIRED by RFC 9728.
	Resource string

	// AuthorizationServers lists OAuth authorization-server issuer
	// identifiers (per RFC 8414) whose tokens this resource accepts.
	AuthorizationServers []string

	// ScopesSupported lists the scope values accepted in access requests
	// to this resource.
	ScopesSupported []string

	// BearerMethodsSupported lists how a bearer token may be presented:
	// "header", "body", and/or "query" (RFC 6750). Defaults to ["header"].
	BearerMethodsSupported []string

	// JWKSURI is the https URL of the resource's JWK Set (public signing
	// keys the resource uses to sign responses, if any).
	JWKSURI string

	// ResourceName is a human-readable name for display.
	ResourceName string

	// ResourceDocumentation is a https URL with developer info.
	ResourceDocumentation string

	// ResourcePolicyURI is a https URL describing data-use policy.
	ResourcePolicyURI string

	// ResourceTOSURI is a https URL describing terms of service.
	ResourceTOSURI string
}

OAuthProtectedResourceConfig configures the /.well-known/oauth-protected-resource endpoint (RFC 9728).

type Order

type Order = entity.Order

type Permission

type Permission = access.Permission

type Plugin

type Plugin interface {
	// Name returns the unique plugin identifier.
	Name() string
	// Init wires the plugin into the App. Called once during App.Start.
	Init(app *App) error
}

Plugin is the interface for lightweight GoFastr extensions — anything that needs to register routes, middleware, hooks, or MCP tools but has no dependency on other modules and no structured start/stop lifecycle of its own.

Plugins have a single integration point: Init(app). From there a plugin does everything it needs by calling into the App — register routes via app.Router, add middleware via app.Use, swap the logger via app.SetLogger, register MCP tools via app.MCP, attach hooks via app.HookRegistry(name).RegisterHook(...), and so on.

There are no optional interfaces. The router resolves middleware late-bound, so middleware added from Init wraps routes registered before the plugin loaded — there is no ordering footgun to dodge.

When to pick Plugin vs Battery

  • Plugin: stateless or self-contained; no dependency on other modules; uses app.OnStart / app.OnStop if it needs lifecycle.
  • Battery: depends on another module being initialised first (e.g. auth needs the user store wired before login), OR needs its own structured OnStart/OnStop distinct from the App-wide hooks (e.g. background queue workers with their own ctx).

Both share the Init(*App) shape; Battery just adds dependency declarations and a separate BatteryLifecycle. Most extensions should start as Plugin and graduate to Battery only when one of those two conditions appears.

type PluginManager

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

PluginManager manages registered plugins.

func NewPluginManager

func NewPluginManager() *PluginManager

NewPluginManager creates a new plugin manager.

func (*PluginManager) All

func (pm *PluginManager) All() []Plugin

All returns all registered plugins in order.

func (*PluginManager) Get

func (pm *PluginManager) Get(name string) (Plugin, error)

Get retrieves a plugin by name.

func (*PluginManager) InitAll

func (pm *PluginManager) InitAll(app *App) error

InitAll initializes all plugins in registration order.

Wraps each Init in a deferred recover so a panic — most commonly `http: multiple registrations for ...` from a duplicate route pattern — gets tagged with the offending plugin's name. Without this the panic surfaces deep in ServeMux with no context about which plugin registered the conflicting route.

Tracks per-plugin init state so that a retry after partial failure (App.InitPlugins rolls back its global latch on error) only re-runs plugins that haven't already applied side effects. Without this a retry would re-Register the routes the successful plugins already added and panic on the ServeMux duplicate-pattern check.

func (*PluginManager) Names

func (pm *PluginManager) Names() []string

Names returns the names of all registered plugins in registration order.

func (*PluginManager) Register

func (pm *PluginManager) Register(plugin Plugin) error

Register adds a plugin to the manager. Returns an error if a plugin with the same name is already registered, or if the name is invalid (empty, whitespace-only, contains a control character, or longer than maxModuleNameLen).

type Policy

type Policy = access.Policy

type ReadinessCheck

type ReadinessCheck struct {
	Name  string
	Check func(ctx context.Context) error
}

ReadinessCheck is a named probe run by GET /readyz. Implementations must be fast (sub-second), idempotent, and safe to invoke concurrently. Return a non-nil error to mark the app "not ready" — clients (load balancers, k8s, Fly health checks) treat a 503 from /readyz as a signal to stop routing traffic.

type ReadinessRegistrar

type ReadinessRegistrar interface {
	RegisterReadinessChecks(app *App)
}

ReadinessRegistrar is the optional interface plugins and batteries implement to contribute checks. The framework's plugin/battery machinery probes for it during InitPlugins and calls RegisterReadinessChecks before health endpoints mount.

The method is deliberately named RegisterReadinessChecks (plural) to avoid colliding with App.RegisterReadiness(name, fn) — the two are different surfaces, and embedding *App into a battery would otherwise produce an ambiguous selector.

type ReadinessResponse

type ReadinessResponse struct {
	Status string            `json:"status"`
	Checks []ReadinessResult `json:"checks"`
}

ReadinessResponse is the JSON shape returned by /readyz.

type ReadinessResult

type ReadinessResult struct {
	Name   string `json:"name"`
	Status string `json:"status"` // "ok", "error", or "timeout"
	Error  string `json:"error,omitempty"`
	DurMS  int64  `json:"durationMs"`
}

ReadinessResult is one row in the /readyz response.

type Registry

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

Registry stores and retrieves Entity definitions by name. It is safe for concurrent use.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new empty entity registry.

func (*Registry) All

func (r *Registry) All() map[string]*entity.Entity

All returns a copy of the map of all registered entities. Map iteration order is randomised by Go; use AllSorted() for stable iteration in code paths that emit order-sensitive output.

func (*Registry) AllSorted

func (r *Registry) AllSorted() []*entity.Entity

AllSorted returns every registered entity in alphabetical order by name. Use this whenever the iteration order affects bytes-on-the-wire (OpenAPI tag emission, LLM-markdown, codegen output) so the same registry produces the same artefact across restarts.

func (*Registry) Get

func (r *Registry) Get(name string) (*entity.Entity, error)

Get retrieves an Entity by name. Returns an error if no entity with that name is registered.

func (*Registry) Register

func (r *Registry) Register(ent *entity.Entity) error

Register adds an Entity to the registry. Returns an error if an entity with the same name already exists.

func (*Registry) SetDB

func (r *Registry) SetDB(db *sql.DB)

SetDB sets the database connection on the registry and propagates it to all registered entities.

type Relation

type Relation = entity.Relation

type RelationType

type RelationType = entity.RelationType

type Role added in v0.16.0

type Role string

Role selects which responsibilities a single binary assumes at boot. One binary, role picked at deploy time — so background load (cron, queue workers, the outbox relay) can run in a dedicated process that doesn't share a listener with request serving.

Resolution precedence (see resolveRole): WithRole > the GOFASTR_ROLE env var > RoleAll. An unknown value in either place fails loudly in NewApp — a typo'd role must never silently run the wrong workload.

const (
	// RoleAll is the default: serve HTTP AND run background consumers
	// (cron, queues, the outbox relay). Exactly today's behavior — zero
	// change for existing apps.
	RoleAll Role = "all"

	// RoleServe runs the full HTTP surface (router, auto-migrate, seeds,
	// plugins, batteries) but does NOT start worker-scoped consumers:
	// AddCron/AddQueue registrations and the outbox relay are skipped, so
	// a serve-only process never starts — nor later tries to drain — a
	// scheduler it never owned. Plain OnStart hooks still run; gate your
	// own via App.Role().
	RoleServe Role = "serve"

	// RoleWorker runs background consumers (cron, queues, outbox relay)
	// and binds addr, but serves ONLY the health surface (/healthz +
	// /readyz). It does NOT mount the app router, entity CRUD, OpenAPI,
	// docs, admin, or well-known discovery routes. Auto-migrate, seeds,
	// plugins, and batteries still run (migrations take a lock; either
	// process type may boot first).
	RoleWorker Role = "worker"
)

type RolePolicy

type RolePolicy = access.RolePolicy

type RouteGroup

type RouteGroup = routegroup.RouteGroup

RouteGroup is the App-level route group abstraction. Created via App.Group().

type Routine

type Routine = migrate.Routine

type SQLModuleStore added in v0.17.0

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

SQLModuleStore persists module state in a gofastr_modules table. Self-migrating (CREATE TABLE IF NOT EXISTS) — not a migrate group.

func NewSQLModuleStore added in v0.17.0

func NewSQLModuleStore(db *sql.DB) (*SQLModuleStore, error)

NewSQLModuleStore creates a SQL-backed store and ensures the table exists. dialect is probed via migrate.DetectDialect when not explicit.

func (*SQLModuleStore) Load added in v0.17.0

func (s *SQLModuleStore) Load(ctx context.Context) (map[string]bool, error)

Load returns every persisted enable/disable row.

func (*SQLModuleStore) SetEnabled added in v0.17.0

func (s *SQLModuleStore) SetEnabled(ctx context.Context, name string, enabled bool) error

SetEnabled upserts a module's enabled state.

type Scheduler

type Scheduler = cron.Scheduler

type SchemaChange

type SchemaChange = migrate.SchemaChange

type SchemaSnapshot

type SchemaSnapshot = migrate.SchemaSnapshot

type SetupRunner added in v0.17.0

type SetupRunner interface {
	// Incomplete reports whether first-run setup has not yet finished.
	Incomplete(ctx context.Context) (bool, error)

	// CanRunHeadless reports whether every required field across all
	// steps resolves from the environment, so bootstrap can run inline.
	CanRunHeadless(ctx context.Context) (bool, error)

	// RunSteps runs all steps synchronously. Called only when
	// CanRunHeadless returns true. An error aborts Start.
	RunSteps(ctx context.Context) error

	// Handler returns the interactive setup surface. swap is called
	// once setup completes to switch to the real handler. healthz and
	// readyz are the app's existing health handlers, passed so the
	// setup surface can serve /healthz and /readyz during setup.
	Handler(swap func(), healthz, readyz http.HandlerFunc) http.Handler

	// SetupURL returns the operator-facing URL (with token) for the
	// startup banner. addr is the bound listen address. Returns ""
	// when not applicable (headless path or token disabled).
	SetupURL(addr string) string
}

SetupRunner is the interface a first-run setup implementation provides. framework defines it (rather than importing battery/setup) to avoid a layering cycle: battery/setup implements it and the host wires the concrete runner via WithSetup.

Lifecycle inside App.Start:

  1. Incomplete is consulted after plugin init and before consumer start. When true (and GOFASTR_SETUP != "off"), Start enters setup mode.
  2. CanRunHeadless distinguishes the two skins. When true every required field resolves from the environment and RunSteps executes inline before the port binds — no wizard is ever served.
  3. When CanRunHeadless is false the interactive skin is served: Handler returns the wizard surface and swap is invoked once the final step succeeds, atomically switching to the real app handler and starting deferred background consumers.

type StringColumn

type StringColumn = entity.StringColumn

type Table

type Table = migrate.Table

type TenantConfig

type TenantConfig = tenant.TenantConfig

type TestApp

type TestApp struct {
	App *App
	// contains filtered or unexported fields
}

TestApp wraps an App for in-memory testing (no real HTTP listener). Uses httptest.NewRequest + http.Handler.ServeHTTP for speed.

func TestHarness

func TestHarness(t testing.TB, app *App) *TestApp

TestHarness creates an in-memory test harness around an App. No real HTTP server is started — requests go directly through the router.

Calls app.InitPlugins() internally so plugin / battery wiring is in place before the first request. Without this, RegisterPlugin'd behaviour silently does nothing under the harness (Init never fires). Idempotent guard inside InitPlugins makes this safe even if Start is called later by the same test.

func (*TestApp) Close

func (ta *TestApp) Close()

Close is a no-op for in-memory testing (provided for API consistency).

func (*TestApp) Delete

func (ta *TestApp) Delete(path string) *TestResponse

Delete performs a DELETE request.

func (*TestApp) Get

func (ta *TestApp) Get(path string) *TestResponse

Get performs a GET request and returns a TestResponse for assertions.

func (*TestApp) Post

func (ta *TestApp) Post(path string, body any) *TestResponse

Post performs a POST request with a JSON body.

func (*TestApp) Put

func (ta *TestApp) Put(path string, body any) *TestResponse

Put performs a PUT request with a JSON body.

func (*TestApp) Request

func (ta *TestApp) Request(method, path string, body io.Reader) *TestRequest

Request creates a raw TestRequest for further customisation before Execute.

type TestRequest

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

TestRequest wraps an http.Request with a fluent builder API.

func (*TestRequest) Execute

func (tr *TestRequest) Execute() *TestResponse

Execute sends the request and returns a TestResponse.

func (*TestRequest) WithBody

func (tr *TestRequest) WithBody(body any) *TestRequest

WithBody sets the request body. Non-reader values are marshalled as JSON.

func (*TestRequest) WithHeader

func (tr *TestRequest) WithHeader(key, value string) *TestRequest

WithHeader adds a header to the request.

type TestResponse

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

TestResponse wraps the recorded response with assertion helpers.

func (*TestResponse) AssertBodyContains

func (tr *TestResponse) AssertBodyContains(t testing.TB, substr string) *TestResponse

AssertBodyContains asserts the body contains the given substring.

func (*TestResponse) AssertHeader

func (tr *TestResponse) AssertHeader(t testing.TB, key, expected string) *TestResponse

AssertHeader asserts a response header value. Chainable.

func (*TestResponse) AssertJSON

func (tr *TestResponse) AssertJSON(t testing.TB, expected any) *TestResponse

AssertJSON asserts the response body equals expected after JSON normalisation. Compares decoded values (not raw strings) so key order and number types don't matter.

func (*TestResponse) AssertStatus

func (tr *TestResponse) AssertStatus(t testing.TB, expected int) *TestResponse

AssertStatus asserts the HTTP status code. Chainable.

func (*TestResponse) Body

func (tr *TestResponse) Body() string

Body returns the response body as a string.

func (*TestResponse) Close

func (tr *TestResponse) Close()

Close is a no-op (API consistency).

func (*TestResponse) JSON

func (tr *TestResponse) JSON(v any) error

JSON decodes the response body into v.

func (*TestResponse) Status

func (tr *TestResponse) Status() int

Status returns the HTTP status code.

type TimestampColumn

type TimestampColumn = entity.TimestampColumn

type TypedQuery

type TypedQuery[T any] = crud.TypedQuery[T]

type UCPConfig added in v0.10.0

type UCPConfig struct {
	ProtocolVersion string
	Services        []map[string]any
	Capabilities    []map[string]any
	Endpoints       []map[string]any
	// SpecURLs are advertised spec/schema URLs (the scanner expects them reachable).
	SpecURLs []string
}

UCPConfig configures /.well-known/ucp (ucp.dev).

type UUIDColumn

type UUIDColumn = entity.UUIDColumn

type ValidationError added in v0.18.0

type ValidationError = crud.ValidationError

type ValidationRegistry

type ValidationRegistry = entity.ValidationRegistry

type ValidatorFunc

type ValidatorFunc = entity.ValidatorFunc

type View

type View = migrate.View

type WebBotAuthConfig added in v0.10.0

type WebBotAuthConfig struct {
	// Keys is the JWK Set "keys" array — the site's public signing keys.
	Keys []map[string]any
}

WebBotAuthConfig configures /.well-known/http-message-signatures-directory. The site publishes its signing keys (a JWK Set) so receivers can verify the requests it sends as a bot/agent. (This is the publishing side, not RFC 9421 inbound verification.)

Directories

Path Synopsis
Package agentsinv is a process-wide registry of agent-onboarding snippets contributed by batteries and the framework root.
Package agentsinv is a process-wide registry of agent-onboarding snippets contributed by batteries and the framework root.
Package datexport is a process-wide registry of data-bearing tables that live OUTSIDE the framework entity registry — the physical tables a battery (auth sessions, the job queue, …) or an app creates with raw DDL.
Package datexport is a process-wide registry of data-bearing tables that live OUTSIDE the framework entity registry — the physical tables a battery (auth sessions, the job queue, …) or an app creates with raw DDL.
Package db holds shared low-level database abstractions used across the GoFastr framework subpackages.
Package db holds shared low-level database abstractions used across the GoFastr framework subpackages.
Package dev provides dev-mode-only helpers (livereload, debug surfaces).
Package dev provides dev-mode-only helpers (livereload, debug surfaces).
Package docs ships the framework's user-facing markdown docs as an embedded filesystem.
Package docs ships the framework's user-facing markdown docs as an embedded filesystem.
experimental
apiversions
Package apiversions provides first-class API versioning built on top of route groups.
Package apiversions provides first-class API versioning built on top of route groups.
Package factory provides Rails-style fixture / factory helpers for GoFastr tests and dev-time seeders.
Package factory provides Rails-style fixture / factory helpers for GoFastr tests and dev-time seeders.
Package fanout provides a Postgres-backed implementation of core/fanout.Fanout using LISTEN/NOTIFY.
Package fanout provides a Postgres-backed implementation of core/fanout.Fanout using LISTEN/NOTIFY.
Package harness is part of the GoFastr harness.
Package harness is part of the GoFastr harness.
client
Package client is part of the GoFastr harness.
Package client is part of the GoFastr harness.
client/tui
Package tui is part of the GoFastr harness.
Package tui is part of the GoFastr harness.
client/web
Package web is part of the GoFastr harness.
Package web is part of the GoFastr harness.
context
Package context is part of the GoFastr harness.
Package context is part of the GoFastr harness.
control
Package control is part of the GoFastr harness.
Package control is part of the GoFastr harness.
control/auth
Package auth implements the capability-token model: claim set, internal JWT-like encoding (no third-party dep), revocation list, and the issuance flow with TTY/notification confirmation.
Package auth implements the capability-token model: claim set, internal JWT-like encoding (no third-party dep), revocation list, and the issuance flow with TTY/notification confirmation.
control/conformance
Package conformance is the cross-transport parity test framework.
Package conformance is the cross-transport parity test framework.
control/inproc
Package inproc is part of the GoFastr harness.
Package inproc is part of the GoFastr harness.
control/mcpserver
Package mcpserver will expose the harness engine as an MCP server.
Package mcpserver will expose the harness engine as an MCP server.
control/multiplex
Package multiplex is part of the GoFastr harness.
Package multiplex is part of the GoFastr harness.
control/resources
Package resources is part of the GoFastr harness.
Package resources is part of the GoFastr harness.
control/rest
Package rest is part of the GoFastr harness.
Package rest is part of the GoFastr harness.
control/ws
Package ws will implement the WebSocket transport for the control plane.
Package ws will implement the WebSocket transport for the control plane.
engine
Package engine is part of the GoFastr harness.
Package engine is part of the GoFastr harness.
hook
Package hook is part of the GoFastr harness.
Package hook is part of the GoFastr harness.
ids
Package ids is part of the GoFastr harness.
Package ids is part of the GoFastr harness.
internal/clock
Package clock provides a swap-able clock for tests.
Package clock provides a swap-able clock for tests.
internal/ulid
Package ulid is part of the GoFastr harness.
Package ulid is part of the GoFastr harness.
logging
Package logging is part of the GoFastr harness.
Package logging is part of the GoFastr harness.
mcpclient
Package mcpclient implements the MCP client (consumer side) the harness uses to talk to external MCP servers.
Package mcpclient implements the MCP client (consumer side) the harness uses to talk to external MCP servers.
memory
Package memory is part of the GoFastr harness.
Package memory is part of the GoFastr harness.
plugin
Package plugin is part of the GoFastr harness.
Package plugin is part of the GoFastr harness.
profile
Package profile is part of the GoFastr harness.
Package profile is part of the GoFastr harness.
provider
Package provider is part of the GoFastr harness.
Package provider is part of the GoFastr harness.
provider/copilot
Package copilot implements the GitHub Copilot Provider.
Package copilot implements the GitHub Copilot Provider.
provider/credstore
Package credstore implements credential storage.
Package credstore implements credential storage.
provider/failover
Package failover composes a chain of Providers with a circuit-breaker per upstream.
Package failover composes a chain of Providers with a circuit-breaker per upstream.
provider/helper
Package helper is part of the GoFastr harness.
Package helper is part of the GoFastr harness.
provider/internal/openai
Package openai is an internal OpenAI-compatible adapter used by the OpenRouter and ZAI providers (both speak the same wire shape).
Package openai is an internal OpenAI-compatible adapter used by the OpenRouter and ZAI providers (both speak the same wire shape).
provider/openrouter
Package openrouter is part of the GoFastr harness.
Package openrouter is part of the GoFastr harness.
provider/routing
Package routing will implement RoutingProvider: a Provider that composes {router, executors[]} so a single turn can use a cheap model for routing and an expensive model for execution.
Package routing will implement RoutingProvider: a Provider that composes {router, executors[]} so a single turn can use a cheap model for routing and an expensive model for execution.
provider/zai
Package zai is part of the GoFastr harness.
Package zai is part of the GoFastr harness.
secrets
Package secrets locates and loads the repo-local .harness-secrets/env file.
Package secrets locates and loads the repo-local .harness-secrets/env file.
session
Package session is part of the GoFastr harness.
Package session is part of the GoFastr harness.
session/sqlite
Package sqlite is part of the GoFastr harness.
Package sqlite is part of the GoFastr harness.
skill
Package skill is part of the GoFastr harness.
Package skill is part of the GoFastr harness.
skill/skillmd
Package skillmd is part of the GoFastr harness.
Package skillmd is part of the GoFastr harness.
slash
Package slash is part of the GoFastr harness.
Package slash is part of the GoFastr harness.
tool
Package tool is part of the GoFastr harness.
Package tool is part of the GoFastr harness.
tool/builtins
Package builtins is part of the GoFastr harness.
Package builtins is part of the GoFastr harness.
tool/pack
Package pack is part of the GoFastr harness.
Package pack is part of the GoFastr harness.
tool/permission
Package permission is part of the GoFastr harness.
Package permission is part of the GoFastr harness.
tracing
Package tracing is part of the GoFastr harness.
Package tracing is part of the GoFastr harness.
Package i18nui provides translated default strings for framework UI surfaces.
Package i18nui provides translated default strings for framework UI surfaces.
Package image is a chainable image pipeline: decode → transform → encode, pure Go with only the standard library and golang.org/x/image as dependencies.
Package image is a chainable image pipeline: decode → transform → encode, pure Go with only the standard library and golang.org/x/image as dependencies.
internal/vp8l
Package vp8l implements a pure-Go VP8L (WebP lossless) encoder.
Package vp8l implements a pure-Go VP8L (WebP lossless) encoder.
internal
casing
Package casing holds snake_case <-> camelCase helpers used internally by the GoFastr framework.
Package casing holds snake_case <-> camelCase helpers used internally by the GoFastr framework.
testdb
Package testdb provides shared per-test database helpers used by the framework's internal tests AND by framework_test (external) tests that can't access package-private helpers.
Package testdb provides shared per-test database helpers used by the framework's internal tests AND by framework_test (external) tests that can't access package-private helpers.
Package isolation resolves worktree-specific local runtime resources.
Package isolation resolves worktree-specific local runtime resources.
Package lifecycle provides a documented, cooperative graceful-shutdown contract for GoFastr applications.
Package lifecycle provides a documented, cooperative graceful-shutdown contract for GoFastr applications.
Package outbox implements a transactional outbox for reliable event delivery to declared durable consumers.
Package outbox implements a transactional outbox for reliable event delivery to declared durable consumers.
Package owner provides a single seam for "who owns this row" lookups during CRUD operations.
Package owner provides a single seam for "who owns this row" lookups during CRUD operations.
Package pluginhost is the reusable, plugin-agnostic host glue for GoFastr heavy-JS plugins that run inside an opaque-origin sandboxed iframe.
Package pluginhost is the reusable, plugin-agnostic host glue for GoFastr heavy-JS plugins that run inside an opaque-origin sandboxed iframe.
Package routegroup provides the App-level route group abstraction.
Package routegroup provides the App-level route group abstraction.
Package static implements static-site generation for a framework.App with a UIHost mounted on it.
Package static implements static-site generation for a framework.App with a UIHost mounted on it.
Package testkit provides PUBLIC test helpers for host apps that use the GoFastr framework.
Package testkit provides PUBLIC test helpers for host apps that use the GoFastr framework.
ui
Package ui is the framework's opinionated component layer on top of core-ui.
Package ui is the framework's opinionated component layer on top of core-ui.
theme
Package theme is the canonical home for the framework's visual design system.
Package theme is the canonical home for the framework's visual design system.
Package uihost wires a core-ui application onto a framework.App's router.
Package uihost wires a core-ui application onto a framework.App's router.

Jump to

Keyboard shortcuts

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