internal

package
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: Feb 17, 2026 License: Apache-2.0 Imports: 42 Imported by: 0

Documentation

Overview

Package internal provides the core types and implementation for the Forge framework.

This package is internal and should not be used directly. Import "github.com/dmitrymomot/forge" instead, which re-exports the public API.

Core Types

The package defines the fundamental types that users interact with:

  • App: Orchestrates the application lifecycle, HTTP routing, and graceful shutdown
  • Context: Provides request/response access, identity, RBAC, and helper methods
  • Router: Interface handlers use to declare routes with HTTP methods and grouping
  • Handler: Interface implemented by types that declare routes on a router
  • HandlerFunc: Signature for individual route handlers that return errors
  • Middleware: Wraps handlers to add cross-cutting concerns like auth or logging
  • ErrorHandler: Custom error handling function for handler errors
  • Permission: Named permission string for RBAC checks
  • RolePermissions: Maps role names to their granted permissions
  • RoleExtractorFunc: Extracts the current user's role from the request context

Context as context.Context

Context embeds context.Context, so it can be passed directly to any function that expects a standard library context. The Deadline, Done, Err, and Value methods delegate to the underlying request context:

func (h *Handler) getUser(c forge.Context) error {
    // Pass c directly to database calls, HTTP clients, etc.
    user, err := h.repo.GetUser(c, userID)
    if err != nil {
        return err
    }
    return c.JSON(200, user)
}

Application Structure

Create an application with New() and configure it using options:

app := internal.New(internal.AppConfig{},
    internal.WithHandlers(authHandler, pageHandler),
    internal.WithMiddleware(loggingMiddleware, panicMiddleware),
    internal.WithHealthChecks(internal.HealthCheck("db", dbCheck)),
)

Handler Pattern

Handlers implement the Handler interface and declare routes:

type AuthHandler struct {
    repo *repository.Queries
}

func (h *AuthHandler) Routes(r internal.Router) {
    r.GET("/login", h.showLogin)
    r.POST("/login", h.handleLogin)
}

Handlers receive dependencies via constructor injection, not context helpers. This keeps handler logic explicit and testable.

Identity Methods

Context provides convenience methods for checking user identity. These are shortcuts over the session system — they load the session lazily on first access and return safe defaults when no session is configured:

  • UserID() string: Returns the authenticated user's ID, or empty string
  • IsAuthenticated() bool: Returns true if a user is associated with the session
  • IsCurrentUser(id string) bool: Returns true if the given ID matches the current user

Example:

func (h *Handler) showProfile(c internal.Context) error {
    if !c.IsAuthenticated() {
        return c.Redirect(302, "/login")
    }
    user, err := h.repo.GetUser(c, c.UserID())
    if err != nil {
        return err
    }
    return c.JSON(200, user)
}

Role-Based Access Control (RBAC)

Configure RBAC with WithRoles, which accepts a permission map and a role extractor function. The role extractor is called lazily on the first Can() call and the result is cached for the lifetime of the request:

app := internal.New(internal.AppConfig{},
    internal.WithRoles(
        internal.RolePermissions{
            "admin":  {"users.read", "users.write", "billing.manage"},
            "member": {"users.read"},
        },
        func(c internal.Context) string {
            role, _ := c.SessionValue("role")
            if s, ok := role.(string); ok {
                return s
            }
            return ""
        },
    ),
)

Check permissions in handlers with Can():

func (h *Handler) deleteUser(c internal.Context) error {
    if !c.Can("users.write") {
        return c.Error(403, "forbidden")
    }
    // proceed with deletion...
}

Retrieve the resolved role with Role():

func (h *Handler) dashboard(c internal.Context) error {
    role := c.Role() // e.g. "admin", "viewer", ""
    // ...
}

Can() and Role() return safe defaults if RBAC is not configured (false and empty string respectively). Both share the same lazy extraction and per-request caching — the role extractor is called at most once.

Request Handling

Each request receives a Context with comprehensive helper methods:

func (h *AuthHandler) handleLogin(c internal.Context) error {
    var form LoginForm
    validationErrs, err := c.Bind(&form)
    if err != nil {
        return c.Error(http.StatusBadRequest, "invalid form")
    }
    if len(validationErrs) > 0 {
        return c.Render(http.StatusUnprocessableEntity, loginTemplate)
    }

    // Process login...
    return c.JSON(http.StatusOK, result)
}

Middleware

Middleware wraps handlers to add cross-cutting concerns:

func LoggingMiddleware(next internal.HandlerFunc) internal.HandlerFunc {
    return func(c internal.Context) error {
        start := time.Now()
        err := next(c)
        duration := time.Since(start)
        c.LogInfo("request processed", "duration", duration)
        return err
    }
}

Middleware can inspect/modify the request, short-circuit processing, or wrap the response. They have full access to the Context and can be route-specific or global.

Error Handling

Errors returned from handlers trigger the ErrorHandler:

func customErrorHandler(c internal.Context, err error) error {
    if statusCode := getStatusCode(err); statusCode > 0 {
        return c.Error(statusCode, err.Error())
    }
    c.LogError("unhandled error", "error", err)
    return c.Error(http.StatusInternalServerError, "internal server error")
}

Server Runtime

Start the server with Run() or use Run() for multi-domain deployments:

// Single app
err := app.Run(internal.RunConfig{Address: ":8080"}, internal.WithRunLogger(log))

// Multi-domain
err := internal.Run(internal.RunConfig{Address: ":8080"},
    internal.WithDomain("api.example.com", apiApp),
    internal.WithDomain("*.example.com", tenantApp),
)

Features

The Context provides helpers for common request patterns:

  • JSON encoding/decoding
  • Form binding with validation and sanitization
  • Cookie management (plain, signed, encrypted, flash)
  • Session management (load, create, authenticate, destroy)
  • Identity shortcuts (UserID, IsAuthenticated, IsCurrentUser)
  • Role-based access control (Can with lazy role extraction)
  • Standard library context.Context compatibility
  • HTMX-aware response rendering
  • File upload/download with configurable storage
  • Background job enqueueing
  • Structured logging with request-scoped values
  • Domain and subdomain extraction
  • Custom context values

