gas

package module
v0.5.6 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 29 Imported by: 0

README

Gas

Test Go Reference Go Version License

Gas is a modular Go framework for building micro-SaaS applications. It provides the plumbing every product needs (dependency injection, routing, middleware, events, migrations, and service lifecycle management) so you can spend your time on business logic instead of rebuilding the same wiring for each project.

📚 Documentation · Getting started · Guides · API reference

Quick start

go get github.com/gasmod/gas
package main

import (
	"log"
	"net/http"

	"github.com/gasmod/gas"
)

func main() {
	app := gas.NewApp()

	app.Router().Handle(nil, http.MethodGet, "/", func(ctx gas.Context) error {
		return ctx.Text(http.StatusOK, "Hello, World!")
	})

	if err := app.Run(); err != nil {
		log.Fatal(err)
	}
}
go run .
# listening on 0.0.0.0:8080

That is a complete Gas app. From there you write services and register modules the same way, letting the DI container wire them together.

Modules

This repository is a monorepo. github.com/gasmod/gas is the core package, and each official module lives beside it as a separate Go module, so go get pulls in only what you use.

Module Import path What it gives you
core github.com/gasmod/gas App and Worker lifecycle, DI container, router, middleware, events, provider interfaces
auth github.com/gasmod/gas/auth JWT, server-side sessions, API keys, and single-use tokens
cache github.com/gasmod/gas/cache Key-value caching, in-memory or Valkey (Redis-compatible)
config github.com/gasmod/gas/config Config from env vars, JSON, .env, and AWS Secrets Manager, bound to structs
database github.com/gasmod/gas/database database/sql and native pgx pools, transaction helpers, sqlc-friendly
email github.com/gasmod/gas/email Transactional email over AWS SES, including templated sends
log github.com/gasmod/gas/log Structured logging (zerolog or slog) and HTTP/OTLP log shipping
migrate github.com/gasmod/gas/migrate Service-owned database migrations with dirty-state detection and rollback
queue github.com/gasmod/gas/queue Job queues backed by AWS SQS
storage github.com/gasmod/gas/storage Object storage on S3 and S3-compatible services
template github.com/gasmod/gas/template Template storage: memory, directory, embedded FS, database, or composite
ui github.com/gasmod/gas/ui HTML rendering with layouts and partials, static files, HTMX fragments

No module imports another module's service package. They meet through provider interfaces declared in core, so any of them can be replaced by your own implementation or a test mock without the others noticing.

Versioning

All modules share a single version and are released together. A release tags the root module vX.Y.Z and each submodule <dir>/vX.Y.Z, which is what go get github.com/gasmod/gas/auth@vX.Y.Z resolves against. Mixing versions across modules is not supported, so upgrade them together.

Gas is pre-1.0: minor versions may contain breaking changes. See the CHANGELOG.

Examples

Five runnable applications live in example/, from a bare hello world up to a full API server using auth, database, storage, queue, email, and cache together.

cd example/hello-world && go run ./cmd

Documentation

Where What
gasmod.github.io/gas Guides, concepts, and getting started
pkg.go.dev API reference for every package
example/ Runnable applications

Documentation lives in one place per kind, on purpose. Guides and concepts are on the site, API reference comes from doc comments on pkg.go.dev, and each module's README is a signpost to both. Code blocks on the site are imported from a Go module that compiles in CI, so they cannot drift from the framework.

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md for the workflow: conventional commits, signed-off commits (DCO), tests with every change, and make lint.

make test-all   # go test -race in every module, inside the Docker test container
make lint-all   # golangci-lint in every module
make build-all  # go build in every module

A go.work file at the root ties the modules together, so local changes to one module are picked up by the others without a replace directive of your own.

Community and policies

License

MIT © Ahmed Kamal

Documentation

Overview

Package gas is the core of the Gas framework: the DI container, the Worker and App lifecycles, the Router with service ownership, the EventBus, request scopes, the error model, the runtime kill switch, the provider interfaces (Logger, CacheProvider, ...) that other gasmod modules implement, and the Principal that authentication middleware stores on the request context.

Wiring

NewWorker registers the *EventBus and the Worker itself, as HealthProvider and ReadyProvider, in its container; NewApp also registers its *Router. A type that implements Service has Init(ctx) called after construction and Close(ctx) at shutdown (singletons) or scope end (scoped); a type that declares Init or Close with any other signature is rejected at startup.

Errors from dependency resolution wrap one of the sentinels ErrNotRegistered, ErrCircularDependency, ErrCaptiveDependency, ErrScopeRequired, ErrNoConstructor, ErrInvalidLifetime, and ErrInvalidServiceShape, so callers classify them with errors.Is.

The container is safe for concurrent resolution. A singleton is built by one goroutine at a time: other goroutines asking for it while it is built wait and get the same instance, so its constructor and Init run once. Besides the cycles BuildAll finds in constructor parameters, resolution catches a constructor or Init that resolves the type being built through a captured container or scope, and two builds on different goroutines that would wait on each other; both return ErrCircularDependency naming the types. To tell a re-entrant resolution from a concurrent one, the container reads the calling goroutine's ID from runtime.Stack, once per constructor call and once per wait.

Lifecycle

Every lifecycle method takes the caller's context: Worker.Start, InitServices, Shutdown, CloseService and RestartService, App.Start and App.Stop, ServiceContainer.BuildAll and NewScope, and Scope.Close. Run uses context.Background().

Each singleton's Init gets the caller's context plus an init deadline, 15 seconds by default (see WithServiceInitTimeout, WithServiceContainerServiceTimeout, and ServiceInitTimeout; 0 disables it). When the deadline passes, the container cancels Init's context and waits for Init to return, then fails with an error wrapping context.DeadlineExceeded, so no Init is left running behind a failed start. An Init that ignores the cancellation and returns nil late is closed again. Init's context is canceled when Init returns, so background work a service starts in Init must not derive from it. Scoped services get the scope's context with no added deadline; in an App that is the request context, and Close gets it without its cancellation, so scoped services clean up even after the client has gone.

A Start that fails closes, in reverse order, every service it had initialized, with its context stripped of cancellation, and returns the startup error joined with any Close errors. It leaves nothing running, and the Worker cannot be started again: later calls to Start, and to InitServices after a failed InitServices, return the same error.

Worker.Shutdown emits SystemShuttingDown and closes every service still active, in reverse initialization order, passing its context to Close. Services already closed by CloseService are skipped. A Close error does not stop the rest; all of them are returned, joined. Shutdown runs once, and later calls return nil. Once it has begun, CloseService and RestartService return an error. App.Stop first emits SystemServerShuttingDown and shuts the http.Server down within ServerSettings.ShutdownTimeout, or sooner if its context ends.

See the module README for usage examples and design rationale.

SPDX-License-Identifier: MIT

Index

Constants

View Source
const (
	CodeBadRequest       = "bad_request"
	CodeInvalidJSON      = "invalid_json"
	CodeInvalidForm      = "invalid_form"
	CodeUnauthorized     = "unauthorized"
	CodeForbidden        = "forbidden"
	CodeNotFound         = "not_found"
	CodeConflict         = "conflict"
	CodeValidationFailed = "validation_failed"
	CodeRateLimited      = "rate_limited"
	CodeInternal         = "internal_error"
	CodeUnavailable      = "service_unavailable"
	CodePayloadTooLarge  = "payload_too_large"
)

Error codes written by Gas core. Applications are free to define their own and pass them to NewError.

Variables

View Source
var (
	// ErrNotRegistered reports that a type, or a dependency of one, has no
	// registration or instance in the container.
	ErrNotRegistered = errors.New("gas: service not registered")

	// ErrCircularDependency reports that the registered constructors depend
	// on each other in a cycle, so no build order exists. It is also returned
	// when a constructor or Init resolves the type being built through a
	// captured container or scope, when two goroutines' singleton builds
	// would wait on each other, and when constructors nest more than 256 deep.
	ErrCircularDependency = errors.New("gas: circular dependency")

	// ErrCaptiveDependency reports a singleton whose constructor depends on a
	// scoped or transient service, which the singleton would hold for the
	// life of the process.
	ErrCaptiveDependency = errors.New("gas: captive dependency")

	// ErrScopeRequired reports a scoped service resolved from the container
	// itself; it can only be resolved from a Scope.
	ErrScopeRequired = errors.New("gas: scoped service resolved outside a scope")

	// ErrNoConstructor reports that the container was asked to construct a
	// type it has no constructor for. It guards an internal invariant: every
	// public path checks the registration first, so the public API does not
	// return it today.
	ErrNoConstructor = errors.New("gas: no constructor")

	// ErrInvalidLifetime reports a registration whose ServiceLifetime is not
	// Singleton, Scoped, or Transient.
	ErrInvalidLifetime = errors.New("gas: invalid service lifetime")

	// ErrInvalidServiceShape reports a registered type that declares Init or
	// Close but does not implement Service.
	ErrInvalidServiceShape = errors.New("gas: invalid service shape")
)

Sentinel errors for dependency resolution. Errors from the container wrap one of these, so callers can classify a failure with errors.Is while the message still names the types involved.

Functions

func ApplyEnqueueOptions

func ApplyEnqueueOptions(
	opts []EnqueueOption,
) (delay time.Duration, groupID, dedupeID string, attrs map[string]string)

ApplyEnqueueOptions resolves variadic EnqueueOption values into a concrete enqueueOptions struct. Implementations call this inside their Enqueue method.

func CacheControl

func CacheControl(opt ...CacheControlOption) func(next http.Handler) http.Handler

CacheControl is middleware that sets the Cache-Control response header based on path matching rules and configured directives. If no path filters are specified, the header is applied to all requests. If no directives are specified, the middleware passes through without setting any header.

func MetadataValue

func MetadataValue[T any](m PrincipalMetadata, key string) (T, bool)

MetadataValue is a type-safe helper that retrieves a typed value from PrincipalMetadata. It returns the value and true if the key exists and the type assertion succeeds, or the zero value and false otherwise.

func MustResolveFromRequestScope

func MustResolveFromRequestScope[T any](r *http.Request) T

MustResolveFromRequestScope retrieves a service of type T from the request's Scope and panics if it cannot be resolved.

func NewNopLogger

func NewNopLogger() func() (*NopLogger, error)

NewNopLogger returns a constructor that constructs a nop-logger implementing the Logger interface, in the CONVENTIONS.md shape. It never fails. Code that needs a Logger value directly, such as a test, can use &NopLogger{}.

func RequestLogger

func RequestLogger[T Logger](opt ...RequestLoggerOption) func(next http.Handler) http.Handler