Design Principles

  • No magic: Explicit code, no reflection, no service containers
  • Flat handlers: Business logic in handlers, extract to services only when shared
  • Constructor injection: All dependencies visible in main.go
  • No context helpers: Packages receive values via parameters
  • Framework, not boilerplate: Provides utilities, not business logic

See the forge package documentation for the public API and usage examples.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSessionNotConfigured is returned when session functionality is used
	// but WithSession was not configured on the app.
	ErrSessionNotConfigured = errors.New("session: not configured")

	// ErrSessionNotFound is returned when a session does not exist.
	ErrSessionNotFound = errors.New("session: not found")

	// ErrSessionExpired is returned when a session has expired.
	ErrSessionExpired = errors.New("session: expired")

	// ErrSessionInvalidToken is returned when a session token is invalid.
	ErrSessionInvalidToken = errors.New("session: invalid token")

	// ErrSessionFingerprintMismatch is returned when session fingerprint validation fails.
	// This may indicate a session hijacking attempt.
	ErrSessionFingerprintMismatch = errors.New("session: fingerprint mismatch")
)

Functions

func ContextValue

func ContextValue[T any](c Context, key any) T

func IsHTTPError

func IsHTTPError(err error) bool

func LoadConfig

func LoadConfig(dst any) error

LoadConfig parses environment variables into dst using struct tags. It loads .env from the working directory automatically via godotenv. Struct fields use `env:"KEY"`, `envDefault:"value"`, and `envSeparator:","` tags to declare their bindings.

func Param

func Param[T ~string | ~int | ~int64 | ~float64 | ~bool](c Context, name string) T

func Query

func Query[T ~string | ~int | ~int64 | ~float64 | ~bool](c Context, name string) T

func QueryDefault

func QueryDefault[T ~string | ~int | ~int64 | ~float64 | ~bool](c Context, name string, defaultValue T) T

QueryDefault retrieves a typed query parameter with a default value. Returns defaultValue if the parameter is empty or cannot be parsed.

func Run

func Run(cfg RunConfig, opts ...RunOption) error

Run starts a multi-domain HTTP server and blocks until shutdown. Use this for composing multiple Apps under different domain patterns. If any Apps have job workers configured, they start automatically before serving requests and stop gracefully during shutdown.

Types

type App

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

App orchestrates the application lifecycle. It manages HTTP routing, middleware, and graceful shutdown. App is immutable after creation - all configuration is done via New().

func New

func New(cfg AppConfig, opts ...Option) *App

New creates a new application with the given config and options. The App is immutable after creation.

func (*App) JobWorker

func (a *App) JobWorker() *JobManager

JobWorker returns the job worker if configured, nil otherwise. This is used internally for multi-domain routing to collect workers.

func (*App) Router

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

Router returns the underlying chi.Router for the App. This is used internally for composing multi-domain routing.

func (*App) Run

func (a *App) Run(cfg RunConfig, opts ...RunOption) error

Run starts a single-domain HTTP server and blocks until shutdown. This is a convenience method for the common single-app case. If job workers are configured, they start automatically before serving requests and stop gracefully during shutdown.

type AppConfig

type AppConfig struct {
	BaseDomain     string        `env:"BASE_DOMAIN"`
	RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
}

AppConfig holds externally configurable application settings.

type CheckFunc

type CheckFunc func(ctx context.Context) error

CheckFunc is the standard health check function signature. This matches existing healthcheck closures in pg, redis, cqrs, and jobs packages.

type Component

type Component interface {
	Render(ctx context.Context, w io.Writer) error
}

Component is the interface for renderable templates. This is compatible with templ.Component.

type Context

type Context interface {
	context.Context

	// Request returns the underlying *http.Request.
	Request() *http.Request

	// Response returns the underlying http.ResponseWriter.
	Response() http.ResponseWriter

	// Context returns the request's context.Context.
	Context() context.Context

	// Param returns the URL parameter value by name.
	// Returns empty string if the parameter doesn't exist.
	Param(name string) string

	// Query returns the query parameter value by name.
	// Returns empty string if the parameter doesn't exist.
	Query(name string) string

	// QueryDefault returns the query parameter value or a default.
	QueryDefault(name, defaultValue string) string

	// Form returns the form value by name.
	// Calls ParseForm/ParseMultipartForm internally on first access.
	// Returns empty string if the field doesn't exist.
	Form(name string) string

	// FormFile returns the first file for the given form key.
	// Returns the file, its header, and any error.
	FormFile(name string) (multipart.File, *multipart.FileHeader, error)

	// UserID returns the authenticated user's ID from the session.
	// Loads the session lazily on first call.
	// Returns empty string if no session, no session manager, or no user.
	UserID() string

	// IsAuthenticated returns true if a user is associated with the session.
	IsAuthenticated() bool

	// IsCurrentUser returns true if the authenticated user's ID matches the given id.
	IsCurrentUser(id string) bool

	// Can returns true if the current user's role grants the given permission.
	// Returns false if RBAC is not configured or the user has no matching permission.
	// The role is extracted lazily and cached for the lifetime of the request.
	Can(permission Permission) bool

	// Role returns the current user's role string.
	// The role is extracted lazily and cached for the lifetime of the request.
	// Returns empty string if RBAC is not configured.
	Role() string

	// Domain returns the normalized domain from the request Host header.
	// Strips port, handles IPv6, and converts to lowercase.
	Domain() string

	// Subdomain extracts the subdomain from the request.
	// Uses the base domain configured via AppConfig.BaseDomain.
	// Returns empty string if no base domain configured or host doesn't match.
	Subdomain() string

	// Header returns the request header value by name.
	Header(name string) string

	// SetHeader sets a response header.
	SetHeader(name, value string)

	// JSON writes a JSON response with the given status code.
	JSON(code int, v any) error

	// String writes a plain text response with the given status code.
	String(code int, s string) error

	// NoContent writes a response with no body.
	NoContent(code int) error

	// Redirect redirects to the given URL with the given status code.
	// Handles both regular HTTP redirects and HTMX requests.
	Redirect(code int, url string) error

	// Error creates and returns an HTTPError without writing a response.
	// The error should be returned from the handler to trigger the error handler.
	Error(code int, message string, opts ...HTTPErrorOption) *HTTPError

	// IsHTMX returns true if the request originated from HTMX.
	IsHTMX() bool

	// Render renders a component with the given status code.
	// For HTMX requests: always uses HTTP 200 (HTMX requires 2xx for swapping).
	// For regular requests: uses the provided status code.
	// Compatible with templ.Component.
	// Optional render options configure HTMX response headers.
	Render(code int, component Component, opts ...htmx.RenderOption) error

	// RenderPartial renders different components based on request type.
	// For HTMX requests: renders partial with HTTP 200.
	// For regular requests: renders fullPage with the provided status code.
	// Optional render options configure HTMX response headers (only applied for HTMX requests).
	RenderPartial(code int, fullPage, partial Component, opts ...htmx.RenderOption) error

	// Bind binds form data, sanitizes, and validates into a struct.
	// Returns validation errors separately from system errors.
	Bind(v any) (ValidationErrors, error)

	// BindQuery binds query parameters, sanitizes, and validates into a struct.
	// Returns validation errors separately from system errors.
	BindQuery(v any) (ValidationErrors, error)

	// BindJSON binds JSON body, sanitizes, and validates into a struct.
	// Returns validation errors separately from system errors.
	BindJSON(v any) (ValidationErrors, error)

	// Written returns true if a response has already been written.
	Written() bool

	// Logger returns the logger for advanced usage.
	Logger() *slog.Logger

	// LogDebug logs a debug message with optional attributes.
	LogDebug(msg string, attrs ...any)

	// LogInfo logs an info message with optional attributes.
	LogInfo(msg string, attrs ...any)

	// LogWarn logs a warning message with optional attributes.
	LogWarn(msg string, attrs ...any)

	// LogError logs an error message with optional attributes.
	LogError(msg string, attrs ...any)

	// Set stores a value in the request context.
	// The value can be retrieved using Get or from c.Context().Value(key).
	Set(key any, value any)

	// Get retrieves a value from the request context.
	// Returns nil if the key is not found.
	Get(key any) any

	// Cookie returns a plain cookie value.
	Cookie(name string) (string, error)

	// SetCookie sets a plain cookie.
	SetCookie(name, value string, maxAge int)

	// DeleteCookie removes a cookie.
	DeleteCookie(name string)

	// CookieSigned returns a signed cookie value.
	// Returns cookie.ErrNoSecret if no secret is configured.
	CookieSigned(name string) (string, error)

	// SetCookieSigned sets a signed cookie.
	// Returns cookie.ErrNoSecret if no secret is configured.
	SetCookieSigned(name, value string, maxAge int) error

	// CookieEncrypted returns an encrypted cookie value.
	// Returns cookie.ErrNoSecret if no secret is configured.
	CookieEncrypted(name string) (string, error)

	// SetCookieEncrypted sets an encrypted cookie.
	// Returns cookie.ErrNoSecret if no secret is configured.
	SetCookieEncrypted(name, value string, maxAge int) error

	// Flash reads and deletes a flash message.
	// Returns cookie.ErrNoSecret if no secret is configured.
	Flash(key string, dest any) error

	// SetFlash sets a flash message.
	// Returns cookie.ErrNoSecret if no secret is configured.
	SetFlash(key string, value any) error

	// Session returns the current session, auto-creating an anonymous session if needed.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	Session() (*Session, error)

	// InitSession creates a new session for this request.
	// Idempotent - no-op if session already exists.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	InitSession() error

	// AuthenticateSession associates a user with the session and rotates the token.
	// Auto-creates a session if one doesn't exist.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	AuthenticateSession(userID string) error

	// SessionValue retrieves a value from the session.
	// Auto-creates an anonymous session if needed.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	SessionValue(key string) (any, error)

	// SetSessionValue stores a value in the session.
	// Auto-creates an anonymous session if needed.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	SetSessionValue(key string, val any) error

	// DeleteSessionValue removes a value from the session.
	// Auto-creates an anonymous session if needed.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	DeleteSessionValue(key string) error

	// DestroySession removes the session and clears the cookie.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	DestroySession() error

	// DestroyOtherSessions removes all sessions for the current user except this one.
	// No-op if the session is not authenticated.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	DestroyOtherSessions() error

	// DestroyAllSessions removes all sessions for the given user.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	DestroyAllSessions(userID string) error

	// ListSessions returns all sessions for the given user.
	// Returns ErrSessionNotConfigured if WithSession was not called.
	ListSessions(userID string) ([]*Session, error)

	// ResponseWriter returns the underlying ResponseWriter for advanced usage.
	// Returns nil if not using the wrapped response writer.
	ResponseWriter() *ResponseWriter

	// Enqueue adds a job to the queue for background processing.
	// Returns job.ErrSessionNotConfigured if WithJobs was not called.
	// Returns job.ErrUnknownTask if the task name is not registered.
	Enqueue(name string, payload any, opts ...job.EnqueueOption) error

	// EnqueueTx adds a job to the queue within a transaction.
	// The job is only visible after the transaction commits.
	// Returns job.ErrSessionNotConfigured if WithJobs was not called.
	// Returns job.ErrUnknownTask if the task name is not registered.
	EnqueueTx(tx pgx.Tx, name string, payload any, opts ...job.EnqueueOption) error

	// Storage returns the configured storage client.
	// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
	Storage() (storage.Storage, error)

	// Upload extracts the named form file from the request and stores it.
	// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
	Upload(field string, opts ...storage.Option) (*storage.FileInfo, error)

	// UploadFromURL downloads a file from sourceURL and stores it.
	// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
	UploadFromURL(sourceURL string, opts ...storage.Option) (*storage.FileInfo, error)

	// Download retrieves a file from storage.
	// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
	Download(key string) (io.ReadCloser, error)

	// DeleteFile removes a file from storage.
	// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
	DeleteFile(key string) error

	// FileURL generates a URL for accessing the file.
	// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
	FileURL(key string, opts ...storage.URLOption) (string, error)

	// T translates a key using the Translator stored in context by the I18n middleware.
	// Returns the key itself if no translator is in context.
	T(key string, placeholders ...i18n.M) string

	// Tn translates a key with pluralization using the Translator stored in context.
	// Returns the key itself if no translator is in context.
	Tn(key string, n int, placeholders ...i18n.M) string

	// Language returns the resolved language from the I18n middleware.
	// Returns an empty string if no translator is in context.
	Language() string

	// FormatNumber formats a number using locale-specific separators.
	// Falls back to fmt.Sprintf if no translator is in context.
	FormatNumber(n float64) string

	// FormatCurrency formats a currency amount using locale-specific formatting.
	// Falls back to fmt.Sprintf if no translator is in context.
	FormatCurrency(amount float64) string

	// FormatPercent formats a percentage using locale-specific formatting.
	// Falls back to fmt.Sprintf if no translator is in context.
	FormatPercent(n float64) string

	// FormatDate formats a date using locale-specific formatting.
	// Falls back to time.Format if no translator is in context.
	FormatDate(date time.Time) string

	// FormatTime formats a time value using locale-specific formatting.
	// Falls back to time.Format if no translator is in context.
	FormatTime(t time.Time) string

	// FormatDateTime formats a datetime using locale-specific formatting.
	// Falls back to time.Format if no translator is in context.
	FormatDateTime(datetime time.Time) string

	// SSE streams Server-Sent Events from the channel to the client.
	// Blocks until the channel closes or the request context is cancelled.
	// Sends keepalive comments at the configured interval (default 30s).
	// Returns nil on clean exit, or an error on marshal/render failure.
	SSE(events <-chan SSEEvent) error
}