RequestLogger is middleware that logs HTTP requests and responses. It resolves T from the request's DI scope, derives a per-request logger from it with With, and stores that logger on the request context with WithLogger, so handlers and later middleware log through it with LoggerFromContext. The resolved logger itself is never changed, so a singleton shared by every request is safe.

When T is registered scoped (the usual pattern: a request-logger type built from the singleton gas.Logger with With), the per-request logger also replaces T's instance in the request scope, so handlers and services that take T as a dependency later in the request get it, request fields included. A singleton T is not shadowed, and a scoped T whose instance is a Service is not replaced, so the scope still closes it. When the request finishes it logs method, path, status, bytes written, duration, and remote address: at error level for a status >= 400, at info level otherwise.

The type parameter T must be a Logger type registered in the DI container. If it cannot be resolved, the middleware passes through silently.

When appendRequestID is enabled (the default), the per-request logger carries a request_id field, and the middleware expects chi's RequestID middleware to be mounted upstream so that a request ID is available via middleware.GetReqID.

func ResolveFromRequestScope

func ResolveFromRequestScope[T any](r *http.Request) (T, error)

ResolveFromRequestScope retrieves or builds a service of type T from the per-request scope in the provided *http.Request.

func SecurityHeaders

func SecurityHeaders(opt ...SecurityHeadersOption) func(next http.Handler) http.Handler

SecurityHeaders is middleware that sets common security-related HTTP response headers. It applies secure defaults out of the box (nosniff, DENY framing, strict referrer policy, and restrictive permissions policy). Use the functional options to override individual header values, or pass an empty string to disable a specific header.

func WantsJSON added in v0.3.5

func WantsJSON(r *http.Request) bool