Context provides request/response access and helper methods. It also implements context.Context by delegating to the underlying request context.

type ErrorHandler

type ErrorHandler func(Context, error) error

ErrorHandler handles errors returned from handlers.

type Extractor

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

Extractor tries multiple sources in order and returns the first match.

func NewExtractor

func NewExtractor(sources ...ExtractorSource) Extractor

NewExtractor creates an Extractor that tries the given sources in order.

func (Extractor) Extract

func (e Extractor) Extract(c Context) (string, bool)

Extract iterates sources in order and returns the first non-empty value. Returns ("", false) if all sources miss.

type ExtractorSource

type ExtractorSource = func(Context) (string, bool)

ExtractorSource extracts a value from the request context. Returns the value and true if found, or ("", false) if not present.

func FromBearerToken

func FromBearerToken() ExtractorSource

FromBearerToken returns a source that reads a Bearer token from the Authorization header. Uses case-insensitive comparison on the "Bearer " prefix.

func FromCookie

func FromCookie(name string) ExtractorSource

FromCookie returns a source that reads from a plain cookie.

func FromCookieEncrypted

func FromCookieEncrypted(name string) ExtractorSource

FromCookieEncrypted returns a source that reads from an encrypted cookie.

func FromCookieSigned

func FromCookieSigned(name string) ExtractorSource

FromCookieSigned returns a source that reads from a signed cookie.

func FromForm

func FromForm(name string) ExtractorSource

FromForm returns a source that reads from a form field.

func FromHeader

func FromHeader(name string) ExtractorSource

FromHeader returns a source that reads from a request header.

func FromParam

func FromParam(name string) ExtractorSource

FromParam returns a source that reads from a URL parameter.

func FromQuery

func FromQuery(name string) ExtractorSource

FromQuery returns a source that reads from a query parameter.

func FromSession

func FromSession(key string) ExtractorSource

FromSession returns a source that reads from a session value. Tries string type assertion first, falls back to fmt.Sprint for non-string values.

type FingerprintMode

type FingerprintMode int

FingerprintMode determines which fingerprint generation algorithm to use.

Fingerprinting helps detect session hijacking by validating that requests come from the same device that created the session.

Example configuration:

app := forge.New(
    forge.WithSession(store,
        forge.WithSessionFingerprint(
            forge.FingerprintCookie,  // Mode
            forge.FingerprintWarn,    // Strictness
        ),
    ),
)
const (
	// FingerprintDisabled disables fingerprint generation and validation.
	// Use this for maximum compatibility.
	FingerprintDisabled FingerprintMode = iota

	// FingerprintCookie uses default settings, excludes IP. Best for most web apps.
	// Validates User-Agent and common request headers.
	FingerprintCookie

	// FingerprintJWT uses minimal fingerprint (User-Agent + header set), excludes Accept headers.
	// Optimized for API clients that may send varying Accept headers.
	FingerprintJWT

	// FingerprintHTMX uses only User-Agent, avoids HTMX header variations.
	// Use this for HTMX-heavy applications where HX-* headers change frequently.
	FingerprintHTMX

	// FingerprintStrict includes IP address. Use for high-security scenarios.
	// WARNING: Will cause false positives for mobile users, VPN users, and dynamic proxies.
	// Only use if your users are on stable networks (e.g., corporate intranet).
	FingerprintStrict
)

type FingerprintStrictness

type FingerprintStrictness int

FingerprintStrictness determines behavior on fingerprint mismatch.

const (
	// FingerprintWarn logs a warning but allows the session to continue.
	// Use when you want visibility without disrupting users.
	FingerprintWarn FingerprintStrictness = iota
	// FingerprintReject invalidates the session on fingerprint mismatch.
	// Returns ErrSessionFingerprintMismatch from LoadSession.
	FingerprintReject
)

type HTTPError

type HTTPError struct {
	// Err is the underlying error (for logging, not exposed to users).
	Err error

	// Message is the user-facing error message.
	Message string

	// Title is an optional title for the error (defaults derived from Code).
	Title string

	// Detail is an optional extended description.
	Detail string

	// ErrorCode is an application-specific error code (for i18n, client handling).
	ErrorCode string

	// RequestID is the request tracking ID.
	RequestID string

	// Code is the HTTP status code (e.g., 404, 500).
	Code int
}

HTTPError represents an HTTP error with all data needed for rendering. It implements the error interface and provides structured data for error handlers to render error pages or toasts.

func AsHTTPError

func AsHTTPError(err error) *HTTPError

AsHTTPError extracts the HTTPError from an error if present. Returns nil if the error is not an HTTPError.

func ErrBadRequest

func ErrBadRequest(message string, opts ...HTTPErrorOption) *HTTPError

func ErrConflict

func ErrConflict(message string, opts ...HTTPErrorOption) *HTTPError

func ErrForbidden

func ErrForbidden(message string, opts ...HTTPErrorOption) *HTTPError

func ErrInternal

func ErrInternal(message string, opts ...HTTPErrorOption) *HTTPError

func ErrNotFound

func ErrNotFound(message string, opts ...HTTPErrorOption) *HTTPError

func ErrServiceUnavailable

func ErrServiceUnavailable(message string, opts ...HTTPErrorOption) *HTTPError

func ErrTooManyRequests

func ErrTooManyRequests(message string, opts ...HTTPErrorOption) *HTTPError

func ErrUnauthorized

func ErrUnauthorized(message string, opts ...HTTPErrorOption) *HTTPError

func ErrUnprocessable

func ErrUnprocessable(message string, opts ...HTTPErrorOption) *HTTPError

func NewHTTPError

func NewHTTPError(code int, message string) *HTTPError

NewHTTPError creates a new HTTPError with the given status code and message.

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) StatusCode

func (e *HTTPError) StatusCode() int

func (*HTTPError) StatusText

func (e *HTTPError) StatusText() string

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

type HTTPErrorOption

type HTTPErrorOption func(*HTTPError)

HTTPErrorOption configures an HTTPError.

func WithDetail

func WithDetail(detail string) HTTPErrorOption

func WithError

func WithError(err error) HTTPErrorOption

func WithErrorCode

func WithErrorCode(code string) HTTPErrorOption

func WithRequestID

func WithRequestID(id string) HTTPErrorOption

func WithTitle

func WithTitle(title string) HTTPErrorOption

type Handler

type Handler interface {
	Routes(r Router)
}

Handler declares routes on a router.

Example:

type AuthHandler struct {
    repo *repository.Queries
}

func (h *AuthHandler) Routes(r forge.Router) {
    r.GET("/login", h.showLogin)
    r.POST("/login", h.handleLogin)
}

type HandlerFunc

type HandlerFunc func(c Context) error

HandlerFunc is the signature for route handlers. It receives a Context and returns an error. Returning a non-nil error triggers the error handling middleware.

type HealthCheckOption

type HealthCheckOption func(healthChecks)

HealthCheckOption adds a readiness check to the health configuration.

func HealthCheck

func HealthCheck(name string, fn CheckFunc) HealthCheckOption

HealthCheck creates a named readiness check option.

type JWTClaimsKey

type JWTClaimsKey struct{}

JWTClaimsKey is the context key used to store parsed JWT claims.

type JobEnqueuer

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

JobEnqueuer wraps the pkg/job.Enqueuer for internal use. It provides enqueueing capability without worker processing.

func NewJobEnqueuer

func NewJobEnqueuer(pool *pgxpool.Pool, opts ...job.EnqueuerOption) (*JobEnqueuer, error)

NewJobEnqueuer creates a new JobEnqueuer with the given pool and options.

func (*JobEnqueuer) Enqueue

func (je *JobEnqueuer) Enqueue(ctx context.Context, name string, payload any, opts ...job.EnqueueOption) error

Enqueue adds a job to the queue.

func (*JobEnqueuer) EnqueueTx

func (je *JobEnqueuer) EnqueueTx(ctx context.Context, tx pgx.Tx, name string, payload any, opts ...job.EnqueueOption) error

EnqueueTx adds a job to the queue within a transaction.

func (*JobEnqueuer) Enqueuer

func (je *JobEnqueuer) Enqueuer() *job.Enqueuer

Enqueuer returns the underlying job.Enqueuer.

type JobManager

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

JobManager wraps the pkg/job.Manager for internal use in the framework.

func NewJobManager

func NewJobManager(pool *pgxpool.Pool, cfg job.Config, opts ...job.Option) (*JobManager, error)

NewJobManager creates a new JobManager with the given pool, config, and options.

func (*JobManager) Manager

func (jm *JobManager) Manager() *job.Manager

Manager returns the underlying job.Manager.

func (*JobManager) Shutdown

func (jm *JobManager) Shutdown() func(context.Context) error

Shutdown returns a shutdown function for the job manager.

func (*JobManager) Start

func (jm *JobManager) Start(ctx context.Context) error

Start begins job processing.

func (*JobManager) Stop

func (jm *JobManager) Stop(ctx context.Context) error

Stop gracefully shuts down job processing.

type LanguageKey

type LanguageKey struct{}

LanguageKey is the context key used to store the resolved language string.

type Middleware

type Middleware func(next HandlerFunc) HandlerFunc

Middleware wraps a HandlerFunc to add cross-cutting concerns. Middleware can inspect/modify the request, short-circuit processing, or wrap the response.

Example:

func Auth(next forge.HandlerFunc) forge.HandlerFunc {
    return func(c forge.Context) error {
        if !isAuthenticated(c) {
            return c.Redirect(302, "/login")
        }
        return next(c)
    }
}

type Option

type Option func(*App)

Option configures the application.

func WithCookieConfig

func WithCookieConfig(cfg cookie.Config) Option

WithCookieConfig configures the cookie manager.

func WithCustomLogger

func WithCustomLogger(l *slog.Logger) Option

WithCustomLogger sets a fully custom logger. Use this when you need complete control over logging configuration.

func WithErrorHandler