WantsJSON reports whether the client prefers a JSON error body. A request that explicitly asks for text/html without also accepting application/json gets plain text; everything else, including an absent or */* Accept header, gets JSON.

Quality values are ignored: the rule is media-type token presence only.

func WithLogger

func WithLogger(ctx context.Context, logger Logger) context.Context

WithLogger returns a copy of ctx with the given Logger attached.

func WithPrincipal

func WithPrincipal(ctx context.Context, principal Principal) context.Context

WithPrincipal stores a Principal in the given context.

func WithRequestScope

func WithRequestScope(ctx context.Context, scope *Scope) context.Context

WithRequestScope adds a Scope instance to the context using a custom key for managing scoped service lifetimes. Useful for testing and managing scoped service lifetimes within request contexts.

func WriteError added in v0.3.5

func WriteError(w http.ResponseWriter, r *http.Request, err error) error

WriteError renders err as the unified error response, negotiating JSON or plain text via the Accept header. Values that are not, and do not wrap, an *Error render as a canonical 500 with the original discarded from the response.

WriteError does not log; the caller decides that. It reads only w and r, so it is safe to call from middleware that runs before the request scope exists.

Types

type App

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

App manages service lifecycle, the HTTP server, and graceful shutdown. It embeds a Worker for DI, events, migrations, and service management, and adds routing, CSRF protection, and an HTTP listener on top.

func NewApp

func NewApp(opts ...Option) *App

NewApp creates an App with the given options. Router and EventBus are created internally and registered in the container.

func (*App) Config

func (a *App) Config() *Config

Config retrieves the application's configuration.

func (*App) Handler

func (a *App) Handler() http.Handler

Handler returns the fully wrapped http.Handler used by the App's server (router behind CSRF protection). Useful for embedding the App in tests or in an externally managed listener.

func (*App) Router

func (a *App) Router() *Router

Router returns the App's router.

func (*App) Run

func (a *App) Run() error

Run initializes all services, runs pending migrations, starts the HTTP server, and blocks until a shutdown signal is received. Startup and shutdown use context.Background(). If the server fails, Run stops the App and returns the server error joined with any error from Stop.

func (*App) Serve

func (a *App) Serve() error

Serve starts the HTTP server and blocks until it stops. A clean shutdown (http.ErrServerClosed) returns nil; any other listener error is returned.

func (*App) Server

func (a *App) Server() *http.Server

Server returns the App's *http.Server, constructing it on first call from the current Config. The instance is cached and safe to call concurrently.

func (*App) Start

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

Start initializes the underlying Worker (services, migrations, hooks) and binds configuration. It does not start the HTTP server; call Serve for that, or use Run to do both and block until shutdown. ctx is passed to the Init of every service built during startup.

Like Worker.Start, a Start that fails, including when the ConfigProvider cannot bind the config (Bind also checks its validate tags), closes every service it had initialized and returns the error; every later Start returns the same error.

func (*App) Stop

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

Stop emits the server-shutting-down event, gracefully shuts down the HTTP server within ShutdownTimeout (or sooner, if ctx ends first), and then shuts down the underlying Worker, passing ctx to each service's Close.

type AppConfigUpdated

type AppConfigUpdated struct {
	Event[AppConfigUpdatedPayload]
}

AppConfigUpdated is emitted when the app config is updated.

type AppConfigUpdatedPayload

type AppConfigUpdatedPayload struct {
	Config Config
}

AppConfigUpdatedPayload carries the updated config.

type AppOption

type AppOption func(*App)

AppOption configures HTTP-specific aspects of an App (error handler, CSRF, trusted origins). Only NewApp accepts AppOption values; passing one to NewWorker panics.

func WithCSRFDenyHandler

func WithCSRFDenyHandler(h http.Handler) AppOption

WithCSRFDenyHandler sets the handler invoked when a cross-origin request is rejected by CSRF protection. The default handler returns 403 Forbidden.

func WithCSRFInsecureBypassPattern

func WithCSRFInsecureBypassPattern(pattern string) AppOption

WithCSRFInsecureBypassPattern adds a URL path pattern that bypasses CSRF cross-origin protection. Use only for endpoints that require unauthenticated cross-origin access and implement their own request validation (e.g. webhook receivers).

func WithErrorHandler

func WithErrorHandler(h ErrorHandler) AppOption

WithErrorHandler configures the function that converts DI-aware handler errors into HTTP responses.

func WithTrustedOrigin

func WithTrustedOrigin(origin string) AppOption

WithTrustedOrigin adds an origin that is permitted to make cross-origin non-safe requests (POST, PUT, PATCH, DELETE, etc.). The origin must be an absolute URL with a scheme and host, e.g. "https://app.example.com". Panics if the origin is not a valid absolute URL.

type BasePrincipalMetadata

type BasePrincipalMetadata map[string]any

BasePrincipalMetadata is a map-based implementation of PrincipalMetadata.

func (BasePrincipalMetadata) Value

func (m BasePrincipalMetadata) Value(key string) any

Value returns the metadata value for the given key, or nil if not present.

type BindOption added in v0.5.5

type BindOption func(*BindOptions)

BindOption changes the BindOptions of one Bind call.

type BindOptions added in v0.5.5

type BindOptions struct {
	// Validate runs struct-tag validation after binding. It defaults to true.
	Validate bool
}

BindOptions holds the settings for one ConfigProvider.Bind call.

type CacheControlOption

type CacheControlOption func(*CacheControlOptions)

CacheControlOption is a functional option for configuring CacheControl.

func WithCacheControlImmutable

func WithCacheControlImmutable() CacheControlOption

WithCacheControlImmutable appends the "immutable" directive.

func WithCacheControlMaxAge

func WithCacheControlMaxAge(val time.Duration) CacheControlOption

WithCacheControlMaxAge appends a "max-age" directive with the given duration.

func WithCacheControlMustRevalidate

func WithCacheControlMustRevalidate() CacheControlOption

WithCacheControlMustRevalidate appends the "must-revalidate" directive.

func WithCacheControlMustUnderstand

func WithCacheControlMustUnderstand() CacheControlOption

WithCacheControlMustUnderstand appends the "must-understand" directive.

func WithCacheControlNoCache

func WithCacheControlNoCache() CacheControlOption

WithCacheControlNoCache appends the "no-cache" directive.

func WithCacheControlNoStore

func WithCacheControlNoStore() CacheControlOption

WithCacheControlNoStore appends the "no-store" directive.

func WithCacheControlNoTransform

func WithCacheControlNoTransform() CacheControlOption

WithCacheControlNoTransform appends the "no-transform" directive.

func WithCacheControlPath

func WithCacheControlPath(val string) CacheControlOption

WithCacheControlPath adds an exact path that should receive the Cache-Control header.

func WithCacheControlPathPrefix

func WithCacheControlPathPrefix(val string) CacheControlOption

WithCacheControlPathPrefix adds a path prefix to match against. Any request whose path starts with this prefix will receive the Cache-Control header.

func WithCacheControlPathPrefixes

func WithCacheControlPathPrefixes(val []string) CacheControlOption

WithCacheControlPathPrefixes adds multiple path prefixes to match against.

func WithCacheControlPathSuffix

func WithCacheControlPathSuffix(val string) CacheControlOption

WithCacheControlPathSuffix adds a path suffix to match against (e.g., ".css", ".js"). Any request whose path ends with this suffix will receive the Cache-Control header.

func WithCacheControlPathSuffixes

func WithCacheControlPathSuffixes(val []string) CacheControlOption

WithCacheControlPathSuffixes adds multiple path suffixes to match against.

func WithCacheControlPaths

func WithCacheControlPaths(val []string) CacheControlOption

WithCacheControlPaths adds multiple exact paths that should receive the Cache-Control header.

func WithCacheControlPrivate

func WithCacheControlPrivate() CacheControlOption

WithCacheControlPrivate appends the "private" directive.

func WithCacheControlProxyRevalidate

func WithCacheControlProxyRevalidate() CacheControlOption

WithCacheControlProxyRevalidate appends the "proxy-revalidate" directive.

func WithCacheControlPublic

func WithCacheControlPublic() CacheControlOption

WithCacheControlPublic appends the "public" directive.

func WithCacheControlSMaxAge

func WithCacheControlSMaxAge(val time.Duration) CacheControlOption

WithCacheControlSMaxAge appends an "s-maxage" directive (shared/CDN cache max age).

func WithCacheControlStaleIfError

func WithCacheControlStaleIfError(val time.Duration) CacheControlOption

WithCacheControlStaleIfError appends a "stale-if-error" directive.

func WithCacheControlStaleWhileRevalidate

func WithCacheControlStaleWhileRevalidate(val time.Duration) CacheControlOption

WithCacheControlStaleWhileRevalidate appends a "stale-while-revalidate" directive.

type CacheControlOptions

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

CacheControlOptions configures the behavior of the CacheControl middleware.

type CacheProvider

type CacheProvider interface {
	Get(ctx context.Context, key string) ([]byte, error)
	Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
	Delete(ctx context.Context, key string) error
	Exists(ctx context.Context, key string) (bool, error)
}

CacheProvider abstracts key-value caching. Implemented by in-memory, Redis, Valkey, or any other cache service.

type Config

type Config struct {
	// Embedded GasEnv
	WithGasEnv

	Server ServerSettings
}

Config holds server-level configuration passed from the host server to the App. Sensible defaults are applied via DefaultConfig().

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a Config with sensible defaults.

type ConfigProvider

type ConfigProvider interface {
	SetDefault(key string, value any)
	SetDefaults(values any) error
	Set(key string, value any)
	// Bind fills dest from the configuration and then validates it against
	// its validate struct tags, unless an option turns validation off.
	Bind(dest any, options ...BindOption) error
	Get(key string) any
	Find(key string) (value any, exist bool)
	Values() map[string]any
}

ConfigProvider is the configuration API services depend on: reading values, setting defaults, and binding into a struct. The config module's *Config implements it. Loading sources is setup work the app does on the concrete config before registering it, so it is not part of this interface.

type Context

type Context interface {
	context.Context
	// ResponseWriter returns the underlying http.ResponseWriter.
	ResponseWriter() http.ResponseWriter
	// Request returns the underlying *http.Request.
	Request() *http.Request
	// JSON serializes v as JSON and writes it with the given status code.
	JSON(status int, v any) error
	// XML serializes v as XML and writes it with the given status code.
	// Uses content type "application/xml; charset=utf-8".
	XML(status int, v any) error
	// RSS serializes v as XML with content type "application/rss+xml; charset=utf-8".
	RSS(status int, v any) error
	// HTML writes an HTML response with the given status code and content string.
	HTML(status int, s string) error
	// Text writes a plain-text response with the given status code.
	Text(status int, s string) error
	// NoContent writes a 204 No Content response.
	NoContent() error

	// WriteError writes err as the unified error response, negotiating JSON or
	// plain text the same way the default ErrorHandler does. Values that are
	// not, and do not wrap, an *Error render as a canonical 500.
	WriteError(err error) error
	// WriteErrorJSON writes the JSON error envelope regardless of the Accept
	// header. Use it for JSON API routes in an app whose ErrorHandler renders
	// HTML.
	WriteErrorJSON(err error) error

	// BadRequest returns a 400 *Error with code CodeBadRequest.
	BadRequest(msg string) error
	// Unauthorized returns a 401 *Error with code CodeUnauthorized.
	Unauthorized(msg string) error
	// Forbidden returns a 403 *Error with code CodeForbidden.
	Forbidden(msg string) error
	// NotFound returns a 404 *Error with code CodeNotFound.
	NotFound(msg string) error
	// Conflict returns a 409 *Error with code CodeConflict.
	Conflict(msg string) error
	// Unprocessable returns a 422 *Error with code CodeValidationFailed.
	Unprocessable(msg string) error
	// TooManyRequests returns a 429 *Error with code CodeRateLimited.
	TooManyRequests(msg string) error
	// Internal returns a 500 *Error with code CodeInternal.
	Internal(msg string) error
	// ServiceUnavailable returns a 503 *Error with code CodeUnavailable.
	ServiceUnavailable(msg string) error

	// BadRequestErr returns a 400 *Error with code CodeBadRequest, caused by err.
	BadRequestErr(err error, msg string) error
	// UnauthorizedErr returns a 401 *Error with code CodeUnauthorized, caused by err.
	UnauthorizedErr(err error, msg string) error
	// ForbiddenErr returns a 403 *Error with code CodeForbidden, caused by err.
	ForbiddenErr(err error, msg string) error
	// NotFoundErr returns a 404 *Error with code CodeNotFound, caused by err.
	NotFoundErr(err error, msg string) error
	// ConflictErr returns a 409 *Error with code CodeConflict, caused by err.
	ConflictErr(err error, msg string) error
	// UnprocessableErr returns a 422 *Error with code CodeValidationFailed,
	// caused by err.
	UnprocessableErr(err error, msg string) error
	// TooManyRequestsErr returns a 429 *Error with code CodeRateLimited,
	// caused by err.
	TooManyRequestsErr(err error, msg string) error
	// InternalErr returns a 500 *Error with code CodeInternal, caused by err.
	InternalErr(err error, msg string) error
	// ServiceUnavailableErr returns a 503 *Error with code CodeUnavailable,
	// caused by err.
	ServiceUnavailableErr(err error, msg string) error

	// Redirect sends an HTTP redirect to the given URL with the given status code.
	Redirect(status int, url string)
	// Param returns the URL parameter value by name (chi.URLParam).
	Param(key string) string
	// Query returns the query string parameter value by name.
	Query(key string) string
	// Header returns the request header value by name.
	Header(key string) string
	// SetHeader sets a response header.
	SetHeader(key, value string)

	// BindJSON decodes the request body as JSON into dest and performs automatic validation
	// using the configured validator.
	BindJSON(dest any) error
	// BindForm binds form data from the HTTP request to the provided destination object
	// and performs automatic validation using the configured validator.
	BindForm(dest any) error
	// Validator returns the *validator.Validate used for request validation,
	// building the package default on first use if none was supplied.
	Validator() *validator.Validate
	// FormDecoder returns the *schema.Decoder used to decode form data into
	// structs, building the package default on first use if none was supplied.
	FormDecoder() *schema.Decoder

	// Principal returns the authenticated Principal attached to the request,
	// or a 401 *Error with code CodeUnauthorized when none is present.
	Principal() (Principal, error)
}

Context is the first parameter of every DI-aware handler. It wraps the HTTP response writer and request into a single value. The per-request scope is accessible via RequestScope(c.Request()) — the adapter resolves dependencies automatically, so handlers rarely need to access the scope directly.

func NewContext

func NewContext(
	parent context.Context,
	w http.ResponseWriter,
	r *http.Request,
	opts ...ContextOption,
) Context

NewContext creates a Context from the standard HTTP pair. It panics if parent, w, or r is nil.

The returned Context is installed as the request's own context, so Request().Context() and the Context itself are the same value.

type ContextOption

type ContextOption func(*reqContext)

ContextOption is a functional option used to modify or extend the behavior of a reqContext at creation time.

func WithFormDecoder

func WithFormDecoder(d *schema.Decoder) ContextOption

WithFormDecoder sets a custom form decoder for the reqContext using the provided *schema.Decoder instance.

func WithMaxJSONBodyBytes added in v0.5.5

func WithMaxJSONBodyBytes(n int64) ContextOption

WithMaxJSONBodyBytes sets how many bytes of the request body BindJSON reads before it fails with a 413 *Error. Zero or a negative n means no limit. Without this option the limit is 1 MiB.

func WithValidate

func WithValidate(v *validator.Validate) ContextOption

WithValidate returns a ContextOption that sets the provided *validator.Validate instance to the reqContext.

type DatabaseProvider

type DatabaseProvider interface {
	DB() *sql.DB
	Driver() string
	Ping(ctx context.Context) error
	Query(ctx context.Context, query string, args ...any) (Rows, error)
	Exec(ctx context.Context, query string, args ...any) (Result, error)
	BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
	WithTx(ctx context.Context, opts *sql.TxOptions, fn func(*sql.Tx) error) (err error)
}

DatabaseProvider abstracts database access. Implemented by gas/database or any other database service. DB() exposes the underlying *sql.DB so that sqlc-generated code and transactions can use it directly.

type Email

type Email struct {
	From     string
	ReplyTo  string
	Subject  string
	TextBody string
	HTMLBody string

	Headers map[string]string

	To  []string
	Cc  []string
	Bcc []string
}

Email represents an email message.

type EmailProvider

type EmailProvider interface {
	Send(ctx context.Context, msg *Email) error
	SendFromTemplate(ctx context.Context, msg *TemplatedEmail) error
}

EmailProvider abstracts email sending.

type EnqueueOption

type EnqueueOption func(*enqueueOptions)

EnqueueOption configures an Enqueue call.

func WithDedupeID

func WithDedupeID(id string) EnqueueOption

WithDedupeID sets a deduplication identifier.

func WithDelay

func WithDelay(d time.Duration) EnqueueOption

WithDelay sets an initial delay before the job becomes visible to consumers.

func WithGroupID

func WithGroupID(id string) EnqueueOption

WithGroupID sets the message group for FIFO queue ordering.

func WithJobAttributes

func WithJobAttributes(attrs map[string]string) EnqueueOption

WithJobAttributes attaches provider-specific key-value metadata to the job.

type Environment added in v0.5.5

type Environment string

Environment is an application environment such as development or production. Values are case-sensitive. The config module's gasenv extension resolves it at load time and stores it under the GasEnv key, so a configuration struct that embeds WithGasEnv gets it when bound:

type AppConfig struct {
	gas.WithGasEnv
	Database DatabaseConfig
}

var c AppConfig
if err := cfg.Bind(&c); err != nil {
	return err
}
if c.GasEnv.IsProduction() {
	// production-only setup
}
const (
	// EnvDevelopment is local development, typically with debug logging and
	// development tools. It is the gasenv extension's fallback.
	EnvDevelopment Environment = "development"

	// EnvTesting is automated and integration testing.
	EnvTesting Environment = "testing"

	// EnvStaging is a production-like environment for validation before a
	// release.
	EnvStaging Environment = "staging"

	// EnvProduction serves real users.
	EnvProduction Environment = "production"
)

The environments the gasenv extension allows by default.

func (Environment) IsDevelopment added in v0.5.5

func (e Environment) IsDevelopment() bool

IsDevelopment reports whether e is EnvDevelopment.

func (Environment) IsDevelopmentLike added in v0.5.5

func (e Environment) IsDevelopmentLike() bool

IsDevelopmentLike reports whether e is EnvDevelopment or EnvTesting.

func (Environment) IsProduction added in v0.5.5

func (e Environment) IsProduction() bool

IsProduction reports whether e is EnvProduction.

func (Environment) IsProductionLike added in v0.5.5

func (e Environment) IsProductionLike() bool

IsProductionLike reports whether e is EnvProduction or EnvStaging.

func (Environment) IsStaging added in v0.5.5

func (e Environment) IsStaging() bool

IsStaging reports whether e is EnvStaging.

func (Environment) IsTesting added in v0.5.5

func (e Environment) IsTesting() bool

IsTesting reports whether e is EnvTesting.

func (Environment) String added in v0.5.5

func (e Environment) String() string

String returns e as a plain string.

type Error added in v0.3.5

type Error struct {
	Status  int            `json:"status"`
	Code    string         `json:"code"`
	Message string         `json:"message"`
	Fields  []FieldError   `json:"fields,omitempty"`
	Details map[string]any `json:"details,omitempty"`
	// contains filtered or unexported fields
}

Error is the unified error shape for Gas handlers. Return it from a DI-aware handler and the ErrorHandler renders it; see WriteError for the wire format.

Status is serialized into the JSON body alongside the HTTP status line. WriteError writes the normalized status to both, so the two never disagree.

The wrapped cause is unexported and never serialized. It reaches logs and errors.Is / errors.As, never a client.

Field order is the JSON key order of the response body, so it is fixed by the wire contract rather than by memory layout.

func AsError added in v0.3.5

func AsError(err error) (*Error, bool)

AsError reports whether err is, or wraps, an *Error and returns it.

func BadRequest added in v0.3.5

func BadRequest(message string) *Error

BadRequest returns a 400 Error with code CodeBadRequest.

func Conflict added in v0.3.5

func Conflict(message string) *Error

Conflict returns a 409 Error with code CodeConflict.

func Forbidden added in v0.3.5

func Forbidden(message string) *Error

Forbidden returns a 403 Error with code CodeForbidden.

func Internal added in v0.3.5

func Internal(message string) *Error

Internal returns a 500 Error with code CodeInternal.

func NewError added in v0.3.5

func NewError(status int, code, message string) *Error

NewError builds an Error with an explicit status, code, and message.

func NotFound added in v0.3.5

func NotFound(message string) *Error

NotFound returns a 404 Error with code CodeNotFound.

func ServiceUnavailable added in v0.3.5

func ServiceUnavailable(message string) *Error

ServiceUnavailable returns a 503 Error with code CodeUnavailable.

func TooManyRequests added in v0.3.5

func TooManyRequests(message string) *Error

TooManyRequests returns a 429 Error with code CodeRateLimited.

func Unauthorized added in v0.3.5

func Unauthorized(message string) *Error

Unauthorized returns a 401 Error with code CodeUnauthorized.

func Unprocessable added in v0.3.5

func Unprocessable(message string) *Error

Unprocessable returns a 422 Error with code CodeValidationFailed.

func (*Error) Error added in v0.3.5

func (e *Error) Error() string

Error implements the error interface. The wrapped cause is included so that logs carry the full chain.

func (*Error) Unwrap added in v0.3.5

func (e *Error) Unwrap() error

Unwrap returns the wrapped cause, so errors.Is and errors.As see through it.

func (*Error) WithCause added in v0.3.5

func (e *Error) WithCause(err error) *Error

WithCause attaches an underlying cause. The cause is logged and is reachable through errors.Is and errors.As, but is never written to a response.

func (*Error) WithDetail added in v0.3.5

func (e *Error) WithDetail(key string, val any) *Error

WithDetail attaches an arbitrary key/value pair to the response payload.

func (*Error) WithField added in v0.3.5

func (e *Error) WithField(field, rule, message string) *Error

WithField appends a field-level validation failure.

type ErrorHandler

type ErrorHandler func(ctx Context, err error)

ErrorHandler converts a handler error into an HTTP response.

type ErrorResponse added in v0.3.5

type ErrorResponse struct {
	Error *Error `json:"error"`
}

ErrorResponse is the wire shape of every error response Gas writes. It is exported so Go clients and tests can decode it.

type Event

type Event[D any] struct{}

Event is embedded in an event type to declare the payload that event carries. An event is a type, not a value:

type OrderPlaced struct{ gas.Event[OrderPlacedPayload] }

Emit, Subscribe, and SubscribeWithOwner take that type as their only explicit type argument and infer the payload from it, so a handler registered for OrderPlaced is bound at compile time to OrderPlacedPayload. Subscriptions are keyed by the event type itself, so two events never collide, even when their payload types are identical.

type EventBus

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

EventBus is a publish/subscribe message bus with service ownership tracking. Subscriptions are keyed by event type; those registered through SubscribeWithOwner are additionally tagged with the owning service, so the kill switch can drop them as a group when that service closes.

All methods are safe for concurrent use.

func NewEventBus

func NewEventBus() *EventBus

NewEventBus creates an empty EventBus.

func (*EventBus) Close added in v0.5.0

func (bus *EventBus) Close(context.Context) error

Close is a no-op; subscriptions are removed through RemoveByService.

func (*EventBus) Emit

func (bus *EventBus) Emit[E event[D], D any](data D) *sync.WaitGroup

Emit dispatches data to every subscriber of event type E, each on its own goroutine, and returns a WaitGroup that completes once they all return:

bus.Emit[OrderPlaced](OrderPlacedPayload{ID: id}).Wait()

Wait on it when handlers must finish before the caller continues. The returned WaitGroup is never nil, and emitting an event nobody subscribes to is a no-op. Emit holds the bus lock only long enough to snapshot the subscriber list; handlers run without it, so a handler may subscribe or emit in turn.

A handler that panics is recovered, so it neither takes the process down nor keeps the WaitGroup from completing, and the other handlers still run. The stack goes to stderr and, on a Worker's bus, the panic is logged with the event type and the owning service.

func (*EventBus) Init added in v0.5.0

func (bus *EventBus) Init(context.Context) error

Init is a no-op. It exists so the EventBus satisfies Service and can be passed as an owner.

func (*EventBus) Name added in v0.5.0

func (bus *EventBus) Name() string

Name returns the service name of the event bus.

func (*EventBus) RemoveByService added in v0.3.5

func (bus *EventBus) RemoveByService(service Service)

RemoveByService removes every subscription that service registered through SubscribeWithOwner, across all event types, and drops any event whose last subscriber is gone. Subscriptions made with Subscribe have no owner and are left in place.

func (*EventBus) Subscribe

func (bus *EventBus) Subscribe[E event[D], D any](handler func(D))

Subscribe registers a handler for event type E with no owning service:

bus.Subscribe[OrderPlaced](func(p OrderPlacedPayload) { ... })

Use SubscribeWithOwner instead when subscribing from a service, so RemoveByService can clean the subscription up when that service closes. Handlers are started in registration order but run concurrently, so a handler that touches shared state must do its own synchronization.

func (*EventBus) SubscribeWithOwner

func (bus *EventBus) SubscribeWithOwner[E event[D], D any](service Service, handler func(D))

SubscribeWithOwner registers a handler for event type E and records service as its owner. Worker.CloseService uses that ownership to remove every subscription belonging to a service it tears down, so services should always subscribe through this method rather than Subscribe.

type FieldError added in v0.3.5

type FieldError struct {
	Field   string `json:"field"`
	Rule    string `json:"rule"`
	Message string `json:"message"`
}

FieldError describes a single field-level validation failure. Field carries the name the client sent (the json tag), not the Go struct field name.

type HealthProvider

type HealthProvider interface {
	CheckHealth(ctx context.Context) map[string]error
}

HealthProvider aggregates health signals across all active services that implement HealthReporter. The returned map keys are service names; a nil value indicates a healthy service.

type HealthReporter

type HealthReporter interface {
	CheckHealth(ctx context.Context) error
}

HealthReporter is implemented by services that can report their own health. Answers the question "am I alive?" — a failure here signals that the process is in a broken state that a restart would resolve (lost connection that won't recover, corrupt internal state, deadlock). Maps to a Kubernetes livenessProbe. A nil return means healthy; a non-nil error describes the failure.

type Job

type Job struct {
	ID            string
	ReceiptHandle string            // opaque token used by Ack/Nack
	Attributes    map[string]string // provider-specific metadata
	Body          []byte
}

Job represents a message received from a queue.

type JobQueueProvider

type JobQueueProvider interface {
	Enqueue(ctx context.Context, queue string, payload []byte, opts ...EnqueueOption) error
	Dequeue(ctx context.Context, queue string, maxMessages int, wait time.Duration) ([]Job, error)
	Ack(ctx context.Context, queue string, job Job) error
	Nack(ctx context.Context, queue string, job Job) error
}

JobQueueProvider abstracts async job/message queue processing. Implemented by gas/queue/sqs or any other queue service. The interface is pull-based: consumers call Dequeue in their own worker loop and acknowledge results with Ack/Nack.

type LogEvent

type LogEvent interface {
	Str(key, val string) LogEvent
	Int(key string, val int) LogEvent
	Int64(key string, val int64) LogEvent
	Float64(key string, val float64) LogEvent
	Bool(key string, val bool) LogEvent
	Err(key string, val error) LogEvent
	Duration(key string, val time.Duration) LogEvent
	Any(key string, val any) LogEvent
	// Send finalizes and emits the log event.
	Send()
}

LogEvent is a single structured log entry. Field methods return the same LogEvent for chaining. Call Send to finalize and emit the event.

type Logger

type Logger interface {
	// Trace starts a log event at the TRACE level.
	Trace(msg string) LogEvent
	// Debug starts a log event at the DEBUG level.
	Debug(msg string) LogEvent
	// Info starts a log event at the INFO level.
	Info(msg string) LogEvent
	// Warn starts a log event at the WARN level.
	Warn(msg string) LogEvent
	// Error starts a log event at the ERROR level.
	Error(msg string) LogEvent

	// With returns a LoggerContext for building a sub-logger with persistent fields.
	With() LoggerContext

	// Flush writes any buffered log entries to the underlying output.
	Flush()
}

Logger is the provider interface for structured logging in the Gas ecosystem. Implementations produce LogEvent values at a given severity level. Each method returns a LogEvent that can be enriched with typed fields before calling Send.

Use With to derive a sub-logger that carries persistent fields across all subsequent log events.

Call Flush to ensure all buffered log entries are written (e.g. before shutdown).

func LoggerFromContext

func LoggerFromContext(ctx context.Context) Logger

LoggerFromContext returns the Logger stored in ctx, or nil if none is present.

type LoggerContext

type LoggerContext interface {
	Str(key, val string) LoggerContext
	Int(key string, val int) LoggerContext
	Int64(key string, val int64) LoggerContext
	Float64(key string, val float64) LoggerContext
	Bool(key string, val bool) LoggerContext
	Err(key string, val error) LoggerContext
	Duration(key string, val time.Duration) LoggerContext
	Any(key string, val any) LoggerContext
	// Logger returns a new Logger that includes all fields added to this context.
	Logger() Logger
}

LoggerContext is a builder for deriving a sub-logger with persistent fields. Each field method returns the same LoggerContext for chaining. Call Logger to obtain the resulting Logger that carries all accumulated fields.

type Middleware

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

Middleware represents either a named middleware (resolved from the router's registry at apply time) or an inline middleware function.

func MiddlewareByName

func MiddlewareByName(name string) Middleware

MiddlewareByName creates a Middleware that will be resolved by name from the router's internal registry when applied.

func MiddlewareFunc

func MiddlewareFunc(fn func(http.Handler) http.Handler) Middleware

MiddlewareFunc creates a Middleware that wraps an inline handler function directly. The middleware will be anonymous and omitted from the route map. Use MiddlewareFuncWithName to give it a display name.

func MiddlewareFuncWithName

func MiddlewareFuncWithName(name string, fn func(http.Handler) http.Handler) Middleware

MiddlewareFuncWithName creates a named inline Middleware. The name appears in the route map alongside routes that this middleware applies to.

type Migration

type Migration struct {
	Version     string
	Service     Service
	Description string
	Up          string
	Down        string
}

Migration represents a single database migration owned by a service.

type MigrationManager

type MigrationManager interface {
	Service

	// Register adds a migration and tracks which service owns it.
	Register(service Service, m Migration)

	// RegisterSlice adds multiple migrations at once for the given service.
	RegisterSlice(service Service, migrations []Migration)

	// RegisterFS reads .up.sql/.down.sql files from an fs.FS and registers
	// them as migrations for the given service. Files must follow the naming
	// convention: {version}_{description}.up.sql / {version}_{description}.down.sql
	// (e.g. 20250216_001_create_users.up.sql).
	RegisterFS(service Service, fsys fs.FS) error

	// RunPending applies all unapplied migrations in global version order.
	// If any migration is marked dirty, execution is blocked until the
	// dirty state is manually resolved.
	RunPending() error

	// Down reverses the last n applied migrations in reverse version order.
	Down(n int) error
}

MigrationManager is the interface for registering and executing database migrations. Services call Register during Init to declare their migrations. The implementation lives in a separate service (gas/migrate) and is wired in by the App.

type NopLogEvent

type NopLogEvent struct{}

NopLogEvent is a LogEvent that discards all fields and performs no action on Send. All methods return the receiver for chaining.

func (*NopLogEvent) Any

func (e *NopLogEvent) Any(string, any) LogEvent

Any adds a key-value pair to the event, where the value can be of any type, returning the event for chaining.

func (*NopLogEvent) Bool

func (e *NopLogEvent) Bool(string, bool) LogEvent

Bool adds a boolean field to the log event and returns the event for method chaining.

func (*NopLogEvent) Duration

func (e *NopLogEvent) Duration(string, time.Duration) LogEvent

Duration adds a field with a duration value to the log entry, identified by the given key.

func (*NopLogEvent) Err

func (e *NopLogEvent) Err(string, error) LogEvent

Err adds an error field to the log event with the specified key and value. Returns the log event for chaining.

func (*NopLogEvent) Float64

func (e *NopLogEvent) Float64(string, float64) LogEvent

Float64 sets a float64 value for the specified key in the log event. It returns the LogEvent instance for chaining.

func (*NopLogEvent) Int

func (e *NopLogEvent) Int(string, int) LogEvent

Int adds an integer field to the log event with the provided key and value, returning the LogEvent for chaining.

func (*NopLogEvent) Int64

func (e *NopLogEvent) Int64(string, int64) LogEvent

Int64 adds an int64 field to the log event with the specified key and value. It returns the LogEvent for chaining.

func (*NopLogEvent) Send

func (e *NopLogEvent) Send()

Send finalizes the log event. In NopLogEvent, it performs no action and discards the event.

func (*NopLogEvent) Str

func (e *NopLogEvent) Str(string, string) LogEvent

Str adds a string field with the given key and value to the log event and returns the event for chaining.

type NopLogger

type NopLogger struct{}

NopLogger is a Logger implementation that silently discards all log output. It uses a singleton pattern — all instances share the same underlying value, resulting in zero allocations per log call.

func (*NopLogger) Debug

func (l *NopLogger) Debug(string) LogEvent

Debug logs a debug-level message and returns a no-op LogEvent for chaining.

func (*NopLogger) Error

func (l *NopLogger) Error(string) LogEvent

Error logs an error-level message with no operational effect, returning a no-op LogEvent.

func (*NopLogger) Flush

func (l *NopLogger) Flush()

Flush performs no operation as NopLogger discards all log output without buffering.

func (*NopLogger) Info

func (l *NopLogger) Info(string) LogEvent

Info logs an informational message and returns a no-op LogEvent instance.

func (*NopLogger) Trace

func (l *NopLogger) Trace(string) LogEvent

Trace creates a no-op log event for a trace-level message and silently discards it.

func (*NopLogger) Warn

func (l *NopLogger) Warn(string) LogEvent

Warn logs a warning-level message and returns a no-op LogEvent instance.

func (*NopLogger) With

func (l *NopLogger) With() LoggerContext

With creates a new LoggerContext for deriving a sub-logger with persistent fields across log events.

type NopLoggerContext

type NopLoggerContext struct{}

NopLoggerContext is a LoggerContext that discards all fields. All methods return the receiver for chaining and Logger returns a NopLogger.

func (*NopLoggerContext) Any

Any adds a field with the given key and value to the context, supporting any type, and returns the LoggerContext.

func (*NopLoggerContext) Bool

Bool adds a boolean field to the logging context under the specified key for structured logging.

func (*NopLoggerContext) Duration

Duration adds a time.Duration field with the specified key and value to the logging context and returns it.

func (*NopLoggerContext) Err

Err adds an error field to the logger context with the specified key and error value, returning the same context.

func (*NopLoggerContext) Float64

Float64 adds a float64 field with the specified key and value to the context and returns the LoggerContext for chaining.

func (*NopLoggerContext) Int

Int adds an integer field to the logger context with the specified key. Returns the same LoggerContext for chaining.

func (*NopLoggerContext) Int64

Int64 adds a key-value pair with an int64 value to the log context and returns the updated LoggerContext.

func (*NopLoggerContext) Logger

func (c *NopLoggerContext) Logger() Logger

Logger returns a no-op Logger instance that discards all log events.

func (*NopLoggerContext) Str

Str adds a string key-value pair to the logger context and returns the same context for chaining.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option is a marker interface satisfied by both WorkerOption and AppOption. NewWorker and NewApp accept ...Option so callers can pass either type.

type Principal

type Principal interface {
	Subject() string
	Scheme() string
	CredentialID() string
	Metadata() PrincipalMetadata
}

Principal represents an authenticated identity. Subject is the stable user identifier, Scheme identifies the authentication method (e.g. "jwt", "session", "apikey"), and CredentialID is the specific credential instance (session ID, JWT jti, API key ID).

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) (Principal, bool)

PrincipalFromContext retrieves the Principal from the context and reports whether one is present. A nil Principal, or a typed-nil one (a nil pointer of a type implementing Principal), counts as absent: the result is nil and false, so a caller checking ok never gets a principal it cannot use.

type PrincipalMetadata

type PrincipalMetadata interface {
	Value(key string) any
}

PrincipalMetadata provides read-only access to arbitrary key-value metadata attached to a Principal at construction time.

type ReadyProvider

type ReadyProvider interface {
	CheckReady(ctx context.Context) map[string]error
}

ReadyProvider aggregates readiness signals across all active services that implement ReadyReporter. The returned map keys are service names; a nil value indicates a ready service.

type ReadyReporter

type ReadyReporter interface {
	CheckReady(ctx context.Context) error
}

ReadyReporter is implemented by services that can report whether they are ready to accept traffic. Answers the question "should traffic be routed to me?" — distinct from health, since a service can be alive but not yet ready (warming caches, pending migrations, dependency briefly unavailable, draining during shutdown). Maps to a Kubernetes readinessProbe. A nil return means ready; a non-nil error describes why the service is not ready.

type RegisteredRoute

type RegisteredRoute struct {
	Method     string
	Path       string
	Middleware []string
}

RegisteredRoute is an exported snapshot of a registered route.

type RequestLoggerOption

type RequestLoggerOption func(*RequestLoggerOptions)

RequestLoggerOption is a functional option for configuring RequestLogger.

func WithRequestLoggerAppendRequestID

func WithRequestLoggerAppendRequestID(val bool) RequestLoggerOption

WithRequestLoggerAppendRequestID controls whether the request ID (from chi's RequestID middleware) is added to the logger's base fields. Defaults to true.

func WithRequestLoggerBuilder

func WithRequestLoggerBuilder(fn func(Logger, *http.Request) Logger) RequestLoggerOption

WithRequestLoggerBuilder sets a function that customizes the logger used for each request. The builder receives the resolved Logger and the current request, and returns a (possibly wrapped or enriched) Logger. It is called after the request ID field is appended (if enabled). This can be used to add extra fields, swap the logger, or adjust log levels per request.

type RequestLoggerOptions

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

RequestLoggerOptions configures the behavior of the RequestLogger middleware.

type Resolver

type Resolver interface {
	// contains filtered or unexported methods
}

Resolver is implemented by ServiceContainer and Scope to provide dependency resolution. The unexported method restricts implementation to this package.

type Result

type Result interface {
	RowsAffected() (int64, error)
	LastInsertId() (int64, error)
}

Result represents the outcome of an Exec operation.

type Router

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

Router is a smart router that wraps Chi and tracks route and middleware ownership by service. MiddlewareByName middleware is validated against an internal registry at registration time and resolved on every build of the routing tree, so ownership changes reach chains registered earlier. The base server uses RemoveByService during kill-switch to replace everything a closed service registered — its routes and its named middleware — with 503.

Top-level routers created via NewRouter() start unsealed: Use, Handle, Group, and Route calls are deferred until Seal() is called. This lets services register middleware and routes in any order during Init. Seal() flushes all pending middleware first (via chi.Use), then replays pending route operations — guaranteeing middleware-before-routes ordering that Chi requires.

Sub-routers (created inside Group/Route callbacks) are always sealed so their calls pass through to Chi immediately.

Concurrency model: once sealed, the live chi tree is immutable. ServeHTTP reads it lock-free via an atomic pointer (served). Runtime mutators (RemoveByService, and Handle/Route/Group/Use via RestartService) never touch the tree in flight — they rebuild a fresh chi tree under r.mu by replaying the recorded registrations and atomically swap it in. In-flight requests keep serving the previous tree, which is never written after publication.

func NewRouter

func NewRouter() *Router

NewRouter creates a Router backed by Chi with an empty middleware registry. The router starts unsealed — Use/Handle/Group/Route calls are deferred until Seal() is called.

func (*Router) Close added in v0.5.0

func (r *Router) Close(context.Context) error

Close is a no-op; routes are removed through RemoveByService.

func (*Router) Delete added in v0.5.2

func (r *Router) Delete(service Service, path string, handler any, middleware ...Middleware)

Delete registers a DELETE route via Handle.

func (*Router) Get added in v0.5.2

func (r *Router) Get(service Service, path string, handler any, middleware ...Middleware)

Get registers a GET route via Handle, which also serves HEAD on the same path.

func (*Router) Group

func (r *Router) Group(fn func(sub *Router))

Group creates an inline route group. The callback receives a sub-Router that shares the parent's middleware registry and route tracking. When the router is unsealed, the group is deferred until Seal().

func (*Router) Handle

func (r *Router) Handle(
	service Service,
	method, path string,
	handler any,
	middleware ...Middleware,
)

Handle registers a route and tracks ownership. The handler can be:

  • http.HandlerFunc or func(http.ResponseWriter, *http.Request) — passed through directly
  • A DI-aware function: func(gas.Context, Dep1, Dep2, ...) error — dependencies are auto-resolved from the per-request scope at call time

Middleware is resolved from the internal registry (for MiddlewareByName) or used directly (for MiddlewareFunc) and applied in order (outermost first). Panics if a named middleware is not registered or if a DI-aware handler has an invalid signature. When the router is unsealed, the registration is deferred until Seal().

func (*Router) Head added in v0.5.2

func (r *Router) Head(service Service, path string, handler any, middleware ...Middleware)

Head registers a HEAD route via Handle.

func (*Router) Init added in v0.5.0

func (r *Router) Init(context.Context) error

Init is a no-op. It exists so the Router satisfies Service and can be passed as an owner.

func (*Router) Mux

func (r *Router) Mux() chi.Router

Mux returns the underlying Chi router so the base server can add global middleware, mount sub-routers, or pass it to http.Server.

This returns the most recently built tree. Mutating it directly bypasses the router's synchronization and is only safe before serving begins.

func (*Router) Name added in v0.5.0

func (r *Router) Name() string

Name returns the name of the router as a string.

func (*Router) NamedMiddleware

func (r *Router) NamedMiddleware() map[string]string

NamedMiddleware returns a snapshot of the named middleware registry as a map of middleware name to owning service.

func (*Router) NotFound

func (r *Router) NotFound(service Service, handler any)

NotFound registers a custom not-found handler for the router, associated with the specified service. The handler can be http.HandlerFunc or a DI-aware function (same rules as Handle). Panics if a not found handler is already registered by another service.

func (*Router) Patch added in v0.5.2

func (r *Router) Patch(service Service, path string, handler any, middleware ...Middleware)

Patch registers a PATCH route via Handle.

func (*Router) Post added in v0.5.2

func (r *Router) Post(service Service, path string, handler any, middleware ...Middleware)

Post registers a POST route via Handle.

func (*Router) Put added in v0.5.2

func (r *Router) Put(service Service, path string, handler any, middleware ...Middleware)

Put registers a PUT route via Handle.

func (*Router) Register

func (r *Router) Register(service Service, name string, mw func(http.Handler) http.Handler)

Register adds a named middleware to the internal registry and tracks which service owns it.

func (*Router) RemoveByService added in v0.3.5

func (r *Router) RemoveByService(service Service)

RemoveByService tears down everything the given service registered. Its routes are replaced with 503 Service Unavailable handlers, and every named middleware it registered is replaced with a 503 short-circuit wherever it is referenced — including on routes owned by services that are still running, and including references nested inside Group/Route blocks. A route guarded by a killed service's middleware therefore stops serving: the middleware is disabled, never skipped, so a teardown can never drop an authorization check and leave the route open.

Register-ing the name again (the RestartService -> Init path) re-arms it.

The teardown is applied by rebuilding a fresh routing tree and swapping it in atomically, so requests in flight on the old tree are never disrupted.

func (*Router) Route

func (r *Router) Route(pattern string, fn func(sub *Router))

Route creates a pattern-scoped route group. The callback receives a sub-Router that shares the parent's middleware registry and route tracking. When the router is unsealed, the route is deferred until Seal().

Calling Route with the same pattern more than once on the same parent is allowed: subsequent calls attach to the sub-mux created by the first call instead of panicking. Each call's body runs inside its own chi.Group, so middleware added via Use() inside a given block only applies to handlers registered in that block.

func (*Router) Routes

func (r *Router) Routes() map[string][]RegisteredRoute

Routes returns a snapshot of all explicitly registered routes grouped by service. Implicit HEAD routes (auto-registered alongside GET) are excluded.

func (*Router) Seal

func (r *Router) Seal()

Seal builds the routing tree from the deferred middleware and route registrations and publishes it. Middleware is applied first (via chi.Use), then route operations are replayed in order. After Seal, structural changes rebuild the tree and swap it in atomically. Seal is idempotent — calling it on an already-sealed router is a no-op.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements http.Handler, delegating to the published Chi tree. The tree is loaded atomically with no lock, so serving never contends with runtime route mutations.

func (*Router) SetErrorHandler

func (r *Router) SetErrorHandler(h ErrorHandler)

SetErrorHandler configures the function that converts DI-aware handler errors into HTTP responses. Must be called before Handle() registrations.

func (*Router) Use

func (r *Router) Use(middleware ...Middleware)

Use applies middleware to the router. Each Middleware is resolved (by name from the registry or used directly) and applied via chi's Use. Panics if a named middleware is not registered. When the router is unsealed, middleware is queued and applied during Seal().

func (*Router) UseMiddlewareByName

func (r *Router) UseMiddlewareByName(middleware string)

UseMiddlewareByName applies a middleware to the router by resolving it from the registry using its name. Panics if the middleware is not registered.

func (*Router) UseMiddlewareFunc

func (r *Router) UseMiddlewareFunc(middleware func(http.Handler) http.Handler)

UseMiddlewareFunc applies a middleware function directly to the router by wrapping it as a MiddlewareFunc.

type Rows

type Rows interface {
	Next() bool
	Scan(dest ...any) error
	Close() error
	Err() error
}

Rows represents the result set of a query. Mirrors the standard database/sql Rows interface so implementations can wrap it directly.

type Scope

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

Scope is a resolution context for scoped service lifetimes.

func RequestScope

func RequestScope(r *http.Request) *Scope

RequestScope returns the per-request Scope from the request context. Panics if called outside the scope middleware (i.e. before InitServices installs it, or on a non-App-managed handler).

func (*Scope) Close

func (s *Scope) Close(ctx context.Context) error

Close calls Close(ctx) on all scoped Service instances resolved in this scope, in reverse resolution order, so a scoped service can still use the scoped dependencies it was built from.

func (*Scope) MustResolve added in v0.5.0

func (s *Scope) MustResolve[T any]() T

MustResolve is like Resolve but panics if the service cannot be resolved.

func (*Scope) Resolve added in v0.5.0

func (s *Scope) Resolve[T any]() (T, error)

Resolve returns the instance registered for T within this scope. A scoped T is built once per Scope and cached for the life of the scope; singletons come from the owning container, and transients are built fresh. Scoped instances that implement Service are closed in reverse resolution order by Scope.Close.

type SecurityHeadersOption

type SecurityHeadersOption func(*SecurityHeadersOptions)

SecurityHeadersOption is a functional option for configuring SecurityHeaders.

func WithSecurityHeadersContentSecurityPolicy

func WithSecurityHeadersContentSecurityPolicy(val string) SecurityHeadersOption

WithSecurityHeadersContentSecurityPolicy sets the Content-Security-Policy header value. Pass an empty string to disable this header. No default is applied, since CSP values are highly application-specific.

func WithSecurityHeadersContentTypeOptions

func WithSecurityHeadersContentTypeOptions(val string) SecurityHeadersOption

WithSecurityHeadersContentTypeOptions sets the X-Content-Type-Options header value. Pass an empty string to disable this header. Default: "nosniff".

func WithSecurityHeadersCrossOriginOpenerPolicy

func WithSecurityHeadersCrossOriginOpenerPolicy(val string) SecurityHeadersOption

WithSecurityHeadersCrossOriginOpenerPolicy sets the Cross-Origin-Opener-Policy header value. Pass an empty string to disable this header. No default is applied.

func WithSecurityHeadersCrossOriginResourcePolicy

func WithSecurityHeadersCrossOriginResourcePolicy(val string) SecurityHeadersOption

WithSecurityHeadersCrossOriginResourcePolicy sets the Cross-Origin-Resource-Policy header value. Pass an empty string to disable this header. No default is applied.

func WithSecurityHeadersFrameOptions

func WithSecurityHeadersFrameOptions(val string) SecurityHeadersOption

WithSecurityHeadersFrameOptions sets the X-Frame-Options header value. Pass an empty string to disable this header. Default: "DENY".

func WithSecurityHeadersPermissionsPolicy

func WithSecurityHeadersPermissionsPolicy(val string) SecurityHeadersOption

WithSecurityHeadersPermissionsPolicy sets the Permissions-Policy header value. Pass an empty string to disable this header. Default: "camera=(), microphone=(), geolocation=()".

func WithSecurityHeadersReferrerPolicy

func WithSecurityHeadersReferrerPolicy(val string) SecurityHeadersOption

WithSecurityHeadersReferrerPolicy sets the Referrer-Policy header value. Pass an empty string to disable this header. Default: "strict-origin-when-cross-origin".

func WithSecurityHeadersStrictTransportSecurity

func WithSecurityHeadersStrictTransportSecurity(val string) SecurityHeadersOption

WithSecurityHeadersStrictTransportSecurity sets the Strict-Transport-Security (HSTS) header value. Pass an empty string to disable this header. No default is applied, since HSTS should only be enabled once the site is fully served over HTTPS.

type SecurityHeadersOptions

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

SecurityHeadersOptions configures which security headers are set by the SecurityHeaders middleware.

type ServerSettings

type ServerSettings struct {
	// Host specifies the hostname or IP address where the server will be
	// hosted: an IP address or an RFC 1123 hostname. ConfigProvider.Bind
	// checks it against the validate tag, for shape only, with no DNS lookup.
	Host string `validate:"required,max=253,ip|hostname_rfc1123"`

	// Port is the port number on which the server listens for incoming requests.
	Port int

	// ReadTimeout is the maximum duration for reading the entire request.
	ReadTimeout time.Duration

	// WriteTimeout is the maximum duration before timing out writes of the response.
	WriteTimeout time.Duration

	// IdleTimeout is the maximum time to wait for the next request when keep-alives are enabled.
	IdleTimeout time.Duration

	// ShutdownTimeout is how long to wait for in-flight requests to complete
	// during graceful shutdown.
	ShutdownTimeout time.Duration

	// MaxJSONBodyBytes caps how many bytes of the request body
	// Context.BindJSON reads; a larger body fails with a 413 *Error. It
	// defaults to 1 MiB. Zero or a negative value means no limit. It does not
	// apply to handlers that read the body themselves.
	MaxJSONBodyBytes int64
}

ServerSettings defines the configuration for a server, including host, port, timeouts, and graceful shutdown settings.

type Service

type Service interface {
	// Name returns the unique identifier for this service (e.g., "gas/auth").
	Name() string

	// Init initializes the service. Called automatically by the DI container
	// after construction. Services register routes, middleware, migrations,
	// and event subscriptions here.
	//
	// For a singleton, ctx is the caller's startup context (or RestartService
	// context) with the service's init deadline added; for a scoped service it
	// is the scope's context. Init must honor ctx: when the deadline passes
	// the container cancels ctx and waits for Init to return, so an Init that
	// ignores it holds up startup. ctx is canceled as soon as Init returns,
	// so work that outlives Init, such as a background goroutine, must not
	// derive from it.
	Init(ctx context.Context) error

	// Close gracefully shuts down the service. Called at App shutdown
	// (singletons) or when a Scope is closed (scoped services). ctx is the
	// caller's shutdown context and bounds the shutdown work.
	Close(ctx context.Context) error
}

Service is the core interface for lifecycle-managed services. Any type registered with the DI container that implements this interface will have Init(ctx) called after construction and Close(ctx) called at shutdown (singletons) or scope end (scoped). This holds however the value was registered: a pre-built instance passed to WithServiceInstance is initialized too. Transient services cannot implement this interface — registration will be rejected.

Implementing it partially is rejected too. Init and Close are the managed lifecycle, so a type declaring either one must implement all three. A type that declares one and stops is not a Service and would be silently inert: Init never runs, Close never runs, and the kill switch cannot see it. BuildAll returns an error naming the missing or mis-typed methods instead.

Name is not a trigger. Declaring only Name leaves a type an ordinary dependency, and the same registration options carry those — loggers, config providers, per-request values — which must keep working.

A type from another package that declares Close (an io.Closer such as *sql.DB) cannot be given the remaining methods; wrap it in a service of your own rather than registering it directly.

type ServiceContainer

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

ServiceContainer is a dependency injection container that manages service registration, construction, and lifetime-scoped resolution.

func NewServiceContainer

func NewServiceContainer(opts ...ServiceContainerOption) *ServiceContainer

NewServiceContainer creates a new ServiceContainer.

func (*ServiceContainer) BuildAll

func (c *ServiceContainer) BuildAll(ctx context.Context) error

BuildAll initializes pre-registered Service instances, then eagerly resolves all singleton services in dependency order. Transient and scoped services are validated but not constructed.

Implementing Service means the container manages the lifecycle, so a pre-built instance is initialized like any other service. It is initialized before anything the container constructs, matching the order it became available, so reversing that order at shutdown stays correct.

func (*ServiceContainer) CanResolve

func (c *ServiceContainer) CanResolve[T any]() bool

CanResolve reports whether the container has an instance or registration for exactly T, which is when Resolve can find it. An interface T is not resolvable just because a registered type implements it: register the constructor under the interface instead. It answers from the registry alone and builds nothing.

func (*ServiceContainer) EachInstance

func (c *ServiceContainer) EachInstance(fn func(reflect.Value))

EachInstance iterates all built singleton instances (including pre-registered ones) in the order they became available. A dependency is always constructed and cached before the constructor that consumes it returns, so this order is a valid initialization order: reversing it is safe for shutdown.

func (*ServiceContainer) MustResolve added in v0.4.0

func (c *ServiceContainer) MustResolve[T any]() T

MustResolve is like Resolve but panics if the service cannot be resolved.

func (*ServiceContainer) NewScope

func (c *ServiceContainer) NewScope(ctx context.Context) *Scope

NewScope creates a scoped resolution context. Scoped services resolved within this scope share instances; singletons delegate to the container. ctx is passed to the Init of every scoped service the scope builds; in an App it is the request's context.

func (*ServiceContainer) RegisterScopedService added in v0.4.0

func (c *ServiceContainer) RegisterScopedService[T any](ctor any)

RegisterScopedService registers a constructor for type T with the Scoped lifetime, so one T is built per Scope and shared within it. Scoped services cannot be resolved from the container itself; see NewScope. See RegisterService for the accepted signatures.

func (*ServiceContainer) RegisterService added in v0.4.0

func (c *ServiceContainer) RegisterService[T any](ctor any, lifetime ServiceLifetime)

RegisterService registers a constructor for type T with the given lifetime. The constructor takes its dependencies as parameters and returns T, or T and an error:

func(DepA, DepB, ...) T
func(DepA, DepB, ...) (T, error)

Dependencies are resolved from the container by their parameter types, so every one of them must itself be registered by the time the service is built.

Panics if ctor is not a function the container can call: it must be non-variadic, return one or two values, produce a first result assignable to T (or implementing T when T is an interface), and, when it returns two, have error as the second. See validateCtorShape.

Panics if lifetime is Transient and T implements Service, because transient services cannot have managed lifecycles. Use Singleton or Scoped instead.

A type that declares Init or Close but does not fully implement Service is rejected by BuildAll; see validateServiceShape.

Registering a type twice replaces the earlier registration.

func (*ServiceContainer) RegisterServiceInstance added in v0.4.0

func (c *ServiceContainer) RegisterServiceInstance[T any](val T)

RegisterServiceInstance registers an already-built value under T, the static type at the call site, not the dynamic type of val. Passing an interface-typed variable therefore makes the value resolvable as that interface and not as its concrete type; pass the concrete type, or name T explicitly, to control which one it registers under.

The value is treated as a singleton. The container owns its lifecycle from here on, so do not call Init yourself before registering, or it runs twice.

func (*ServiceContainer) RegisterSingletonService added in v0.4.0

func (c *ServiceContainer) RegisterSingletonService[T any](ctor any)

RegisterSingletonService registers a constructor for type T with the Singleton lifetime, so one T is built and shared by every consumer. See RegisterService for the accepted signatures.

func (*ServiceContainer) RegisterTransientService added in v0.4.0

func (c *ServiceContainer) RegisterTransientService[T any](ctor any)

RegisterTransientService registers a constructor for type T with the Transient lifetime, so a fresh T is built on every resolution. T must not implement Service. See RegisterService for the accepted signatures.

func (*ServiceContainer) Resolve added in v0.4.0

func (c *ServiceContainer) Resolve[T any]() (T, error)

Resolve returns the instance registered for T, building it and its dependencies on first use and caching it when T is a singleton. Scoped registrations cannot be resolved here and return an error; call NewScope and resolve from the Scope instead.

type ServiceContainerOption added in v0.5.5

type ServiceContainerOption func(*ServiceContainerOptions)

ServiceContainerOption configures a ServiceContainer at construction.

func WithServiceContainerServiceTimeout added in v0.5.5

func WithServiceContainerServiceTimeout(timeout time.Duration) ServiceContainerOption

WithServiceContainerServiceTimeout sets the deadline the container gives each singleton service's Init. The default is 15 seconds, and 0 disables the deadline so only the caller's context applies. A service that implements ServiceInitTimeout overrides it for itself.

Panics if timeout is negative.

type ServiceContainerOptions added in v0.5.5

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

ServiceContainerOptions holds the settings a ServiceContainer is built with. Its fields are set through ServiceContainerOption values passed to NewServiceContainer.

type ServiceInitTimeout added in v0.5.5

type ServiceInitTimeout interface {
	// InitTimeout returns how long the container waits for Init.
	InitTimeout() time.Duration
}

ServiceInitTimeout is implemented by a singleton Service that needs a different Init deadline than the container default (see WithServiceInitTimeout). It applies at startup and on RestartService. When the deadline passes, Init's context is canceled and, once Init returns, the call fails with an error that wraps context.DeadlineExceeded. If Init ignored the cancellation and returned nil, the service is closed again before the error is returned. 0 means no deadline; a negative value fails Init without running it.

type ServiceLifetime

type ServiceLifetime uint8

ServiceLifetime controls how a service instance is created and cached.

const (
	// ServiceLifetimeSingleton services are created once and shared across all consumers.
	ServiceLifetimeSingleton ServiceLifetime = iota
	// ServiceLifetimeScoped services are created once per Scope.
	ServiceLifetimeScoped
	// ServiceLifetimeTransient services are created fresh on every resolution.
	ServiceLifetimeTransient
)

func (ServiceLifetime) String

func (l ServiceLifetime) String() string

String returns the lowercase name of the lifetime ("singleton", "scoped", "transient"), or "unknown" for a value outside that set.

type SystemAllServicesInitialized

type SystemAllServicesInitialized struct {
	Event[SystemAllServicesInitializedPayload]
}

SystemAllServicesInitialized is emitted when all services have been successfully initialized.

type SystemAllServicesInitializedPayload

type SystemAllServicesInitializedPayload struct{}

SystemAllServicesInitializedPayload is an empty payload for the all-services-initialized event.

type SystemServerShuttingDown

type SystemServerShuttingDown struct {
	Event[SystemServerShuttingDownPayload]
}

SystemServerShuttingDown is emitted when the HTTP server is shutting down. For code that should run on any shutdown (not just HTTP), subscribe to SystemShuttingDown instead.

type SystemServerShuttingDownPayload

type SystemServerShuttingDownPayload struct{}

SystemServerShuttingDownPayload is an empty payload for the server-shutting-down event.

type SystemServiceClosed

type SystemServiceClosed struct {
	Event[SystemServiceClosedPayload]
}

SystemServiceClosed is emitted when a service is closed at runtime.

type SystemServiceClosedPayload

type SystemServiceClosedPayload struct {
	ServiceName string
}

SystemServiceClosedPayload carries the name of the closed service.

type SystemServiceInitialized

type SystemServiceInitialized struct {
	Event[SystemServiceInitializedPayload]
}

SystemServiceInitialized is emitted when a service is (re-)initialized at runtime.

type SystemServiceInitializedPayload

type SystemServiceInitializedPayload struct {
	ServiceName string
}

SystemServiceInitializedPayload carries the name of the initialized service.

type SystemShuttingDown

type SystemShuttingDown struct {
	Event[SystemShuttingDownPayload]
}

SystemShuttingDown is emitted when a Worker or App begins its shutdown sequence. It fires for both HTTP (App) and non-HTTP (Worker) workloads.

type SystemShuttingDownPayload

type SystemShuttingDownPayload struct{}

SystemShuttingDownPayload is an empty payload for the shutting-down event.

type TemplateProvider

type TemplateProvider interface {
	// Get returns the raw template content by name.
	Get(ctx context.Context, name string) ([]byte, error)

	// List returns all available template names.
	List(ctx context.Context) ([]string, error)

	// Register adds or replaces a template by name and raw content.
	Register(ctx context.Context, name string, content []byte) error

	// RegisterFS walks an fs.FS and registers every template file found.
	RegisterFS(ctx context.Context, fsys fs.FS) error
}

TemplateProvider abstracts template storage and retrieval. Implementations may be backed by a filesystem, database, memory, or any combination. Consumers such as UIProvider (HTML rendering) and EmailProvider (email templating) resolve raw template content through this interface.

type TemplatedEmail

type TemplatedEmail struct {
	SubjectTemplate string
	TextTemplate    string
	HTMLTemplate    string
	Data            any
	Email
}

TemplatedEmail represents an email message where templates and data define subject, text, and HTML bodies.

type UIProvider

type UIProvider interface {
	Render(w http.ResponseWriter, name string, data any) error
	RenderFragment(
		w http.ResponseWriter,
		name string,
		data any,
	) error // renders without layout wrapper, useful for HTMX
	RenderWithStatus(w http.ResponseWriter, status int, name string, data any) error
	RegisterFuncs(funcs template.FuncMap)
}

UIProvider abstracts template rendering. Implemented by gas/ui or any other UI service. Template storage and retrieval is handled by TemplateProvider; UIProvider focuses on compilation and rendering.

type WithGasEnv added in v0.5.5

type WithGasEnv struct {
	GasEnv Environment
}

WithGasEnv is embedded in a configuration struct to carry the application environment. Its GasEnv field binds from the configuration's GasEnv key, which the gasenv extension sets during load; without the extension, nothing sets it unless a provider does, and GasEnv stays empty.

type Worker

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

Worker manages service lifecycle, dependency injection, and the event bus without an HTTP server. Use it for non-HTTP environments such as AWS Lambda, background job processors, or CLI tools. For HTTP servers, use App which embeds Worker and adds routing, CSRF protection, and an HTTP listener.

func NewWorker

func NewWorker(opts ...Option) *Worker

NewWorker creates a Worker with the given options. Only WorkerOption values are applied; passing an AppOption panics.

func (*Worker) ActiveServices

func (w *Worker) ActiveServices() []string

ActiveServices returns the names of all currently active services.

func (*Worker) ActiveServicesMap

func (w *Worker) ActiveServicesMap() map[string]Service

ActiveServicesMap returns a map of all currently active services. This is a test helper.

func (*Worker) CheckHealth

func (w *Worker) CheckHealth(ctx context.Context) map[string]error

CheckHealth runs CheckHealth concurrently on every active service that implements HealthReporter and returns a map of service name to result (nil if healthy). Services that do not implement HealthReporter are omitted.

func (*Worker) CheckReady

func (w *Worker) CheckReady(ctx context.Context) map[string]error

CheckReady runs CheckReady concurrently on every active service that implements ReadyReporter and returns a map of service name to result (nil if ready). Services that do not implement ReadyReporter are omitted.

func (*Worker) CloseService

func (w *Worker) CloseService[T Service](ctx context.Context) error

CloseService performs the kill-switch sequence for the service registered under T at runtime:

w.CloseService[*auth.Service](ctx)

Infrastructure is cleaned up first, so even if Close panics or fails, the service's routes and event subscriptions are already gone. Its routes then answer 503, and named middleware it owns is disabled wherever it is used.

The service is looked up among the instances the container has already built, so a registration that was never initialized is reported as an error rather than constructed here. Returns an error if T has no built instance or is not currently active, or Shutdown has begun; a failure from the service's own Close is logged, not returned, because the teardown above it has already happened.

ctx is passed to the service's Close.

func (*Worker) ConfigProvider

func (w *Worker) ConfigProvider() ConfigProvider

ConfigProvider resolves the ConfigProvider from the DI container. Returns nil if no ConfigProvider is registered.

func (*Worker) EventBus

func (w *Worker) EventBus() *EventBus

EventBus returns the Worker's event bus.

func (*Worker) InitServices

func (w *Worker) InitServices(ctx context.Context) error

InitServices builds all singletons via the DI container (which calls Init(ctx) on each Service automatically), then collects Service instances for runtime management. Collection follows the container's instance order, so serviceOrder is a genuine initialization order and Shutdown can reverse it safely. ctx is passed to the Init of every singleton, each under its init deadline (see WithServiceInitTimeout).

InitServices runs once. If it fails, it closes, in reverse order, every service it had initialized, with ctx stripped of its cancellation, and returns the error joined with any Close errors; later calls return that same error.

func (*Worker) MigrationManager

func (w *Worker) MigrationManager() MigrationManager

MigrationManager resolves the MigrationManager from the DI container. Returns nil if no MigrationManager is registered.

func (*Worker) ReadyFunc added in v0.5.0

func (w *Worker) ReadyFunc(fn func(*ServiceContainer) error)

ReadyFunc registers a function that runs after all services are initialized and migrations are applied, but before Run blocks or Start returns. It is the imperative form of WithReadyFunc and may be called any number of times before startup; the funcs run in registration order and the first error aborts startup.

Safe for concurrent use. Panics if called once Start has reached the ready hooks, because the func would never run.

func (*Worker) RegisterScopedService added in v0.4.0

func (w *Worker) RegisterScopedService[T any](ctor any)

RegisterScopedService registers a constructor for type T with the Scoped lifetime on the worker's service container, so one T is built per Scope. In an App, each request gets its own scope; see ResolveFromRequestScope.

func (*Worker) RegisterService added in v0.4.0

func (w *Worker) RegisterService[T any](ctor any, lifetime ServiceLifetime)

RegisterService registers a constructor for type T with the given lifetime on the worker's service container. It is the imperative form of WithService, for wiring done after the Worker is built. See ServiceContainer.RegisterService for the accepted constructor signatures and the panics on a bad one.

func (*Worker) RegisterServiceInstance added in v0.4.0

func (w *Worker) RegisterServiceInstance[T any](val T)

RegisterServiceInstance registers an already-built value on the worker's service container under T, the static type at the call site, not the dynamic type of val. Treated as a singleton; see ServiceContainer.RegisterServiceInstance for the lifecycle it takes on.

func (*Worker) RegisterSingletonService added in v0.4.0

func (w *Worker) RegisterSingletonService[T any](ctor any)

RegisterSingletonService registers a constructor for type T with the Singleton lifetime on the worker's service container, so one T is built and shared by every consumer.

func (*Worker) RegisterTransientService added in v0.4.0

func (w *Worker) RegisterTransientService[T any](ctor any)

RegisterTransientService registers a constructor for type T with the Transient lifetime on the worker's service container, so a fresh T is built on every resolution. T must not implement Service.

func (*Worker) RestartService

func (w *Worker) RestartService[T Service](ctx context.Context) error

RestartService re-initializes the previously closed service registered under T, re-running Init on the same instance so it registers its routes, middleware and subscriptions again:

w.RestartService[*auth.Service](ctx)

The service must have been registered with the Worker at construction time and built during InitServices (that is, it must be a singleton in serviceOrder). Returns an error if T has no built instance, is already active, Shutdown has begun, or its Init fails. ctx is passed to the service's Init.

func (*Worker) Run

func (w *Worker) Run() error

Run initializes services, runs migrations and ready hooks, then blocks until a SIGINT or SIGTERM signal is received before shutting down. For non-blocking lifecycle control, use Start and Shutdown directly. Startup and shutdown use context.Background().

func (*Worker) ServiceContainer

func (w *Worker) ServiceContainer() *ServiceContainer

ServiceContainer returns the Worker's dependency injection container.

func (*Worker) Shutdown

func (w *Worker) Shutdown(ctx context.Context) (err error)

Shutdown emits SystemShuttingDown and closes every active service in reverse initialization order, passing ctx to each Close. A service already closed by CloseService is skipped, and each service Shutdown closes leaves ActiveServices. A failing Close does not stop the rest; every Close error is returned, joined, and not logged.

Shutdown runs once. Later calls, including concurrent ones, wait for the first to finish and return nil. Once Shutdown has begun, CloseService and RestartService return an error.

func (*Worker) Start

func (w *Worker) Start(ctx context.Context) error

Start initializes all services, runs pending migrations, and executes ready hooks. It does NOT block — use it when you need explicit lifecycle control (e.g. AWS Lambda). Call Shutdown when done. ctx is passed to the Init of every service built during startup; canceling it aborts startup.

A Start that fails leaves nothing running: it closes, in reverse order, every service it had initialized, with ctx stripped of its cancellation, and returns the startup error joined with any Close errors. The Worker cannot be started again: every later Start returns the same error, and a later Shutdown has nothing to close.

type WorkerOption

type WorkerOption func(*Worker)

WorkerOption configures a Worker. It is the base option type for DI registration, ready hooks, and other non-HTTP concerns. Both NewWorker and NewApp accept WorkerOption values.

func WithReadyFunc

func WithReadyFunc(fn func(*ServiceContainer) error) WorkerOption

WithReadyFunc registers a function that runs after all services are initialized and migrations are applied but before Run blocks or Start returns. Multiple funcs are called in registration order; any error aborts startup.

func WithScopedService

func WithScopedService[T any](ctor any) WorkerOption

WithScopedService registers a service constructor with a scoped lifetime.

func WithService

func WithService[T any](ctor any, lifetime ServiceLifetime) WorkerOption

WithService registers a constructor-based service with the given lifetime.

func WithServiceInitTimeout added in v0.5.5

func WithServiceInitTimeout(timeout time.Duration) WorkerOption

WithServiceInitTimeout sets the deadline the Worker's service container gives each singleton service's Init, at startup and on RestartService. When it passes, Init's context is canceled and startup fails once Init returns. The default is 15 seconds, and 0 disables the deadline so only the caller's context applies. A service that implements ServiceInitTimeout overrides it for itself.

Panics if timeout is negative.

func WithServiceInstance

func WithServiceInstance[T any](val T) WorkerOption

WithServiceInstance registers a pre-built service instance (singleton).

Pre-built means pre-constructed, not pre-initialized: if the value implements Service, the container calls Init(ctx) on it during InitServices like any other service, and Close(ctx) at shutdown. Implementing Service is what hands the lifecycle to the container, however the value got there. Do not call Init yourself before registering, or it runs twice.

func WithSingletonService

func WithSingletonService[T any](ctor any) WorkerOption

WithSingletonService registers a singleton service constructor.

func WithTransientService

func WithTransientService[T any](ctor any) WorkerOption

WithTransientService registers a transient service constructor.

Directories

Path Synopsis
auth module
cache module
config module
database module
email module
log module
migrate module
queue module
storage module
template module
ui module

Jump to

Keyboard shortcuts

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