func WithErrorHandler(h ErrorHandler) Option

WithErrorHandler sets a custom error handler for handler errors. Called when a handler returns a non-nil error.

func WithHandlers

func WithHandlers(h ...Handler) Option

WithHandlers registers handlers that declare routes. Each handler's Routes method is called during setup.

func WithHealthChecks

func WithHealthChecks(checks ...HealthCheckOption) Option

WithHealthChecks enables health check endpoints with hardcoded paths. Liveness: /_live — always returns OK if process is running. Readiness: /_ready — runs all configured checks.

func WithJobEnqueuer

func WithJobEnqueuer(pool *pgxpool.Pool, opts ...job.EnqueuerOption) Option

WithJobEnqueuer enables job enqueueing without worker processing. Use this for web servers that dispatch work to separate worker processes. Workers must be running elsewhere to process the enqueued jobs.

func WithJobWorker

func WithJobWorker(pool *pgxpool.Pool, cfg job.Config, opts ...job.Option) Option

WithJobWorker enables job processing without enqueueing capability. Use this for dedicated background worker processes that don't need to dispatch additional jobs. Workers are started automatically when the app runs and stopped gracefully during shutdown.

func WithJobs

func WithJobs(pool *pgxpool.Pool, cfg job.Config, opts ...job.Option) Option

WithJobs enables both job enqueueing and worker processing using River. A pgxpool.Pool is required for the job queue. Workers are started automatically when the app runs and stopped gracefully during shutdown.

func WithLogger

func WithLogger(component string, extractors ...logger.ContextExtractor) Option

WithLogger creates a logger with a component name and optional extractors. The component name is added to every log entry for easy filtering. Extractors pull values from context (e.g., request_id, user_id).

func WithMethodNotAllowedHandler

func WithMethodNotAllowedHandler(h HandlerFunc) Option

WithMethodNotAllowedHandler sets a custom 405 handler.

func WithMiddleware

func WithMiddleware(mw ...Middleware) Option

WithMiddleware adds global middleware to the application. Middleware is applied in the order provided.

func WithNotFoundHandler

func WithNotFoundHandler(h HandlerFunc) Option

WithNotFoundHandler sets a custom 404 handler.

func WithRoles

func WithRoles(permissions RolePermissions, extractor RoleExtractorFunc) Option

WithRoles configures role-based access control for the application. The permissions map defines which permissions each role grants. The extractor function determines the current user's role from the request context. Roles are extracted lazily (once per request) and cached.

func WithSSEKeepAlive

func WithSSEKeepAlive(d time.Duration) Option

WithSSEKeepAlive sets the interval for SSE keepalive comments. Defaults to 30 seconds if not set or if d <= 0.

func WithSession

func WithSession(store Store, opts ...SessionOption) Option

WithSession enables server-side session management. A Store implementation must be provided (e.g., PostgresStore). Sessions are loaded lazily and saved automatically before the response is written. Additional configuration can be provided via SessionOption functions.

func WithStaticFiles

func WithStaticFiles(pattern string, fsys fs.FS, subDir string) Option

WithStaticFiles mounts a static file handler at the given pattern. Directory listings are disabled. Files are served with default cache headers.

func WithStorage

func WithStorage(s storage.Storage) Option

WithStorage configures file storage for the application. A storage.Storage implementation must be provided (e.g., S3Client). Enables c.Upload(), c.UploadFromURL(), c.Download(), c.DeleteFile(), and c.FileURL().

type Permission

type Permission string

Permission represents a named permission string.

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

ResponseWriter wraps http.ResponseWriter to provide response interception. It tracks write status, runs hooks before the first write, and transforms status codes for HTMX requests.

func NewResponseWriter

func NewResponseWriter(w http.ResponseWriter, isHTMX bool) *ResponseWriter

NewResponseWriter creates a new ResponseWriter.

func (*ResponseWriter) Flush

func (w *ResponseWriter) Flush()

Flush implements the http.Flusher interface.

func (*ResponseWriter) Hijack

func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)

Hijack implements the http.Hijacker interface.

func (*ResponseWriter) OnBeforeWrite

func (w *ResponseWriter) OnBeforeWrite(fn func())

OnBeforeWrite registers a hook to run before the first write. Hooks are called in registration order when WriteHeader or Write is first called.

func (*ResponseWriter) Push

func (w *ResponseWriter) Push(target string, opts *http.PushOptions) error

Push implements the http.Pusher interface.

func (*ResponseWriter) Seal

func (w *ResponseWriter) Seal()

Seal prevents any further writes to the response. After sealing, both Write and WriteHeader become silent no-ops. Used by the timeout middleware to prevent the handler goroutine from corrupting the response after timeout has fired.

func (*ResponseWriter) Size

func (w *ResponseWriter) Size() int64

Size returns the number of bytes written to the response body.

func (*ResponseWriter) Status

func (w *ResponseWriter) Status() int

Status returns the HTTP status code of the response.

func (*ResponseWriter) Unwrap

func (w *ResponseWriter) Unwrap() http.ResponseWriter

Unwrap returns the underlying ResponseWriter. This allows middleware to access the original writer if needed.

func (*ResponseWriter) Write

func (w *ResponseWriter) Write(b []byte) (int, error)

func (*ResponseWriter) WriteHeader

func (w *ResponseWriter) WriteHeader(code int)

WriteHeader sends an HTTP response header with the provided status code. For HTMX requests, non-200 status codes are transformed to 200.

func (*ResponseWriter) Written

func (w *ResponseWriter) Written() bool

Written returns true if the response has been written.

type RoleExtractorFunc

type RoleExtractorFunc = func(Context) string

RoleExtractorFunc extracts the current user's role from the request context.

type RolePermissions

type RolePermissions = map[string][]Permission

RolePermissions maps role names to their granted permissions.

type Router

type Router interface {
	// GET registers a handler for GET requests.
	GET(path string, h HandlerFunc, mw ...Middleware)

	// POST registers a handler for POST requests.
	POST(path string, h HandlerFunc, mw ...Middleware)

	// PUT registers a handler for PUT requests.
	PUT(path string, h HandlerFunc, mw ...Middleware)

	// PATCH registers a handler for PATCH requests.
	PATCH(path string, h HandlerFunc, mw ...Middleware)

	// DELETE registers a handler for DELETE requests.
	DELETE(path string, h HandlerFunc, mw ...Middleware)

	// HEAD registers a handler for HEAD requests.
	HEAD(path string, h HandlerFunc, mw ...Middleware)

	// OPTIONS registers a handler for OPTIONS requests.
	OPTIONS(path string, h HandlerFunc, mw ...Middleware)

	// Group creates an inline route group.
	// All routes defined inside fn share no common pattern prefix.
	Group(fn func(r Router))

	// Route creates a route group with a pattern prefix.
	// All routes defined inside fn share the pattern prefix.
	Route(pattern string, fn func(r Router))

	// Use appends middleware to the router's middleware stack.
	Use(mw ...Middleware)

	// Mount attaches an http.Handler at the given pattern.
	// Use this for legacy handlers or third-party routers.
	Mount(pattern string, h http.Handler)
}

Router is the interface handlers use to declare routes. It provides HTTP method routing and grouping capabilities.

type RunConfig

type RunConfig struct {
	Address         string        `env:"ADDRESS"          envDefault:":8080"`
	ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"30s"`
}

RunConfig holds externally configurable runtime settings.

type RunOption

type RunOption func(*runConfig)

RunOption configures the server runtime.

func WithContext

func WithContext(ctx context.Context) RunOption

WithContext sets a custom base context for signal handling. Useful for testing or when integrating with existing context hierarchies. Defaults to context.Background() if not set.

func WithDomain

func WithDomain(pattern string, app *App) RunOption

WithDomain maps a host pattern to an App. Patterns: "api.example.com" (exact) or "*.example.com" (wildcard)

func WithFallback

func WithFallback(app *App) RunOption

WithFallback sets the default App for requests that don't match any domain. If no domains are configured, the fallback becomes the main handler.

func WithRunLogger

func WithRunLogger(l *slog.Logger) RunOption

WithRunLogger sets the application logger for the runtime. If nil, logging is disabled.

func WithShutdownHook

func WithShutdownHook(fn func(context.Context) error) RunOption

WithShutdownHook registers a cleanup function to run during shutdown. Hooks are called in the order they were registered. Each hook receives a context with the shutdown timeout.

func WithStartupHook

func WithStartupHook(fn func(context.Context) error) RunOption

WithStartupHook registers a function to run during server startup. Hooks are called in the order they were registered, after the port is bound but before serving requests. If any hook fails, the server stops and returns the error.

type SSEEvent

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

func SSEComment

func SSEComment(text string) SSEEvent

func SSEJSON

func SSEJSON(event string, v any) SSEEvent

func SSERetry

func SSERetry(ms int) SSEEvent

func SSEString

func SSEString(event, data string) SSEEvent

func SSETempl

func SSETempl(event string, component Component) SSEEvent

type Session

type Session struct {
	CreatedAt    time.Time
	LastActiveAt time.Time
	ExpiresAt    time.Time

	UserID      *string        // nil = anonymous session
	Data        map[string]any // Arbitrary session data
	ID          string         // Unique identifier (ULID)
	TokenHash   string         // SHA-256 hash of the cookie token
	IP          string         // Client IP address
	UserAgent   string         // Raw User-Agent header
	Device      string         // Parsed device info (e.g., "Chrome/128 (macOS, desktop)")
	Fingerprint string         // Device fingerprint for hijacking detection
	// contains filtered or unexported fields
}

Session represents a user session with metadata and arbitrary values.

Goroutine Safety

Session methods are NOT goroutine-safe. Session instances should only be accessed from a single request context. If you need to share session data across goroutines, make a copy of the required values first.

Auto-Creation

Sessions are automatically created when accessed via Context methods:

  • c.Session() - loads existing or creates anonymous session
  • c.SessionValue(key) - auto-creates if needed, returns value
  • c.SetSessionValue(key, val) - auto-creates if needed, stores value

No manual c.InitSession() is required. Sessions are lazily created on first access.

Session Lifecycle

Anonymous sessions are created with UserID = nil. When a user authenticates, call c.AuthenticateSession(userID) to promote the session and rotate the token for session fixation prevention.

Example:

func LoginHandler(c forge.Context) error {
    // Validate credentials...
    if err := c.AuthenticateSession(user.ID); err != nil {
        return err
    }
    return c.Redirect(http.StatusSeeOther, "/dashboard")
}

Session Limits

By default, users can have up to 5 concurrent sessions (configurable up to 10). When the limit is reached, the oldest session is automatically deleted. This prevents session accumulation and provides "logout from all devices" functionality.

func NewSession

func NewSession(token string, expiresAt time.Time) *Session

NewSession creates a new session with the given token and expiration.

func (*Session) ClearDirty

func (s *Session) ClearDirty()

ClearDirty marks the session as clean (saved). Called by the session manager after persisting changes.

func (*Session) ClearNew

func (s *Session) ClearNew()

func (*Session) DeleteValue

func (s *Session) DeleteValue(key string)

DeleteValue removes a value from the session. Marks the session as dirty only if the key existed.

WARNING: NOT goroutine-safe. Only call from the request context.

func (*Session) GetValue

func (s *Session) GetValue(key string) (any, bool)

GetValue retrieves a value from the session. Returns (value, true) if found, (nil, false) if not found.

WARNING: NOT goroutine-safe. Only call from the request context.

func (*Session) IsAuthenticated

func (s *Session) IsAuthenticated() bool

func (*Session) IsDirty

func (s *Session) IsDirty() bool

IsDirty returns true if the session has unsaved changes.

func (*Session) IsExpired

func (s *Session) IsExpired() bool

func (*Session) IsNew

func (s *Session) IsNew() bool

func (*Session) MarkDirty

func (s *Session) MarkDirty()

MarkDirty marks the session as needing to be saved.

func (*Session) SetValue

func (s *Session) SetValue(key string, val any)

SetValue stores a value in the session. Marks the session as dirty for automatic saving.

WARNING: NOT goroutine-safe. Only call from the request context.

type SessionOption

type SessionOption func(*sessionConfig)

SessionOption configures session settings.

func WithMaxSessionsPerUser

func WithMaxSessionsPerUser(max int) SessionOption

WithMaxSessionsPerUser sets the maximum concurrent sessions per user. Value is capped at maxAllowedSessionsPerUser (10).

func WithSessionCookieName

func WithSessionCookieName(name string) SessionOption

WithSessionCookieName sets the session cookie name.

This is the ONLY session-specific cookie setting. All other cookie settings (domain, path, secure, httpOnly, sameSite) are inherited from the global cookie manager configured via WithCookieConfig().

Example:

app := forge.New(
    forge.AppConfig{},
    // Configure cookie security globally
    forge.WithCookieConfig(cookie.Config{
        Domain:   ".example.com",
        Secure:   true,
        HTTPOnly: true,
        SameSite: "lax",
    }),
    // Configure session with custom cookie name
    forge.WithSession(store,
        forge.WithSessionCookieName("__session"), // Only name is customizable
    ),
)

func WithSessionFingerprint

func WithSessionFingerprint(mode FingerprintMode, strictness FingerprintStrictness) SessionOption

WithSessionFingerprint configures fingerprint validation.

func WithSessionLogger

func WithSessionLogger(logger *slog.Logger) SessionOption

WithSessionLogger sets the logger for session events.

func WithSessionTTL

func WithSessionTTL(ttl time.Duration) SessionOption

WithSessionTTL sets the session time-to-live duration.

func WithSessionTouchThreshold

func WithSessionTouchThreshold(threshold time.Duration) SessionOption

WithSessionTouchThreshold sets the minimum time between LastActiveAt updates.

type Store

type Store interface {
	// Create persists a new session.
	// The Store is responsible for serializing Session.Data to JSON.
	Create(ctx context.Context, s *Session) error

	// GetByTokenHash retrieves a session by its SHA-256 token hash.
	// Returns ErrSessionNotFound if the session doesn't exist.
	// Returns ErrSessionExpired if the session has expired.
	//
	// PERFORMANCE: This method is called on EVERY request with a session cookie.
	// It MUST use an index on token_hash column for fast lookups.
	//
	// The Store is responsible for deserializing Session.Data from JSON.
	GetByTokenHash(ctx context.Context, tokenHash string) (*Session, error)

	// Update saves changes to an existing session.
	// The Store is responsible for serializing Session.Data to JSON.
	Update(ctx context.Context, s *Session) error

	// Delete removes a session by its ID.
	Delete(ctx context.Context, id string) error

	// ListByUserID retrieves all sessions for a user.
	// Used for "view active sessions" functionality.
	// The Store is responsible for deserializing Session.Data from JSON.
	ListByUserID(ctx context.Context, userID string) ([]*Session, error)

	// CountByUserID returns the number of sessions for a user.
	// Used for enforcing session limits.
	CountByUserID(ctx context.Context, userID string) (int, error)

	// DeleteByUserID removes all sessions for a user.
	// Useful for "logout from all devices" functionality.
	DeleteByUserID(ctx context.Context, userID string) error

	// DeleteByUserIDExcept removes all sessions for a user except the specified session ID.
	// Used for "logout from other devices" functionality.
	// FIX #6: Enables batch delete instead of N+1 queries.
	DeleteByUserIDExcept(ctx context.Context, userID, exceptID string) error

	// DeleteOldestByUserID removes the oldest session for a user (by last_active_at).
	// Used for enforcing max session limits per user.
	DeleteOldestByUserID(ctx context.Context, userID string) error

	// Touch updates the LastActiveAt timestamp without loading the full session.
	// Used for activity tracking without full session updates when touch threshold is met.
	Touch(ctx context.Context, id string, lastActiveAt time.Time) error
}

Store defines the interface for session persistence.

Database Requirements

Your Store implementation MUST create the following indexes for performance:

CREATE INDEX idx_sessions_token_hash ON sessions(token_hash);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);

The token_hash index is CRITICAL for session lookup performance.

Data Serialization

The Store is responsible for serializing the Session.Data map[string]any field. For SQL databases, use JSONB or JSON column type. Example Postgres schema below.

Example Postgres Schema

CREATE TABLE sessions (
    id           TEXT PRIMARY KEY,
    token_hash   TEXT NOT NULL,
    user_id      TEXT,
    data         JSONB NOT NULL DEFAULT '{}',
    ip           TEXT NOT NULL DEFAULT '',
    user_agent   TEXT NOT NULL DEFAULT '',
    device       TEXT NOT NULL DEFAULT '',
    fingerprint  TEXT NOT NULL DEFAULT '',
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at   TIMESTAMPTZ NOT NULL
);

CREATE INDEX idx_sessions_token_hash ON sessions(token_hash);
CREATE INDEX idx_sessions_user_id ON sessions(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);

Cleanup Job

Implement a background job to delete expired sessions:

DELETE FROM sessions WHERE expires_at < NOW();

Run this hourly or daily depending on your traffic.

Session cookies inherit security settings from the global cookie manager configured via WithCookieConfig(). Only the cookie name is session-specific and can be customized via WithSessionCookieName().

Example cookie configuration:

app := forge.New(
    forge.AppConfig{},
    forge.WithCookieConfig(cookie.Config{
        Domain:   ".example.com",
        Secure:   true,      // All cookies (including sessions) are secure
        HTTPOnly: true,      // All cookies are httpOnly
        SameSite: "strict",  // All cookies use strict sameSite
    }),
    forge.WithSession(store),
)

type TranslatorKey

type TranslatorKey struct{}

TranslatorKey is the context key used to store the i18n Translator.

type ValidationErrors

type ValidationErrors = validator.ValidationErrors

ValidationErrors is a collection of validation errors.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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