diff --git a/adaptive/doc.go b/adaptive/doc.go
index 1e027cc2..3ffbf0d7 100644
--- a/adaptive/doc.go
+++ b/adaptive/doc.go
@@ -11,4 +11,8 @@
//
// Users select the adaptive engine via celeris.Config{Engine: celeris.Adaptive}.
// It is the default engine on Linux.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/engines
package adaptive
diff --git a/doc.go b/doc.go
index d89c7029..bc4974b8 100644
--- a/doc.go
+++ b/doc.go
@@ -1,624 +1,41 @@
-// Package celeris provides an ultra-low latency HTTP server with
-// dual-architecture I/O (io_uring + epoll) and a high-level API
-// for routing and request handling.
+// Package celeris is a high-performance HTTP server with dual-architecture
+// I/O (io_uring + epoll) and a high-level API for routing and request handling.
//
-// # Quick Start
+// Create a server with [New], register routes with the verb methods
+// ([Server.GET], [Server.POST], …), and serve with [Server.Start] (or
+// [Server.StartWithContext] for graceful shutdown). Routes support static
+// paths, named parameters (:id) and catch-all wildcards (*path).
//
// s := celeris.New(celeris.Config{Addr: ":8080"})
-// s.GET("/hello", func(c *celeris.Context) error {
-// return c.String(200, "Hello, World!")
+// s.GET("/users/:id", func(c *celeris.Context) error {
+// return c.JSON(200, map[string]string{"id": c.Param("id")})
// })
// log.Fatal(s.Start())
//
-// # Routing
+// Handlers have the signature [HandlerFunc] (func(*[Context]) error) and
+// receive a pooled *[Context] that carries the request and response: params
+// ([Context.Param]), query/form/body parsing ([Context.Query],
+// [Context.Bind]), and typed responses ([Context.JSON], [Context.String],
+// [Context.File], [Context.Stream]). Do not retain a *[Context] after the
+// handler returns; use [Context.BodyCopy] to keep body bytes alive.
//
-// Routes support static paths, named parameters, and catch-all wildcards:
+// Group routes with [Server.Group], attach middleware with [Server.Use],
+// [RouteGroup.Use] or [Route.Use], and return [HTTPError] (via [NewHTTPError])
+// for HTTP-status errors. The in-tree middleware/* packages (logger, recovery,
+// cors, compress, …) supply ready-made [HandlerFunc] middleware.
//
-// s.GET("/users/:id", handler) // /users/42 → Param("id") = "42"
-// s.GET("/files/*path", handler) // /files/a/b → Param("path") = "/a/b"
+// By default handlers run inline on the I/O worker; mark I/O-bound routes with
+// [Route.Async] (or flip the default via [Config.AsyncHandlers]) to dispatch
+// them on a per-connection goroutine.
//
-// # URL Parameters
+// On Linux, [Config.Engine] selects between [IOUring], [Epoll], [Adaptive] and
+// [Std]; other platforms run [Std] only. [Config.Protocol] selects [HTTP1],
+// [H2C] or [Auto] (HTTP/1.1 + h2c). [Server.Collector] exposes per-request
+// metrics. Test handlers with the github.com/goceleris/celeris/celeristest
+// helpers.
//
-// Access matched parameters by name. Type-safe parsing methods are available:
+// # Documentation
//
-// id := c.Param("id") // string
-// n, err := c.ParamInt("id") // int
-// n64, err := c.ParamInt64("id") // int64
-//
-// Query parameters support defaults and multi-values:
-//
-// page := c.Query("page") // string
-// page := c.QueryDefault("page", "1") // with default
-// limit := c.QueryInt("limit", 10) // int with default
-// tags := c.QueryValues("tag") // []string
-// all := c.QueryParams() // url.Values
-// raw := c.RawQuery() // raw query string
-//
-// # Route Groups
-//
-// api := s.Group("/api")
-// api.GET("/items", listItems)
-//
-// # Async Handlers
-//
-// By default every handler runs inline on the I/O worker — lowest latency,
-// zero handoff. Routes that block on I/O (database, RPC, file system) can
-// opt per-route into the per-connection dispatch goroutine so the worker
-// stays free to drive other connections:
-//
-// s.GET("/healthz", healthHandler) // sync (default), inline on worker
-// s.GET("/db", dbHandler).Async() // async, per-conn goroutine
-//
-// Or flip the default at the group / server level and opt routes back to
-// sync individually:
-//
-// api := s.Group("/api").Async() // async for all /api/* routes
-// api.GET("/products", productHandler)
-// api.GET("/cached", cachedHandler).Sync() // opt this one back to sync
-//
-// Precedence is route > group > server default ([Config.AsyncHandlers]).
-// Works identically across iouring, epoll and adaptive (per-request H1
-// dispatch, per-stream H2 dispatch). On the std engine the per-route flag
-// is a no-op (net/http already runs handler-per-goroutine).
-//
-// SAFETY: do NOT call .Sync() / .Async(false) on a handler that hijacks
-// or detaches the connection (WebSocket upgrade, SSE). Detached flows run
-// async by construction.
-//
-// # Middleware
-//
-// Middleware is provided by the in-tree middleware/ packages (e.g.
-// middleware/logger, middleware/recovery). Use Server.Use to register
-// middleware globally or per route group.
-//
-// import (
-// "github.com/goceleris/celeris/middleware/logger"
-// "github.com/goceleris/celeris/middleware/recovery"
-// )
-// s.Use(logger.New(), recovery.New())
-//
-// To write custom middleware, use the HandlerFunc signature and call
-// Context.Next to invoke downstream handlers. Next returns the first
-// error from downstream, which middleware can handle or propagate:
-//
-// func timing() celeris.HandlerFunc {
-// return func(c *celeris.Context) error {
-// err := c.Next()
-// elapsed := time.Since(c.StartTime())
-// c.SetHeader("x-response-time", elapsed.String())
-// return err
-// }
-// }
-//
-// # Error Handling
-//
-// Handlers return errors. Unhandled errors are caught by the routerAdapter
-// safety net: *HTTPError writes its Code+Message; bare errors write 500.
-//
-// s.GET("/data", func(c *celeris.Context) error {
-// data, err := fetchData()
-// if err != nil {
-// return celeris.NewHTTPError(500, "fetch failed")
-// }
-// return c.JSON(200, data)
-// })
-//
-// Middleware can intercept errors from downstream handlers:
-//
-// s.Use(func(c *celeris.Context) error {
-// err := c.Next()
-// if err != nil {
-// log.Println("error:", err)
-// return c.JSON(500, map[string]string{"error": "internal"})
-// }
-// return nil
-// })
-//
-// # Global Error Handler
-//
-// Register a global error handler with Server.OnError. This is called when
-// an unhandled error reaches the safety net after all middleware has had its
-// chance. Use it to render structured error responses (e.g. JSON) instead of
-// the default text/plain fallback:
-//
-// s.OnError(func(c *celeris.Context, err error) {
-// var he *celeris.HTTPError
-// code := 500
-// msg := "internal server error"
-// if errors.As(err, &he) {
-// code = he.Code
-// msg = he.Message
-// }
-// c.JSON(code, map[string]string{"error": msg})
-// })
-//
-// If the handler does not write a response, the default text/plain fallback
-// applies. OnError must be called before Start.
-//
-// # Custom 404 / 405 Handlers
-//
-// s.NotFound(func(c *celeris.Context) error {
-// return c.JSON(404, map[string]string{"error": "not found"})
-// })
-// s.MethodNotAllowed(func(c *celeris.Context) error {
-// return c.JSON(405, map[string]string{"error": "method not allowed"})
-// })
-//
-// # Graceful Shutdown
-//
-// ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
-// defer stop()
-//
-// s := celeris.New(celeris.Config{
-// Addr: ":8080",
-// ShutdownTimeout: 10 * time.Second,
-// })
-// s.GET("/ping", func(c *celeris.Context) error {
-// return c.String(200, "pong")
-// })
-// if err := s.StartWithContext(ctx); err != nil {
-// log.Fatal(err)
-// }
-//
-// # Engine Selection
-//
-// On Linux, choose between IOUring, Epoll, Adaptive, or Std engines.
-// On other platforms, only Std is available.
-//
-// s := celeris.New(celeris.Config{
-// Addr: ":8080",
-// Engine: celeris.Adaptive,
-// })
-//
-// # Protocol Selection
-//
-// The Protocol field controls HTTP version negotiation:
-//
-// celeris.HTTP1 // HTTP/1.1 only (default)
-// celeris.H2C // HTTP/2 cleartext (h2c) only
-// celeris.Auto // Auto-detect: serves both HTTP/1.1 and H2C
-//
-// Example:
-//
-// s := celeris.New(celeris.Config{
-// Addr: ":8080",
-// Protocol: celeris.Auto,
-// })
-//
-// # net/http Compatibility
-//
-// Wrap existing net/http handlers. Response bodies from adapted handlers
-// are buffered in memory (capped at 100 MB).
-//
-// s.GET("/legacy", celeris.Adapt(legacyHandler))
-//
-// # Context Lifecycle
-//
-// Context objects are pooled and recycled between requests. Do not retain
-// references to a *[Context] after the handler returns. Copy any needed
-// values before returning. For the request body specifically, use
-// Context.BodyCopy to obtain a copy that outlives the handler:
-//
-// safe := c.BodyCopy() // safe to pass to a goroutine
-//
-// When using Detach, the returned done function MUST be called — failure
-// to do so permanently leaks the Context from the pool.
-//
-// # Observability
-//
-// The Server.Collector method returns an [observe.Collector] that records
-// per-request metrics (throughput, latency histogram, error rate, active
-// connections). Use Collector.Snapshot to retrieve a point-in-time copy:
-//
-// snap := s.Collector().Snapshot()
-// fmt.Println(snap.RequestsTotal, snap.ErrorsTotal)
-//
-// For Prometheus or debug endpoint integration, see the
-// middleware/metrics and middleware/debug packages.
-//
-// # Configuration
-//
-// Config.Workers controls the number of I/O workers (default: GOMAXPROCS).
-// Config.ShutdownTimeout sets the graceful shutdown deadline for
-// StartWithContext (default: 30s).
-//
-// # Named Routes & Reverse URLs
-//
-// Assign names to routes with Route.Name, then generate URLs via Server.URL:
-//
-// s.GET("/users/:id", handler).Name("user")
-// url, _ := s.URL("user", "42") // "/users/42"
-//
-// For catch-all routes the value replaces the wildcard segment:
-//
-// s.GET("/files/*filepath", handler).Name("files")
-// url, _ := s.URL("files", "/css/style.css") // "/files/css/style.css"
-//
-// Use Server.Routes to list all registered routes.
-//
-// # Form Handling
-//
-// Parse url-encoded and multipart form bodies:
-//
-// name := c.FormValue("name")
-// all := c.FormValues("tags")
-//
-// For file uploads, use FormFile or MultipartForm:
-//
-// file, header, err := c.FormFile("avatar")
-// defer file.Close()
-//
-// # File Serving
-//
-// Serve static files with automatic content-type detection and Range support:
-//
-// s.GET("/download", func(c *celeris.Context) error {
-// return c.File("/var/data/report.pdf")
-// })
-//
-// Callers must sanitize user-supplied paths to prevent directory traversal.
-//
-// # Static File Serving
-//
-// Serve an entire directory under a URL prefix:
-//
-// s.Static("/assets", "./public")
-//
-// This is equivalent to:
-//
-// s.GET("/assets/*filepath", func(c *celeris.Context) error {
-// return c.FileFromDir("./public", c.Param("filepath"))
-// })
-//
-// # Streaming
-//
-// For simple cases, stream an io.Reader as a buffered response (capped at 100 MB):
-//
-// return c.Stream(200, "text/plain", reader)
-//
-// For true incremental streaming (SSE, chunked responses), use StreamWriter
-// with the Detach pattern:
-//
-// s.GET("/events", func(c *celeris.Context) error {
-// sw := c.StreamWriter()
-// if sw == nil {
-// return c.String(200, "streaming not supported")
-// }
-// done := c.Detach()
-// sw.WriteHeader(200, [][2]string{{"content-type", "text/event-stream"}})
-// go func() {
-// defer done()
-// defer sw.Close()
-// for event := range events {
-// sw.Write([]byte("data: " + event + "\n\n"))
-// sw.Flush()
-// }
-// }()
-// return nil
-// })
-//
-// StreamWriter returns nil if the response is currently being buffered by
-// an upstream middleware (e.g. compress, etag); call StreamWriter before
-// any middleware buffers, or use [Context.BufferDepth] to detect the
-// state. All shipped engines (std, epoll, io_uring) support streaming via
-// the H1/H2 response adapter.
-//
-// # Cookies
-//
-// Read and write cookies:
-//
-// val, err := c.Cookie("session")
-// c.SetCookie(&celeris.Cookie{Name: "session", Value: token, HTTPOnly: true})
-//
-// # Authentication
-//
-// Extract HTTP Basic Authentication credentials:
-//
-// user, pass, ok := c.BasicAuth()
-//
-// # Request Body Parsing
-//
-// Bind auto-detects the format from Content-Type:
-//
-// var user User
-// if err := c.Bind(&user); err != nil {
-// return err
-// }
-//
-// Or use format-specific methods:
-//
-// c.BindJSON(&user) // application/json
-// c.BindXML(&user) // application/xml
-//
-// For raw body access:
-//
-// body := c.Body() // []byte, valid only during handler
-// safe := c.BodyCopy() // []byte, safe to retain after handler
-// r := c.BodyReader() // io.Reader wrapper
-//
-// # Response Methods
-//
-// Context provides typed response methods:
-//
-// c.JSON(200, data) // application/json
-// c.XML(200, data) // application/xml
-// c.HTML(200, "
Hello
") // text/html
-// c.String(200, "Hello, %s", name) // text/plain (fmt.Sprintf)
-// c.Blob(200, "image/png", pngBytes) // arbitrary content type
-// c.NoContent(204) // status only, no body
-// c.Redirect(302, "/new-location") // redirect with Location header
-// c.File("/path/to/report.pdf") // file with MIME detection + Range
-// c.FileFromDir(baseDir, userPath) // safe file serving (traversal-safe)
-// c.Stream(200, "text/plain", reader) // io.Reader → response (100 MB cap)
-// c.Respond(200, data) // auto-format based on Accept header
-//
-// All response methods return ErrResponseWritten if called after a response
-// has already been sent.
-//
-// # Content Negotiation
-//
-// Inspect the Accept header and auto-select the response format:
-//
-// best := c.Negotiate("application/json", "application/xml", "text/plain")
-//
-// Or use Respond to auto-format based on Accept:
-//
-// return c.Respond(200, myStruct) // JSON, XML, or text based on Accept
-//
-// # Accept Negotiation
-//
-// Beyond content type, negotiate encodings and languages:
-//
-// enc := c.AcceptsEncodings("gzip", "br", "identity")
-// lang := c.AcceptsLanguages("en", "fr", "de")
-//
-// # Route-Level Middleware
-//
-// Attach middleware to individual routes without creating a group:
-//
-// s.GET("/admin", adminHandler).Use(authMiddleware)
-//
-// Route.Use inserts middleware before the final handler, after server/group
-// middleware. Must be called before Server.Start.
-//
-// # Response Capture
-//
-// Middleware can inspect the response body after c.Next() by opting in:
-//
-// func logger() celeris.HandlerFunc {
-// return func(c *celeris.Context) error {
-// c.CaptureResponse()
-// err := c.Next()
-// body := c.ResponseBody() // captured response body
-// ct := c.ResponseContentType() // captured Content-Type
-// // ... log body, ct ...
-// return err
-// }
-// }
-//
-// # Response Buffering
-//
-// Middleware that needs to transform response bodies (compress, ETag, cache)
-// uses BufferResponse to intercept and modify the response before it is sent:
-//
-// func compress() celeris.HandlerFunc {
-// return func(c *celeris.Context) error {
-// c.BufferResponse()
-// err := c.Next()
-// if err != nil {
-// return err
-// }
-// body := c.ResponseBody()
-// compressed := gzip(body)
-// c.SetResponseBody(compressed)
-// c.SetHeader("content-encoding", "gzip")
-// return c.FlushResponse()
-// }
-// }
-//
-// BufferResponse is depth-tracked: multiple middleware layers can each call
-// BufferResponse, and the response is only sent when the outermost layer
-// calls FlushResponse. If middleware forgets to flush, a safety net in the
-// handler adapter auto-flushes the response.
-//
-// CaptureResponse and BufferResponse serve different purposes. CaptureResponse
-// is read-only: the response is written to the wire AND a copy is captured for
-// inspection (ideal for loggers). BufferResponse defers the wire write entirely,
-// allowing middleware to transform the body before sending (ideal for compress,
-// ETag, cache). If both are active, BufferResponse takes precedence.
-//
-// # Response Inspection
-//
-// Check response state from middleware after calling c.Next():
-//
-// written := c.IsWritten() // true after response sent to wire
-// size := c.BytesWritten() // response body size in bytes
-// status := c.StatusCode() // status code set by handler
-// status := c.ResponseStatus() // captured status (with BufferResponse)
-// hdrs := c.RequestHeaders() // all request headers as [][2]string
-//
-// # Error Types
-//
-// Handlers signal HTTP errors via HTTPError:
-//
-// return celeris.NewHTTPError(404, "user not found")
-// return celeris.NewHTTPError(500, "db error").WithError(err) // wrap cause
-//
-// Sentinel errors for common conditions:
-//
-// celeris.ErrResponseWritten // response already sent
-// celeris.ErrEmptyBody // Bind called with empty body
-// celeris.ErrNoCookie // Cookie() with missing cookie
-// celeris.ErrHijackNotSupported // Hijack on unsupported connection
-//
-// # Flow Control
-//
-// Middleware calls Next to invoke downstream handlers. Abort stops the chain:
-//
-// err := c.Next() // call next handler; returns first error
-// c.Abort() // stop chain (does not write response)
-// c.AbortWithStatus(403) // stop chain and send status code
-// c.IsAborted() // true if Abort was called
-//
-// # Key-Value Storage
-//
-// Store request-scoped data for sharing between middleware and handlers.
-// Three storage surfaces are provided; pick the narrowest one that fits
-// the value type:
-//
-// // Generic any-typed store. Backwards compatible; boxes into interface{}.
-// c.Set("userID", 42)
-// v, ok := c.Get("userID")
-// all := c.Keys() // copy of all stored pairs
-//
-// // Typed string store — no interface boxing, 1 alloc saved per layer.
-// c.SetString("tenantID", "acme-prod")
-// s, ok := c.GetString("tenantID")
-//
-// // Dedicated zero-alloc request-ID field. Same value surfaces via
-// // c.Get(celeris.RequestIDKey) for back-compat.
-// c.SetRequestID("abcd-1234")
-// id := c.RequestID()
-//
-// GetString falls back to the any-typed c.keys map and to the dedicated
-// request-ID field, so code calling c.Get(key) continues to see values
-// written via c.SetString. The csrf, basicauth, keyauth, and requestid
-// middleware use the typed accessors; custom middleware that carry
-// string-only request-scoped values should prefer them too.
-//
-// # Content-Disposition
-//
-// Set Content-Disposition headers for file downloads or inline display:
-//
-// c.Attachment("report.pdf") // prompts download
-// c.Inline("image.png") // suggests inline display
-//
-// # Request Detection
-//
-// Detect request characteristics:
-//
-// c.IsWebSocket() // true if Upgrade: websocket
-// c.IsTLS() // true if X-Forwarded-Proto is "https"
-//
-// # Form Presence
-//
-// FormValueOK distinguishes a missing field from an empty value:
-//
-// val, ok := c.FormValueOK("name")
-// if !ok {
-// // field was not submitted
-// }
-//
-// # Request Inspection
-//
-// Additional request accessors:
-//
-// scheme := c.Scheme() // "http" or "https" (checks X-Forwarded-Proto)
-// ip := c.ClientIP() // from X-Forwarded-For or X-Real-Ip
-// method := c.Method() // HTTP method
-// path := c.Path() // path without query string
-// full := c.FullPath() // matched route pattern (e.g. "/users/:id")
-// raw := c.RawQuery() // raw query string without leading '?'
-//
-// # Remote Address
-//
-// Context.RemoteAddr returns the TCP peer address. On native engines (epoll,
-// io_uring), the address is captured from accept(2) or getpeername(2). On the
-// std engine, it comes from http.Request.RemoteAddr.
-//
-// addr := c.RemoteAddr() // e.g. "192.168.1.1:54321"
-//
-// # Host
-//
-// Context.Host returns the request host, checking the :authority pseudo-header
-// first (HTTP/2) then falling back to the Host header (HTTP/1.1):
-//
-// host := c.Host() // e.g. "example.com"
-//
-// # Connection Hijacking
-//
-// HTTP/1.1 connections can be hijacked on all engines for WebSocket or
-// other protocols that require raw TCP access:
-//
-// conn, err := c.Hijack()
-// if err != nil {
-// return err
-// }
-// defer conn.Close()
-// // conn is a net.Conn — caller owns the connection
-//
-// HTTP/2 connections cannot be hijacked because multiplexed streams
-// share a single TCP connection.
-//
-// # Graceful Restart
-//
-// Start the server with a pre-existing listener for zero-downtime deploys:
-//
-// ln, _ := celeris.InheritListener("LISTEN_FD")
-// if ln != nil {
-// log.Fatal(s.StartWithListener(ln))
-// } else {
-// log.Fatal(s.Start())
-// }
-//
-// # Config Surface Area
-//
-// In addition to the basic config fields, the following tuning fields
-// are available:
-//
-// celeris.Config{
-// // Basic
-// Addr: ":8080",
-// Protocol: celeris.Auto, // HTTP1, H2C, or Auto
-// Engine: celeris.Adaptive, // IOUring, Epoll, Adaptive, Std
-//
-// // Workers
-// Workers: 0, // I/O workers (default GOMAXPROCS)
-//
-// // Timeouts (0 = default: 300s read/write, 600s idle; -1 = no timeout)
-// ReadTimeout: 0,
-// WriteTimeout: 0,
-// IdleTimeout: 0,
-// ShutdownTimeout: 30*time.Second,
-//
-// // Limits
-// MaxFormSize: 0, // multipart form memory (0 = 32 MB; -1 = unlimited)
-// MaxConns: 0, // max connections per worker (0 = unlimited)
-//
-// // H2 Tuning
-// MaxConcurrentStreams: 100, // H2 streams per connection
-// MaxFrameSize: 16384, // H2 frame payload size
-// InitialWindowSize: 65535, // H2 stream flow-control window
-// MaxHeaderBytes: 0, // header block size (0 = 16 MB)
-//
-// // I/O
-// DisableKeepAlive: false, // disable HTTP keep-alive
-// BufferSize: 0, // per-connection I/O buffer (0 = engine default)
-// SocketRecvBuf: 0, // SO_RCVBUF (0 = OS default)
-// SocketSendBuf: 0, // SO_SNDBUF (0 = OS default)
-//
-// // Observability
-// DisableMetrics: false, // skip per-request metric recording
-// Logger: nil, // *slog.Logger for engine diagnostics
-//
-// // Connection callbacks (must be fast — blocks the event loop)
-// OnConnect: func(addr string) { ... },
-// OnDisconnect: func(addr string) { ... },
-// }
-//
-// # Listener Address
-//
-// After Start or StartWithContext, Server.Addr returns the bound address.
-// This is useful when listening on ":0" to discover the OS-assigned port:
-//
-// addr := s.Addr() // net.Addr; use addr.String() for "127.0.0.1:49152"
-//
-// # Testing
-//
-// The github.com/goceleris/celeris/celeristest package provides test helpers:
-//
-// ctx, rec := celeristest.NewContext("GET", "/hello")
-// defer celeristest.ReleaseContext(ctx)
-// handler(ctx)
-// // inspect rec.StatusCode, rec.Headers, rec.Body
+// Full guides, tutorials and examples: https://goceleris.dev/docs
+// Start here: https://goceleris.dev/docs/getting-started
package celeris
diff --git a/driver/internal/async/doc.go b/driver/internal/async/doc.go
index 0c13ed81..471dd93a 100644
--- a/driver/internal/async/doc.go
+++ b/driver/internal/async/doc.go
@@ -21,7 +21,7 @@
// - [Backoff]: exponential-with-jitter delay computation for retry
// loops. Not safe for concurrent use.
//
-// - Internal [health] checker scheduled by the pool.
+// - Internal health checker scheduled by the pool.
//
// Drivers build their user-facing API (postgres.Pool, redis.Client) by
// composing these primitives with a protocol-specific dispatch loop.
diff --git a/driver/memcached/doc.go b/driver/memcached/doc.go
index a18f1f92..35dae85f 100644
--- a/driver/memcached/doc.go
+++ b/driver/memcached/doc.go
@@ -1,217 +1,43 @@
-// Package memcached is celeris's native Memcached driver. It speaks both the
-// text and binary wire protocols directly on top of the celeris event loop,
-// using the same async-bridge architecture as the Redis and PostgreSQL
-// drivers: one in-flight request enqueued per conn, responses demuxed from
-// the loop's recv callback, and per-worker idle pools so handlers stay on
-// the same CPU that dispatched them.
-//
-// # Usage
-//
-// client, err := memcached.NewClient("localhost:11211",
-// memcached.WithEngine(srv), // optional; omit for a standalone loop
-// memcached.WithProtocol(memcached.ProtocolText),
-// )
+// Package memcached is celeris's native Memcached driver, speaking the text
+// and binary wire protocols directly on the celeris event loop.
+//
+// [NewClient] returns a [Client], a pooled single-node handle whose typed API
+// covers storage (Set, Add, Replace, Append, Prepend, CAS and their *Bytes
+// variants), retrieval (Get, GetBytes, GetMulti, GetMultiBytes, Gets for the
+// CAS token), arithmetic (Incr, Decr), key ops (Delete, Touch), and server
+// ops (Flush, FlushAfter, Stats, Version, Ping). [ClusterClient], built with
+// [NewClusterClient], mirrors the same command surface but shards keys across
+// several nodes via a libmemcached-compatible ketama ring with passive failure
+// detection and background health probing.
+//
+// Behavior is tuned with [Option] values passed to [NewClient]: [WithProtocol]
+// ([ProtocolText] default, [ProtocolBinary] opt-in), [WithEngine] to colocate
+// socket I/O on a celeris.Server's worker threads, [WithMaxOpen],
+// [WithMaxIdlePerWorker], [WithTimeout], [WithDialTimeout], [WithMaxLifetime],
+// [WithMaxIdleTime], and [WithHealthCheckInterval]. Addresses may carry an
+// optional "memcache://" or "memcached://" scheme prefix.
+//
+// Misses and precondition failures surface as the sentinels [ErrCacheMiss],
+// [ErrNotStored], and [ErrCASConflict]; other client-side faults as
+// [ErrClosed], [ErrProtocol], [ErrPoolExhausted], [ErrMalformedKey],
+// [ErrInvalidCAS], and (for clusters) [ErrNoNodes]. Other server-side replies arrive as [*MemcachedError].
+//
+// TLS, SASL authentication, and server-side compression are not supported;
+// deploy over a trusted network and compress large values client-side.
+// [ClusterClient] topology is static — to add or remove nodes, rebuild the
+// client or front the fleet with a proxy such as mcrouter.
+//
+// client, err := memcached.NewClient("localhost:11211")
+// if err != nil {
+// log.Fatal(err)
+// }
// defer client.Close()
-//
// if err := client.Set(ctx, "key", "value", time.Minute); err != nil {
// log.Fatal(err)
// }
// v, err := client.Get(ctx, "key")
//
-// [WithEngine] routes the driver's FDs through the same epoll/io_uring
-// instance as the HTTP workers. When it is omitted, a package-level
-// standalone loop is resolved and reference-counted
-// (driver/internal/eventloop).
-//
-// # DSNs
-//
-// Addresses may optionally include a "memcache://" or "memcached://" scheme
-// prefix; the prefix is stripped. The resulting "host:port" is passed to
-// net.Dialer verbatim. TLS is not supported in v1.4.0.
-//
-// # Options
-//
-// - [WithProtocol] selects the wire dialect (text or binary); default text.
-// - [WithMaxOpen] caps the total number of connections across workers.
-// - [WithMaxIdlePerWorker] bounds per-worker idle pool size.
-// - [WithTimeout] sets an advisory per-op deadline when ctx carries none.
-// - [WithDialTimeout] sets the TCP dial timeout.
-// - [WithMaxLifetime] / [WithMaxIdleTime] bound pooled-conn age and idleness.
-// - [WithHealthCheckInterval] configures the background health sweep.
-// - [WithEngine] wires the driver into a celeris.Server's event loop.
-//
-// # Typed Commands
-//
-// The typed API on [Client] covers:
-//
-// - Storage: Set, Add, Replace, Append, Prepend, CAS.
-// - Retrieval: Get, GetBytes, GetMulti, GetMultiBytes, Gets (returns CAS).
-// - Arithmetic: Incr, Decr.
-// - Keys: Delete, Touch.
-// - Server: Flush, FlushAfter, Stats, Version, Ping.
-//
-// Values passed to Set / Add / Replace / CAS may be string, []byte, integer,
-// float, bool, nil, or fmt.Stringer. Anything else returns an error.
-//
-// # Errors
-//
-// - [ErrCacheMiss] — key not found.
-// - [ErrNotStored] — Add/Replace/Append/Prepend precondition failed.
-// - [ErrCASConflict] — CAS token mismatch (or concurrent modification).
-// - [ErrClosed] — command issued against a closed client.
-// - [ErrProtocol] — reply did not parse.
-// - [ErrMalformedKey] — key violates text-protocol constraints (whitespace,
-// control bytes, > 250 bytes).
-// - [ErrPoolExhausted] — all idle conns are stale (rare; pool blocks when full).
-//
-// Non-sentinel server errors surface as [*MemcachedError] carrying the reply
-// kind ("ERROR", "CLIENT_ERROR", "SERVER_ERROR") or, in binary mode, the
-// raw status code.
-//
-// # Pool
-//
-// Like the Redis and Postgres drivers, memcached uses [async.Pool] for
-// worker-affinity connection pooling. Each worker owns an idle list guarded
-// by its own lock; Acquire prefers the caller's worker before scanning
-// neighbors. Acquire blocks when MaxOpen is reached (wait-queue semantics)
-// rather than returning ErrPoolExhausted.
-//
-// # WithEngine and standalone operation
-//
-// [WithEngine] is optional. When omitted, [NewClient] resolves a standalone
-// event loop backed by the platform's best mechanism (epoll on Linux,
-// goroutine-per-conn on Darwin). The standalone loop is reference-counted
-// inside driver/internal/eventloop and shared across all drivers that omit
-// WithEngine. Correctness is identical with or without WithEngine; sharing
-// the HTTP server's event loop reduces per-IO syscalls and improves data
-// locality because driver FDs land on the same worker goroutine as HTTP
-// handlers.
-//
-// # Text vs binary protocol
-//
-// The text protocol is the default. Every memcached deployment supports it,
-// responses are self-describing, and the parser is trivially debugged by
-// reading a PCAP. The binary protocol is opt-in via [WithProtocol]; it adds
-// an opaque correlation ID, fixed-size framing, and explicit status codes.
-// The binary dialect is useful where malformed user input or embedded NULs
-// in values must round-trip cleanly. Both dialects truly pipeline
-// [Client.GetMulti] in a single round trip — binary uses OpGetKQ + OpNoop,
-// text uses the multi-key `get` command. Multi-packet server replies such
-// as binary Stats are decoded natively on the active conn.
-//
-// # Expiration times
-//
-// Callers pass a Go time.Duration relative TTL. The driver rewrites it on
-// the wire as either a relative second-count (when <= 30 days) or an
-// absolute Unix timestamp (when > 30 days), matching the memcached server
-// convention. Pass 0 for no expiration.
-//
-// # Key validation
-//
-// Keys are validated against text-protocol rules (1..250 bytes, no
-// whitespace or control bytes) uniformly in both dialects, so that callers
-// who switch from text to binary do not accidentally ship previously
-// valid-looking keys that the text server would have rejected.
-//
-// # Multi-server sharding (ClusterClient)
-//
-// Production memcached deployments fan keys across N independent nodes; the
-// memcached protocol has no cluster-aware peer discovery, so sharding is a
-// client-side concern. [ClusterClient] owns a static ring of [Client]
-// instances (one per configured address) and routes each key via a
-// libmemcached-compatible ketama consistent-hash ring.
-//
-// cc, err := memcached.NewClusterClient(memcached.ClusterConfig{
-// Addrs: []string{
-// "memcache-a:11211",
-// "memcache-b:11211",
-// "memcache-c:11211",
-// },
-// // Optional: relative weights (nil = equal).
-// // Weights: []uint32{1, 2, 1},
-// })
-// defer cc.Close()
-// cc.Set(ctx, "user:42", "payload", time.Hour)
-//
-// The ring assigns 160 virtual nodes per unit weight, placed at
-// MD5("-") boundaries (4 ring points per digest). Lookup uses
-// CRC32-IEEE on the user key and a binary search with wrap-around. Removing
-// one of N nodes only re-homes the ~1/N share of keys it owned; the rest
-// keep their owner, matching the usual ketama guarantee.
-//
-// The [ClusterClient] API mirrors [Client] command-for-command:
-//
-// - Single-key ops (Get, Set, Delete, Incr, ...) route to pickNode(key).
-// - GetMulti / GetMultiBytes partition keys by owner, fan out one
-// sub-request per node in parallel, and merge the per-node responses.
-// On error the first error wins; partial results from other nodes are
-// discarded.
-// - Stats, Version return a per-node map keyed by address.
-// - Flush, FlushAfter, Ping fan out to every node; the first error wins.
-// - NodeFor(key) exposes the ring's routing decision for debugging and
-// for out-of-band work that needs to track per-node state.
-//
-// Topology is static for the lifetime of the client: memcached nodes do
-// not gossip and [ClusterClient] does not run a background refresh loop.
-// Adding or removing a node requires tearing down the client and building
-// a new one. For a dynamically-changing fleet, deploy a proxy such as
-// mcrouter or twemproxy in front of the nodes and point the single-node
-// [Client] at the proxy.
-//
-// # Node failure detection and failover (v1.4.1)
-//
-// [ClusterClient] tracks the health of each node and automatically
-// reroutes around failed ones so a single dead node does not translate
-// into per-key errors for every key in its slice of the ring.
-//
-// Detection is passive + probe:
-//
-// - Each [Client] operation issued through the cluster updates the
-// node's consecutive-failure counter. Infrastructure errors (dial
-// failures, I/O errors, protocol corruption) count; protocol-level
-// responses ([ErrCacheMiss], [ErrNotStored], [ErrCASConflict],
-// [*MemcachedError]) do NOT — they are valid server replies.
-// - Once the counter reaches [ClusterConfig.FailureThreshold] (default
-// 2), the node is marked failing. [ClusterClient.pickNode] then
-// walks the node list clockwise from the failing node's position
-// and returns the first healthy neighbor it finds.
-// - A background goroutine (configurable via
-// [ClusterConfig.HealthProbeInterval], default 5s) pings any
-// failing node that has been down for at least 1 second and clears
-// the failing flag on success.
-// - Any successful operation passively clears the flag — so a node
-// that silently recovers while still receiving traffic from
-// passed-through requests is picked up immediately.
-//
-// Key-redistribution implications:
-//
-// - The successor is the next node in insertion order (the order
-// given in [ClusterConfig.Addrs]), NOT the next ring neighbor.
-// This preserves the consistent-hash invariant that all keys
-// formerly routed to failing node N now route to the same
-// successor — rather than being scattered across the ring.
-// - During the failure transition, keys that were cached on N will
-// MISS on the successor. During recovery, keys that were cached on
-// the successor will MISS on N. Applications must tolerate both
-// transitions — caches are not authoritative.
-//
-// Observability: [ClusterClient.NodeHealth] returns a snapshot of the
-// failing flag per address; [ClusterClient.NodeStatsMap] includes the
-// last-fail timestamp and consecutive-fail counter.
-//
-// Semantics vs. gomemcache/dalli: celeris matches dalli's "mark after
-// N failures, passive heal on success" policy. The background probe
-// is an addition for deployments where a failing node receives no
-// traffic (otherwise the passive path would never clear the flag).
-//
-// # Known limitations (v1.4.0)
+// # Documentation
//
-// - No TLS — deploy over VPC, loopback, or a sidecar.
-// - No SASL authentication; memcached servers configured with -Y will
-// reject the driver's handshake. Deploy with network-level auth.
-// - No server-side compression. Values are sent as-is; large payloads
-// should be compressed on the client side.
-// - [ClusterClient] topology is static: no background refresh. Memcached
-// nodes do not gossip. To add/remove nodes at runtime, tear down and
-// rebuild the client, or front the fleet with mcrouter.
+// Full guides and examples: https://goceleris.dev/docs/data-stores
package memcached
diff --git a/driver/postgres/doc.go b/driver/postgres/doc.go
index a2baa8ad..6730d397 100644
--- a/driver/postgres/doc.go
+++ b/driver/postgres/doc.go
@@ -1,226 +1,46 @@
// Package postgres is celeris's native PostgreSQL driver. It speaks the
// PostgreSQL v3 wire protocol directly on top of the celeris event loop,
-// exposes a database/sql compatible driver, and also provides a lower-level
-// worker-affinity Pool for callers that want to skip database/sql overhead.
+// exposes a database/sql-compatible driver, and also provides a lower-level
+// worker-affinity [Pool] for callers that want to skip database/sql overhead.
//
-// # Usage
+// There are two entry points. Use database/sql for portability (works with
+// any ORM); use the direct [Pool] for peak throughput.
//
-// Two entry points are supported. Which one to pick depends on whether the
-// caller wants portability (database/sql + any ORM) or peak throughput
-// (direct Pool bound to a celeris.Server).
-//
-// (a) database/sql
-//
-// import (
-// "database/sql"
-// _ "github.com/goceleris/celeris/driver/postgres"
-// )
-//
-// db, err := sql.Open("celeris-postgres", "postgres://app:pass@localhost/mydb?sslmode=disable")
-//
-// database/sql owns the pool in this mode. The driver registers itself under
-// [DriverName] in its init. Every *sql.Conn handed out by db is a pgConn
-// running on a standalone event loop (one is resolved and reference-counted
-// inside driver/internal/eventloop).
-//
-// (b) Direct Pool
+// // database/sql — registers itself under DriverName in init.
+// import _ "github.com/goceleris/celeris/driver/postgres"
+// db, err := sql.Open(postgres.DriverName, "postgres://app:pass@localhost/mydb?sslmode=disable")
//
+// // Direct Pool — optionally bound to a celeris.Server event loop.
// pool, err := postgres.Open(dsn, postgres.WithEngine(srv))
-// defer pool.Close()
-//
-// rows, err := pool.QueryContext(ctx, "SELECT id, name FROM users WHERE tenant = $1", tenantID)
-//
-// The direct Pool pins each connection to a worker (see [WithWorker] for the
-// context-based hint API), so a handler running on worker N preferentially
-// re-uses conns whose event-loop callbacks also land on worker N. When
-// [WithEngine] is passed, the Pool shares the same epoll/io_uring instance
-// as the HTTP workers; otherwise a standalone driver loop is resolved.
-//
-// # DSNs
-//
-// Both the libpq URL form and the key=value form are accepted.
-//
-// URL form:
-//
-// postgres://user:pass@host:5432/dbname?sslmode=disable&application_name=svc&connect_timeout=5
-//
-// Key=value form:
-//
-// host=localhost port=5432 user=app password=secret dbname=mydb sslmode=disable
-//
-// Recognized DSN keys: host, port, user, password, dbname / database, sslmode,
-// connect_timeout (seconds), statement_cache_size, auto_cache_statements,
-// application_name. Any other key is forwarded to the server as a
-// StartupMessage parameter (so search_path, timezone, statement_timeout, and
-// similar GUCs can be set at connect time).
-//
-// auto_cache_statements (default true at the [Pool.Open] / [NewConnector]
-// layer): when true, cacheable SELECT-style QueryContext calls with
-// arguments transparently auto-prepare on first use and reuse the
-// prepared statement (Bind+Execute+Sync) on subsequent invocations.
-// Mirrors pgx's QueryExecModeCacheStatement default. Set
-// `auto_cache_statements=false` in the DSN to opt out and stay on the
-// extended-protocol-without-cache path. Arg-less queries always take the
-// simple-query path regardless.
-//
-// # Options
-//
-// Pool knobs are supplied as functional options to [Open]:
-//
-// - [WithEngine] bind the pool to a celeris.Server event loop.
-// - [WithMaxOpen] total conn cap (default NumWorkers*4).
-// - [WithMaxIdlePerWorker] per-worker idle list cap (default 2).
-// - [WithMaxLifetime] max conn age (default 30m).
-// - [WithMaxIdleTime] max idle duration (default 5m).
-// - [WithHealthCheck] background sweep interval (default 30s; 0 disables).
-// - [WithStatementCacheSize] per-conn prepared-statement LRU (default 256).
-// - [WithApplication] application_name startup parameter.
-//
-// # Transactions
-//
-// [Pool.BeginTx] opens a [Tx] that pins its connection until [Tx.Commit] or
-// [Tx.Rollback] is called. Passing *sql.TxOptions selects the isolation level
-// (Read Uncommitted / Read Committed / Repeatable Read / Serializable) and
-// the read-only flag, all folded into a single BEGIN round trip.
-//
-// Savepoints are reachable three ways:
-//
-// 1. (*postgres.Tx).Savepoint / ReleaseSavepoint / RollbackToSavepoint on
-// the direct Pool transaction.
-//
-// 2. database/sql via sql.Conn.Raw + the exported [Conn] alias:
-//
-// conn, _ := db.Conn(ctx)
-// defer conn.Close()
-// _ = conn.Raw(func(raw any) error {
-// pc := raw.(*postgres.Conn)
-// return pc.Savepoint(ctx, "sp1")
-// })
-//
-// 3. Raw simple queries ("SAVEPOINT sp1") issued through Exec on a
-// transaction; the first two forms are preferred because they
-// validate the name.
-//
-// Savepoint names must match [A-Za-z0-9_]+; other identifiers are
-// rejected before the wire write to avoid SQL-injection.
-//
-// # Bulk COPY
-//
-// [Pool.CopyFrom] streams rows into a table via COPY FROM STDIN. Callers
-// implement [CopyFromSource] (or use [CopyFromSlice] for in-memory fixtures)
-// to feed rows. [Pool.CopyTo] runs COPY ... TO STDOUT and invokes a callback
-// for each raw row. Both use PG's text format with tab separators and
-// backslash escaping.
-//
-// # Pool configurations
-//
-// database/sql and postgres.Pool each maintain their own set of connections.
-// Opening a sql.DB with sql.Open("celeris-postgres", dsn) and separately
-// calling postgres.Open(dsn) does not share pool state — configure
-// sql.DB.SetMaxOpenConns on the former and [WithMaxOpen] on the latter
-// independently.
-//
-// # Type support
-//
-// The encoder/decoder understands the following server OIDs out of the box
-// (see postgres/protocol for the full codec table):
-//
-// bool, int2, int4, int8, float4, float8, text, varchar, bytea, uuid,
-// date, timestamp, timestamptz, numeric, json, jsonb, and the one-
-// dimensional array variants of those types (_bool, _int4, _text, ...).
-//
-// Go type mappings:
-//
-// bool ↔ bool
-// int2/int4/int8 ↔ int64 (accepts int, int32 at encode)
-// float4/float8 ↔ float64
-// text/varchar ↔ string
-// bytea ↔ []byte
-// uuid ↔ []byte (16 bytes)
-// date/timestamp/tstz ↔ time.Time
-// numeric ↔ string (use strconv or math/big to convert)
-// json/jsonb ↔ []byte or string
-// arrays ↔ []T of the element's Go type
-//
-// Any argument that implements [database/sql/driver.Valuer] is resolved to
-// its underlying value before encoding. Any destination that implements
-// [database/sql.Scanner] receives the raw bytes. Custom types can be plugged
-// in via [protocol.RegisterType].
-//
-// # Errors
-//
-// Server-side errors surface as [*PGError] (a re-export of
-// [protocol.PGError]). The struct carries the five-character SQLSTATE code,
-// the short message, and any optional fields the server attached (detail,
-// hint, constraint name, etc.). Sentinels are wrapped so errors.Is and
-// errors.As work as expected:
-//
-// var pgErr *postgres.PGError
-// if errors.As(err, &pgErr) && pgErr.Code == "23505" { ... }
-//
-// Other package-level sentinels: [ErrPoolClosed], [ErrClosed], [ErrBadConn],
-// [ErrSSLNotSupported], [ErrUnsupportedAuth].
-//
-// # Query cancellation
-//
-// Canceling a context aborts the in-flight query by dialing a short-lived
-// side connection and sending the PostgreSQL v3 CancelRequest packet
-// (protocol code 80877102). The client blocks until the server drains the
-// rest of the original query's response — cancellation is cooperative, not
-// instantaneous.
-//
-// # WithEngine and standalone operation
-//
-// [WithEngine] is optional. When omitted, [Open] resolves a standalone event
-// loop backed by the platform's best mechanism (epoll on Linux, goroutine-
-// per-conn on Darwin). The standalone loop is reference-counted inside
-// driver/internal/eventloop and shared across all drivers that omit WithEngine.
-// Correctness is identical with or without WithEngine; the difference is
-// performance: sharing the HTTP server's event loop improves data locality
-// and saves one epoll/uring syscall per I/O. Expect ~5-20% lower latency for
-// serial queries when WithEngine is used.
-//
-// database/sql mode (sql.Open) always uses the standalone loop. For optimal
-// performance, prefer the direct Pool with WithEngine(srv).
-//
-// # Rows iteration
-//
-// [Pool.QueryContext] returns a *Rows value. Iterate with Next + Scan:
-//
-// rows, err := pool.QueryContext(ctx, "SELECT id, name FROM users")
-// if err != nil { return err }
-// defer rows.Close()
-// for rows.Next() {
-// var id int64
-// var name string
-// if err := rows.Scan(&id, &name); err != nil { return err }
-// // process id, name
-// }
-// if err := rows.Err(); err != nil {
-// return err // check Err() after the loop
-// }
-//
-// Always check [Rows.Err] after the loop — it surfaces errors from the
-// underlying query that were deferred until iteration completed (e.g.
-// cancellation, network errors, or server-side errors on large result sets).
-//
-// # Known limitations
//
-// - TLS is not yet supported. sslmode=require, verify-ca, and verify-full
-// are rejected at [Open] time with [ErrSSLNotSupported]. Deploy over
-// VPC, loopback, or a sidecar TLS terminator.
-// - Result sets are fully buffered before Rows.Next returns — there is no
-// true row-by-row streaming. Callers with large result sets should page
-// via LIMIT/OFFSET or a server-side DECLARE CURSOR inside a transaction.
-// - Authentication supports trust, cleartext, MD5, and SCRAM-SHA-256
-// (without channel binding). GSS, SSPI, Kerberos, and SCRAM-SHA-256-PLUS
-// are not implemented.
-// - No LISTEN / NOTIFY. Asynchronous NotificationResponse frames received
-// from the server are silently dropped by the dispatcher.
-// - COPY IN / COPY OUT is exposed via [Pool.CopyFrom] / [Pool.CopyTo] on
-// the direct Pool API. database/sql has no analogous surface; callers
-// who need COPY must use the direct Pool.
-// - No PG 14+ pipeline mode (Execute chaining).
-// - No cluster- or replica-routing logic; callers with a read replica
-// should open a second Pool pointed at it.
+// [Open] accepts both the libpq URL form and the key=value DSN form. Pool
+// behavior is tuned with functional [Option] values: [WithEngine],
+// [WithMaxOpen], [WithMaxIdlePerWorker], [WithMaxLifetime], [WithMaxIdleTime],
+// [WithHealthCheck], [WithStatementCacheSize], and [WithApplication].
+// [WithEngine] is optional and affects performance only: when set, the pool
+// shares the HTTP server's event loop instead of resolving a standalone one.
+// [WithWorker] adds a per-call worker-affinity hint to a context.
+//
+// Query with [Pool.QueryContext] (returns [*Rows]), [Pool.QueryRow]
+// (returns [*Row]), or [Pool.ExecContext] (returns [Result]). Transactions
+// come from [Pool.BeginTx], which returns a [*Tx] with [Tx.Commit],
+// [Tx.Rollback], and savepoint helpers ([Tx.Savepoint],
+// [Tx.ReleaseSavepoint], [Tx.RollbackToSavepoint]). Bulk loads use
+// [Pool.CopyFrom] (feed rows via [CopyFromSource] or [CopyFromSlice]) and
+// [Pool.CopyTo].
+//
+// Server errors surface as [*PGError] (alias of [protocol.PGError]) carrying
+// the SQLSTATE code; match with errors.As. Package sentinels include
+// [ErrPoolClosed], [ErrClosed], [ErrBadConn], [ErrSSLNotSupported],
+// [ErrUnsupportedAuth], and [ErrResultTooBig]. Custom type codecs can be
+// registered with [protocol.RegisterType].
+//
+// Current limitations: TLS is not yet supported (use sslmode=disable behind a
+// VPC/loopback/sidecar); authentication is limited to trust, cleartext, MD5,
+// and SCRAM-SHA-256 without channel binding; there is no LISTEN/NOTIFY,
+// pipeline mode, or replica routing.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/data-stores
package postgres
diff --git a/driver/redis/doc.go b/driver/redis/doc.go
index 830470b1..c98ff181 100644
--- a/driver/redis/doc.go
+++ b/driver/redis/doc.go
@@ -1,264 +1,42 @@
-// Package redis is celeris's native Redis driver. It speaks RESP2 and RESP3
-// directly on top of the celeris event loop using the same async-bridge
-// architecture as the celeris PostgreSQL driver: one in-flight request
-// enqueued per conn, responses demuxed from the loop's recv callback, and
-// per-worker idle pools so handlers stay on the same CPU that dispatched
-// them.
-//
-// # Usage
-//
-// client, err := redis.NewClient("localhost:6379",
-// redis.WithEngine(srv), // optional; omit for a standalone loop
-// redis.WithPassword("secret"),
-// redis.WithDB(0),
-// )
+// Package redis is celeris's native RESP2/RESP3 Redis driver, designed to run
+// its socket I/O on the celeris event loop alongside your HTTP handlers.
+//
+// [NewClient] returns a [Client] backed by a per-worker connection pool;
+// connections are dialed lazily on first command and negotiate RESP3 (HELLO 3)
+// with automatic RESP2 fallback. Pass [WithEngine] to colocate the driver's
+// file descriptors on the same workers as a running celeris server (lower
+// latency via better data locality); omit it to use a standalone, internally
+// reference-counted event loop. Other options include [WithPassword],
+// [WithUsername], [WithDB], [WithForceRESP2], [WithProto], and [WithOnPush];
+// see [Config] for the full set.
+//
+// client, err := redis.NewClient("localhost:6379", redis.WithEngine(srv))
// defer client.Close()
-//
// v, err := client.Get(ctx, "key")
//
-// [WithEngine] routes the driver's FDs through the same epoll/io_uring
-// instance as the HTTP workers. When it is omitted, a package-level
-// standalone loop is resolved and reference-counted
-// (driver/internal/eventloop).
-//
-// Connections are lazily dialed on first command. The first dial does the
-// RESP3 handshake (HELLO 3 + AUTH + SETNAME), falls back to RESP2 + AUTH +
-// SELECT if HELLO is rejected, or speaks RESP2 unconditionally when
-// [WithForceRESP2] is set (required for ElastiCache classic clusters and
-// for older servers that do not implement HELLO).
-//
-// # Commands
-//
-// The typed API on [Client] covers:
-//
-// - Strings: Get, GetBytes, Set, SetNX, Del, Exists, Incr, Decr, MGet,
-// Expire, PExpire, ExpireAt, PExpireAt, Persist, TTL.
-// - Hashes: HGet, HSet, HDel, HGetAll, HExists, HKeys, HVals.
-// - Lists: LPush, RPush, LPop, RPop, LRange, LLen.
-// - Sets: SAdd, SRem, SMembers, SIsMember, SCard.
-// - Sorted sets: ZAdd, ZRange, ZRangeByScore, ZRem, ZScore, ZCard.
-// - Keys: Type, Rename, RandomKey, Scan (cursor iteration).
-// - Pub/Sub: Publish, Subscribe, PSubscribe (see below).
-// - Scripting: Eval, EvalSHA, ScriptLoad.
-// - Watch for optimistic locking (pins a conn for the callback).
-//
-// Any command the typed surface does not expose can be sent via [Client.Do],
-// which accepts ...any args, converts them through [argify], and returns a
-// [*protocol.Value] plus a server-side error as [*RedisError].
-//
-// # Pipeline
-//
-// [Client.Pipeline] batches commands into a single network write. Each
-// queued call returns a typed handle (*StringCmd, *IntCmd, ...); the
-// caller then invokes [Pipeline.Exec] to flush and harvest the replies:
-//
-// p := client.Pipeline()
-// a := p.Set("k", "v", 0)
-// b := p.Incr("counter")
-// if err := p.Exec(ctx); err != nil { ... }
-// _, _ = a.Result()
-// n, _ := b.Result()
-//
-// All commands in one Exec ride the same connection, so replies are
-// returned in the same order as commands were enqueued. Replies are
-// detached from the reader's scratch buffer before the conn is released,
-// so each Result() call is safe to retain.
-//
-// # Transactions
-//
-// Typed MULTI/EXEC transactions are built via [Client.TxPipeline]. Queued
-// commands return deferred *Cmd handles that populate after [Tx.Exec]:
-//
-// tx, _ := client.TxPipeline(ctx)
-// defer tx.Discard() // no-op after a successful Exec
-// a := tx.Incr("visits")
-// b := tx.Incr("uniques")
-// if err := tx.Exec(ctx); err != nil { ... }
-// va, _ := a.Result()
-// vb, _ := b.Result()
-//
-// Exec sends MULTI + the buffered commands + EXEC in a single write. If a
-// WATCHed key changed (EXEC returns a null array) every queued *Cmd.Result()
-// returns [ErrTxAborted]. WATCH / UNWATCH are available on the Tx itself
-// and must be called before any command is queued.
-//
-// Raw MULTI/EXEC via [Client.Do] also works; the pool tracks the
-// MULTI/EXEC/DISCARD state on the pinned conn so a premature release issues
-// a DISCARD before the conn returns to the idle list.
-//
-// # Pub/Sub
-//
-// [Client.Subscribe] and [Client.PSubscribe] open a dedicated subscriber
-// connection. Messages arrive on [PubSub.Channel]:
-//
-// ps, _ := client.Subscribe(ctx, "events")
-// defer ps.Close()
-// for m := range ps.Channel() { handle(m) }
-//
-// On transport error the driver automatically reconnects with exponential
-// backoff and replays the tracked SUBSCRIBE/PSUBSCRIBE list. Messages that
-// arrive while the connection is down are lost — delivery is at-most-once.
-// The channel buffer defaults to 256; when it fills, [PubSub.deliver] drops
-// the oldest message to make room and bumps [PubSub.Drops].
-//
-// # RESP2 vs RESP3
-//
-// RESP3 is negotiated with HELLO 3 during the handshake and brings richer
-// reply types (Map, Set, Double, Bool, Verbatim, BigNumber, Push) that the
-// driver surfaces via the [protocol.Value] type-tag enum. When the server
-// rejects HELLO the driver transparently downgrades to RESP2 and speaks
-// AUTH + SELECT, so almost all callers can leave the default alone.
-// [WithForceRESP2] pins the connection to RESP2 for deployments that must
-// avoid HELLO entirely. [Client.Proto] reports the negotiated version.
-//
-// # Zero-copy semantics
-//
-// The RESP reader returns [protocol.Value] structs whose Str / Array / Map
-// fields alias the reader's internal buffer. To keep hot-path reads
-// allocation-free, the driver does NOT copy on every reply. Instead:
-//
-// - Typed accessors on [Client] (Get, HGet, MGet, ...) return freshly
-// allocated Go strings / slices / maps — the copy happens inside the
-// decode helper, so the caller never sees aliased bytes.
-// - Pipeline results are detached (deep-copied) before the conn returns
-// to the idle pool, so each *Cmd.Result() is independent.
-// - [Client.Do] returns a [*protocol.Value] that has already been detached
-// from the reader buffer; callers can retain it freely.
-//
-// The only place raw aliasing is observable is inside custom dispatch
-// routines that reach into the internal protocol APIs directly — typed
-// callers are always safe.
-//
-// # Errors
-//
-// Server-side error replies surface as [*RedisError] with Prefix (e.g.
-// "WRONGTYPE", "ERR", "MOVED") and full Msg. Sentinels are wired through
-// [RedisError.Is]:
-//
-// - [ErrNil] null bulk reply (GET on missing key, LPOP on empty list).
-// - [ErrClosed] Client or PubSub after Close.
-// - [ErrProtocol] reply did not parse.
-// - [ErrMoved] cluster redirect (not followed — cluster is unsupported).
-// - [ErrWrongType] operation on a key of the wrong kind.
-// - [ErrPoolExhausted] all idle conns are stale (rare; pool blocks when full).
-// - [ErrTxAborted] WATCH / EXEC aborted the transaction.
-//
-// # WithEngine and standalone operation
-//
-// [WithEngine] is optional. When omitted, [NewClient] resolves a standalone
-// event loop backed by the platform's best mechanism (epoll on Linux, goroutine-
-// per-conn on Darwin). The standalone loop is reference-counted inside
-// driver/internal/eventloop and shared across all drivers that omit WithEngine.
-// Correctness is identical with or without WithEngine; the difference is
-// performance: sharing the HTTP server's event loop saves one epoll/uring
-// syscall per I/O because driver FDs land on the same worker goroutine as HTTP
-// handlers, improving data locality. Expect ~5-20% lower latency for serial
-// queries when WithEngine is used.
-//
-// # Pipeline.Release lifetime
-//
-// [Pipeline.Release] returns the Pipeline — and all of its internal slabs —
-// to a sync.Pool for reuse. After Release, every typed Cmd handle (*StringCmd,
-// *IntCmd, *StatusCmd, *FloatCmd, *BoolCmd) previously returned from the
-// Pipeline is invalid. Calling Result() on an invalidated handle returns
-// [ErrClosed]. Release is optional; un-Released Pipelines are GC'd normally.
-// For tight hot paths, pooling via Release eliminates per-request slab allocs.
-//
-// # SCAN usage
-//
-// Cursor-based iteration is exposed via [Client.Scan]:
-//
-// it := client.Scan(ctx, "user:*", 100)
-// for {
-// key, ok := it.Next(ctx)
-// if !ok {
-// break
-// }
-// // process key
-// }
-// if err := it.Err(); err != nil {
-// log.Fatal(err)
-// }
-//
-// The ScanIterator handles cursor paging internally. The match pattern is
-// passed as MATCH and the count hint as COUNT. Both are optional — pass ""
-// and 0 to iterate all keys. The iterator is NOT safe for concurrent use.
-//
-// # Watch (optimistic locking)
-//
-// [Client.Watch] pins a connection, WATCHes the given keys, and invokes fn
-// with a [Tx] that queues commands under MULTI/EXEC. If a WATCHed key is
-// modified before EXEC, the transaction aborts with [ErrTxAborted] and the
-// caller can retry:
-//
-// err := client.Watch(ctx, func(tx *redis.Tx) error {
-// val, err := client.Get(ctx, "counter")
-// if err != nil { return err }
-// n, _ := strconv.Atoi(val)
-// tx.Set("counter", strconv.Itoa(n+1), 0)
-// return tx.Exec(ctx)
-// }, "counter")
-//
-// # Push callbacks (client tracking)
-//
-// RESP3 push frames on command connections (e.g. CLIENT TRACKING invalidation
-// messages) can be intercepted via [WithOnPush] or [Client.OnPush]:
-//
-// client, _ := redis.NewClient("localhost:6379",
-// redis.WithOnPush(func(channel string, data []protocol.Value) {
-// log.Printf("push: %s %v", channel, data)
-// }),
-// )
-//
-// When no callback is registered, push frames on command connections are
-// silently dropped. Push frames on pub/sub connections are always routed
-// through the [PubSub.Channel] mechanism.
-//
-// # Cluster Transactions
-//
-// Redis Cluster requires all keys in a MULTI/EXEC transaction to reside in
-// the same hash slot. Cross-slot transactions are impossible by design —
-// the server returns -CROSSSLOT. The celeris driver validates slot affinity
-// client-side and returns [ErrCrossSlot] before contacting the server when
-// keys span multiple slots.
-//
-// Use hash tags to colocate related keys on the same slot:
-//
-// user:{123}:name → hashes only "{123}" → slot X
-// user:{123}:email → hashes only "{123}" → slot X
-//
-// [ClusterClient.TxPipeline] returns a [ClusterTx] that queues commands and
-// verifies all keys target the same slot:
-//
-// tx := cluster.TxPipeline()
-// a := tx.Set("{user:123}:name", "Alice", 0)
-// b := tx.Set("{user:123}:email", "alice@example.com", 0)
-// if err := tx.Exec(ctx); err != nil { ... }
-//
-// [ClusterClient.Watch] validates slot affinity for the watched keys and
-// delegates to the target node's [Client.Watch]:
-//
-// err := cluster.Watch(ctx, func(tx *redis.Tx) error {
-// tx.Incr("{user:123}:visits")
-// return tx.Exec(ctx)
-// }, "{user:123}:visits")
-//
-// # Known limitations
-//
-// - TLS (rediss://) is not yet supported; the scheme is rejected in
-// [NewClient] with a clear error. Deploy over VPC, loopback, or a
-// sidecar TLS terminator.
-// - Cluster ([ClusterClient]) and Sentinel ([SentinelClient]) are supported.
-// MOVED/ASK redirects are handled transparently by ClusterClient; Sentinel
-// auto-discovers the master and handles failovers via +switch-master.
-// Cluster pipeline splits commands by slot and executes per-node
-// sub-pipelines in parallel. SSUBSCRIBE (shard channels, Redis 7+) is
-// not supported; use regular SUBSCRIBE which is cluster-wide.
-// - No wrappers for RedisJSON, RediSearch, RedisGraph, or RedisTimeSeries —
-// use [Client.Do] with the raw command strings.
-// - Pub/Sub auto-reconnects but messages delivered while the conn is down
-// are lost. Callers needing at-least-once should add server-side
-// durability (Streams + consumer groups).
-// - database/sql is not implemented — Redis is not a SQL database.
-// - Read/write timeouts are advisory today; cancellation is via ctx.
+// Key exported symbols and when to reach for them:
+//
+// - [Client] — typed commands for strings, hashes, lists, sets, sorted sets,
+// keys, scripting (Eval, EvalSHA, ScriptLoad) and pub/sub. [Client.Do]
+// (plus DoString/DoInt/DoBool/DoSlice) is the escape hatch for any command
+// the typed surface omits.
+// - [Client.Pipeline] returns a [Pipeline] that batches commands into one
+// write; queued calls return typed handles (*StringCmd, *IntCmd, etc.)
+// resolved after [Pipeline.Exec].
+// - [Client.TxPipeline] and [Client.Watch] provide MULTI/EXEC transactions
+// and optimistic locking via [Tx]; aborts surface as [ErrTxAborted].
+// - [Client.Subscribe] / [Client.PSubscribe] return a [PubSub] whose
+// [PubSub.Channel] delivers [Message]s, with automatic reconnect.
+// - [Client.Scan] returns a [ScanIterator] for cursor-based key iteration.
+// - [NewClusterClient] ([ClusterClient]) and [NewSentinelClient]
+// ([SentinelClient]) cover Redis Cluster and Sentinel deployments.
+//
+// Typed accessors and pipeline/Do results are detached from the reader buffer,
+// so returned values are safe to retain. Server error replies surface as
+// [*RedisError] (with sentinels such as [ErrNil], [ErrWrongType],
+// [ErrCrossSlot]); see [errors.Is]. TLS (rediss://) is not yet supported.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/data-stores
package redis
diff --git a/engine/doc.go b/engine/doc.go
index df2b60ae..f1349c85 100644
--- a/engine/doc.go
+++ b/engine/doc.go
@@ -4,4 +4,8 @@
// This package is the dependency root for engine-related types. User code
// typically interacts with engines through the top-level celeris package
// rather than importing this package directly.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/engines
package engine
diff --git a/middleware/adapters/doc.go b/middleware/adapters/doc.go
index 96664814..84909eb1 100644
--- a/middleware/adapters/doc.go
+++ b/middleware/adapters/doc.go
@@ -1,68 +1,18 @@
-// Package adapters bridges stdlib net/http middleware into celeris handler chains.
+// Package adapters bridges stdlib net/http middleware and handlers into celeris handler chains.
//
// Use [WrapMiddleware] to adapt any func(http.Handler) http.Handler into a
-// [celeris.HandlerFunc]. This enables reuse of existing stdlib middleware
-// such as rs/cors, gorilla/csrf, and similar libraries.
+// [celeris.HandlerFunc], enabling reuse of existing stdlib middleware such as
+// rs/cors, gorilla/csrf, and similar libraries.
//
-// corsHandler := cors.Handler(cors.Options{AllowedOrigins: []string{"*"}})
-// server.Use(adapters.WrapMiddleware(corsHandler))
+// Use [ReverseProxy] to forward requests to a backend URL, with optional hooks
+// via [WithTransport], [WithModifyRequest], [WithModifyResponse], and
+// [WithErrorHandler].
//
-// Warning: Do not use both adapters.WrapMiddleware with a stdlib CORS
-// library (e.g., rs/cors) AND the native celeris/middleware/cors in the
-// same chain. This produces duplicate Access-Control-* headers and
-// conflicting preflight handling. Use one or the other.
+// Note: do not combine [WrapMiddleware] with a stdlib CORS library and the
+// native celeris/middleware/cors in the same chain — this produces duplicate
+// Access-Control-* headers and conflicting preflight handling.
//
-// For converting celeris handlers to stdlib, use [celeris.ToHandler] directly.
+// # Documentation
//
-// # How It Works
-//
-// WrapMiddleware creates a temporary http.ResponseWriter (responseCapture)
-// and reconstructs an *http.Request from the celeris Context. The stdlib
-// middleware runs against these. If the middleware calls the inner handler,
-// control returns to the celeris chain via c.Next(). Any response headers
-// the stdlib middleware set before calling next are copied to the celeris
-// response.
-//
-// If the stdlib middleware short-circuits (does not call the inner handler),
-// the captured status code, headers, and body are written through celeris.
-//
-// # Performance
-//
-// WrapMiddleware reconstructs an *http.Request per call, which costs
-// 8-15 heap allocations depending on header count and body presence.
-// For hot-path middleware (e.g., CORS on every request), prefer the native
-// celeris/middleware/cors over adapters.WrapMiddleware(rs/cors) for
-// zero-alloc performance. Use WrapMiddleware for middleware that runs
-// infrequently or where the stdlib library has no native celeris equivalent.
-//
-// # Limitations
-//
-// The responseCapture used by WrapMiddleware does not implement
-// http.Hijacker or http.Flusher. Stdlib middleware that requires
-// WebSocket upgrade (via Hijack) or streaming flush (via Flush)
-// will not work through WrapMiddleware. For these use cases,
-// implement the middleware natively in celeris.
-//
-// # Reverse Proxy
-//
-// [ReverseProxy] creates a handler that forwards requests to a target URL
-// using [net/http/httputil.ReverseProxy] under the hood:
-//
-// target, _ := url.Parse("http://backend:8080")
-// server.Any("/api/*path", adapters.ReverseProxy(target))
-//
-// Options:
-//
-// - [WithTransport]: set a custom [http.RoundTripper]
-// - [WithModifyRequest]: mutate outbound requests (e.g. add headers)
-// - [WithModifyResponse]: inspect or modify backend responses before forwarding
-// - [WithErrorHandler]: custom error handling for proxy failures
-//
-// The proxy automatically sets X-Forwarded-For, X-Forwarded-Host, and
-// X-Forwarded-Proto via [net/http/httputil.ProxyRequest.SetXForwarded].
-// Panics if target is nil.
-//
-// ReverseProxy delegates to [celeris.Adapt], which buffers the response.
-// Streaming responses (SSE, WebSocket upgrade) are not supported through
-// the proxy.
+// Full guides and examples: https://goceleris.dev/docs/net-http-interop
package adapters
diff --git a/middleware/basicauth/doc.go b/middleware/basicauth/doc.go
index 124bf78d..06f83c68 100644
--- a/middleware/basicauth/doc.go
+++ b/middleware/basicauth/doc.go
@@ -1,57 +1,35 @@
// Package basicauth provides HTTP Basic Authentication middleware for
// celeris.
//
-// The middleware parses the Authorization header via the framework's
-// [celeris.Context.BasicAuth] method, validates credentials via a
-// user-supplied function, and stores the authenticated username in the
-// context store under [UsernameKey]. Failed authentication returns 401
-// with a WWW-Authenticate header.
-//
-// One of [Config].Validator, [Config].ValidatorWithContext, [Config].Users,
-// or [Config].HashedUsers is required; omitting all four panics at
-// initialization.
-//
-// Simple usage with a Users map (auto-generates a constant-time validator):
+// The middleware parses the Authorization header via [celeris.Context.BasicAuth],
+// validates credentials via a user-supplied function, and stores the
+// authenticated username in the context store under [UsernameKey]. Failed
+// authentication returns 401 with WWW-Authenticate, Cache-Control, and Vary
+// headers.
+//
+// Exactly one credential source is required; [New] panics otherwise:
+// - [Config].Users — plaintext map, auto-generates a constant-time HMAC validator.
+// - [Config].HashedUsers + [Config].HashedUsersFunc — opaque hash strings with
+// a caller-supplied compare function (bcrypt, argon2id, scrypt, etc.).
+// HashedUsersFunc is mandatory when HashedUsers is set.
+// - [Config].Validator — arbitrary func(user, pass string) bool.
+// - [Config].ValidatorWithContext — same, with request context access.
+//
+// Minimal usage with a Users map:
//
// server.Use(basicauth.New(basicauth.Config{
// Users: map[string]string{
// "admin": "secret",
-// "user": "pass",
-// },
-// }))
-//
-// Hashed passwords with SHA-256 (avoids storing plaintext):
-//
-// server.Use(basicauth.New(basicauth.Config{
-// HashedUsers: map[string]string{
-// "admin": basicauth.HashPassword("secret"),
// },
// }))
//
-// WARNING: SHA-256 is a fast hash with no work factor. For high-security
-// password storage, use bcrypt, scrypt, or Argon2 via a custom
-// [Config].Validator or [Config].HashedUsersFunc.
-//
-// Plugging in bcrypt via HashedUsersFunc:
-//
-// server.Use(basicauth.New(basicauth.Config{
-// HashedUsers: map[string]string{
-// "admin": "$2a$10$...", // bcrypt hash
-// },
-// HashedUsersFunc: func(hash, password string) bool {
-// return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
-// },
-// }))
-//
-// # Retrieving the Username
-//
-// Use [UsernameFromContext] to retrieve the authenticated username from
-// downstream handlers:
+// Use [UsernameFromContext] to retrieve the authenticated username downstream.
+// Set [Config].Skip or [Config].SkipPaths to bypass the middleware selectively.
//
-// name := basicauth.UsernameFromContext(c)
+// Note: [HashPassword] (SHA-256) is deprecated and credential-grade only with a
+// modern KDF. Use bcrypt or argon2 via [Config].HashedUsersFunc instead.
//
-// # Skipping
+// # Documentation
//
-// Set [Config].Skip to bypass the middleware dynamically, or
-// [Config].SkipPaths for exact-match path exclusions.
+// Full guides and examples: https://goceleris.dev/docs/middleware-auth
package basicauth
diff --git a/middleware/bodylimit/doc.go b/middleware/bodylimit/doc.go
index e198c8d5..b191be91 100644
--- a/middleware/bodylimit/doc.go
+++ b/middleware/bodylimit/doc.go
@@ -1,50 +1,27 @@
// Package bodylimit provides request body size limiting middleware for
// celeris.
//
-// The middleware enforces a maximum request body size using a two-phase
-// approach: first checking Content-Length (fast path), then verifying
-// actual body bytes (catches lying or absent Content-Length). Requests
-// exceeding the limit are rejected with 413 Request Entity Too Large.
-//
-// Basic usage with the default 4 MB limit:
-//
-// server.Use(bodylimit.New())
-//
-// Human-readable size string:
-//
-// server.Use(bodylimit.New(bodylimit.Config{
-// Limit: "10MB",
-// }))
-//
-// [Config].Limit accepts SI and IEC units: B, KB, MB, GB, TB, PB, EB,
-// KiB, MiB, GiB, TiB, PiB, EiB. Fractional values are supported.
-// When set, Limit takes precedence over MaxBytes.
-//
-// LAYER OF DEFENSE — read carefully:
-//
-// The DoS-grade cap lives at the engine read layer:
-// [celeris.Config.MaxRequestBodySize] (default 100 MB). Bodies larger
-// than that are rejected by the engine BEFORE any buffering happens, so
-// the framework never holds them in memory. Set
-// [celeris.Config.MaxRequestBodySize] to your real maximum at server
-// construction time.
-//
-// This middleware runs AFTER the engine has already buffered the body.
-// Use it for per-route caps that are smaller than the server-wide
-// MaxRequestBodySize (e.g. a /comments endpoint that should accept at
-// most 64 KB while /uploads accepts up to MaxRequestBodySize). It is
-// NOT a substitute for setting MaxRequestBodySize itself; a malicious
-// client can still send up to MaxRequestBodySize before this middleware
-// rejects them.
-//
-// Enable [Config].ContentLengthRequired to reject requests without
-// Content-Length (411 Length Required), forcing clients to declare the
-// body size up-front.
-//
-// Bodyless methods (GET, HEAD, DELETE, OPTIONS, TRACE, CONNECT) are
-// auto-skipped. Use [Config].Skip or [Config].SkipPaths for additional
-// exclusions. Set [Config].ErrorHandler to customize error responses.
-//
-// [ErrBodyTooLarge] and [ErrLengthRequired] are exported sentinel errors
-// usable with errors.Is for upstream error handling.
+// [New] returns a [celeris.HandlerFunc] that enforces a maximum request body
+// size using a two-phase check: Content-Length header (fast path) then actual
+// body bytes (catches lying or absent Content-Length). Requests exceeding the
+// limit are rejected with 413 Request Entity Too Large.
+//
+// Configure via [Config]: set [Config].Limit to a human-readable size string
+// (e.g. "10MB", "1.5GiB"; SI and IEC units accepted; takes precedence over
+// [Config].MaxBytes), or set [Config].MaxBytes directly (default 4 MiB).
+// Enable [Config].ContentLengthRequired to reject requests that omit
+// Content-Length with 411 Length Required. Bodyless methods (GET, HEAD,
+// DELETE, OPTIONS, TRACE, CONNECT) are auto-skipped; use [Config].Skip or
+// [Config].SkipPaths for additional exclusions. Customize error responses with
+// [Config].ErrorHandler. Sentinel errors [ErrBodyTooLarge] and
+// [ErrLengthRequired] are usable with errors.Is.
+//
+// Note: this middleware runs after the engine has buffered the body. It adds
+// an application-level per-route cap below the server-wide engine limit
+// (celeris.Config.MaxRequestBodySize, default 100 MiB); it is not a
+// substitute for setting that limit.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-traffic
package bodylimit
diff --git a/middleware/cache/doc.go b/middleware/cache/doc.go
index 7a29eda7..cf4a09fb 100644
--- a/middleware/cache/doc.go
+++ b/middleware/cache/doc.go
@@ -36,4 +36,8 @@
// - [Invalidate] removes a single computed key
// - [InvalidatePrefix] removes every key with the given prefix
// (requires store.PrefixDeleter)
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-content
package cache
diff --git a/middleware/circuitbreaker/doc.go b/middleware/circuitbreaker/doc.go
index 504f0a6a..29de236e 100644
--- a/middleware/circuitbreaker/doc.go
+++ b/middleware/circuitbreaker/doc.go
@@ -1,111 +1,26 @@
// Package circuitbreaker provides circuit breaker middleware for celeris.
//
-// The circuit breaker monitors downstream error rates using a sliding window
-// and automatically stops sending requests when failure thresholds are exceeded,
-// giving the failing service time to recover.
+// The breaker monitors downstream error rates using a sliding window and
+// automatically stops sending requests when failure thresholds are exceeded,
+// giving the failing service time to recover. It operates as a three-state
+// machine: Closed (normal), Open (rejecting all requests with 503), and
+// HalfOpen (allowing limited probe requests to test recovery).
//
-// # Three-State Machine
+// Use [New] to create middleware with default settings (50% threshold,
+// minimum 10 requests, 10 s window, 30 s cooldown). Pass a [Config] to
+// tune [Config.Threshold], [Config.MinRequests], [Config.WindowSize],
+// [Config.CooldownPeriod], [Config.HalfOpenMax], [Config.IsError], and
+// [Config.OnStateChange].
//
-// The breaker operates in three states:
+// Use [NewWithBreaker] to obtain a [*Breaker] reference for programmatic
+// state inspection ([Breaker.State]), sliding-window counter export
+// ([Breaker.Counts]), and forced reset ([Breaker.Reset]) from health-check
+// or admin handlers.
//
-// - Closed: All requests pass through. The sliding window tracks successes
-// and failures. When the failure ratio meets or exceeds [Config].Threshold
-// (and at least [Config].MinRequests have been observed), the breaker
-// trips to Open.
+// [ErrServiceUnavailable] is the sentinel error (503) returned when the
+// breaker is open; use errors.Is to match it in upstream middleware.
//
-// - Open: All requests are immediately rejected with 503 Service Unavailable.
-// A Retry-After header is set with the remaining cooldown seconds. After
-// [Config].CooldownPeriod elapses, the breaker transitions to HalfOpen.
+// # Documentation
//
-// - HalfOpen: Up to [Config].HalfOpenMax probe requests are allowed through.
-// If a probe succeeds, the breaker returns to Closed. If a probe fails,
-// the breaker returns to Open. Excess requests beyond HalfOpenMax are
-// rejected with 503.
-//
-// # Sliding Window Algorithm
-//
-// The observation window is divided into 10 time buckets spanning
-// [Config].WindowSize. Each bucket tracks successes and failures using atomic
-// counters. Expired buckets (older than WindowSize) are excluded from the
-// error rate calculation. This provides a smooth, time-decaying view of
-// the failure rate without requiring a global lock on the hot path.
-//
-// # Response Classification
-//
-// By default, responses with status >= 500 are counted as failures. Use
-// [Config].IsError to customize classification (e.g., treat 429 as a failure,
-// or ignore certain 5xx codes).
-//
-// # Retry-After Header
-//
-// When the breaker is open, the middleware sets a Retry-After header with
-// the number of seconds until the cooldown period expires. Clients that
-// respect this header avoid unnecessary retries during the cooldown.
-//
-// # Programmatic Access
-//
-// Use [NewWithBreaker] to obtain a reference to the underlying [Breaker]
-// struct. This allows programmatic state inspection ([Breaker.State]),
-// window counter export ([Breaker.Counts]) for dashboards and Prometheus,
-// and forced reset ([Breaker.Reset]) for health checks, admin endpoints,
-// or integration tests.
-//
-// # Middleware Ordering
-//
-// Place the circuit breaker after rate limiting and before timeout middleware:
-//
-// server.Use(ratelimit.New())
-// server.Use(circuitbreaker.New())
-// server.Use(timeout.New())
-//
-// This ensures rate-limited requests are rejected before reaching the breaker,
-// and timed-out requests are properly classified by the breaker.
-//
-// # In-Flight Requests During Transition
-//
-// Requests that are already executing when the breaker transitions to Open
-// continue to completion — only NEW requests are rejected. This is by design:
-// interrupting in-flight requests could cause data corruption or incomplete
-// operations.
-//
-// # Per-Endpoint Breakers
-//
-// To use separate breakers for different services or route groups, create
-// multiple instances and apply them to the appropriate groups:
-//
-// payments := s.Group("/api/payments")
-// payments.Use(circuitbreaker.New(circuitbreaker.Config{Threshold: 0.3}))
-//
-// # Thread Safety
-//
-// The breaker is safe for concurrent use. State reads and counter updates
-// use atomic operations (lock-free hot path). State transitions acquire a
-// mutex with double-check locking to prevent duplicate transitions.
-//
-// Basic usage with defaults (50% threshold, minimum 10 requests, 10s window):
-//
-// server.Use(circuitbreaker.New())
-//
-// Custom threshold and cooldown:
-//
-// server.Use(circuitbreaker.New(circuitbreaker.Config{
-// Threshold: 0.3,
-// MinRequests: 20,
-// CooldownPeriod: time.Minute,
-// }))
-//
-// Programmatic access for health checks:
-//
-// mw, breaker := circuitbreaker.NewWithBreaker()
-// server.Use(mw)
-// server.GET("/health", func(c *celeris.Context) error {
-// if breaker.State() == circuitbreaker.Open {
-// return c.JSON(503, map[string]string{"circuit": "open"})
-// }
-// return c.JSON(200, map[string]string{"circuit": "closed"})
-// })
-//
-// [ErrServiceUnavailable] is the exported sentinel error (503) returned when
-// the breaker is open, usable with errors.Is for error handling in upstream
-// middleware.
+// Full guides and examples: https://goceleris.dev/docs/middleware-traffic
package circuitbreaker
diff --git a/middleware/compress/doc.go b/middleware/compress/doc.go
index 6a8551c5..2bac8b85 100644
--- a/middleware/compress/doc.go
+++ b/middleware/compress/doc.go
@@ -1,169 +1,34 @@
// Package compress provides transparent response compression middleware
// for celeris.
//
-// Supported encodings are zstd, brotli, gzip, and deflate, negotiated via
-// the Accept-Encoding request header. The server-side priority order is
-// configurable; the default prefers zstd > brotli > gzip. Deflate is
-// supported but opt-in only (not in the default Encodings list) because
-// it is a legacy encoding superseded by gzip.
+// It negotiates an encoding from the Accept-Encoding request header and
+// compresses 2xx responses on the fly. Supported encodings are zstd, brotli
+// ("br"), gzip, and deflate; the default server-side priority is
+// zstd > br > gzip. Deflate is supported but opt-in (add "deflate" to
+// [Config].Encodings) because it is superseded by gzip.
//
-// Basic usage with defaults (zstd > br > gzip, MinLength 256):
+// [New] returns the middleware; call it with no arguments for defaults
+// (MinLength 256, the default encoding list and excluded content types) or
+// pass a [Config] to tune behavior:
//
// server.Use(compress.New())
//
-// Gzip-only with fastest compression:
+// Per-encoding levels are set via [Config].GzipLevel, [Config].BrotliLevel,
+// [Config].ZstdLevel, and [Config].DeflateLevel, each accepting a [Level]
+// (LevelDefault, LevelFastest, LevelBest, LevelNone) or an integer in the
+// encoding's valid range. Use [Config].Skip, [Config].SkipPaths,
+// [Config].MinLength, and [Config].ExcludedContentTypes to control what gets
+// compressed.
//
-// server.Use(compress.New(compress.Config{
-// Encodings: []string{"gzip"},
-// GzipLevel: compress.LevelFastest,
-// }))
+// The middleware buffers the response body before compressing, so it does not
+// apply to handlers that use [celeris.Context.StreamWriter]. For streaming
+// endpoints, wrap a StreamWriter with [NewCompressedStream] (gzip or brotli
+// only), or bypass compression with [Config].Skip.
//
-// Per-encoding compression levels are configurable via [Config].GzipLevel,
-// [Config].BrotliLevel, and [Config].ZstdLevel. Each accepts [Level]
-// constants (LevelDefault, LevelFastest, LevelBest, LevelNone) or an
-// integer within the encoding's valid range.
+// This package is a separate Go module (middleware/compress/go.mod); import
+// it as "github.com/goceleris/celeris/middleware/compress".
//
-// Exclude specific content types and paths:
+// # Documentation
//
-// server.Use(compress.New(compress.Config{
-// ExcludedContentTypes: []string{"image/", "video/", "audio/", "application/octet-stream"},
-// SkipPaths: []string{"/health", "/metrics"},
-// }))
-//
-// # Method Filtering
-//
-// HEAD and OPTIONS requests bypass compression entirely (only Vary is added).
-// This avoids unnecessary buffering for requests that do not produce
-// response bodies.
-//
-// # Content-Negotiation
-//
-// The middleware calls [celeris.Context.AcceptsEncodings] with the
-// configured encoding list. If the client does not accept any supported
-// encoding, the response passes through uncompressed.
-//
-// # Status Code Range
-//
-// Only 2xx responses are compressed. Error responses (4xx, 5xx), redirects
-// (3xx), and informational responses (1xx) pass through uncompressed.
-//
-// # Deflate (opt-in)
-//
-// The "deflate" encoding (raw DEFLATE, RFC 1951) is supported but not
-// included in the default Encodings list. Add it explicitly:
-//
-// server.Use(compress.New(compress.Config{
-// Encodings: []string{"zstd", "br", "gzip", "deflate"},
-// }))
-//
-// Deflate is a legacy encoding. Prefer gzip, brotli, or zstd for new
-// deployments.
-//
-// # Streaming Compression
-//
-// For endpoints that produce large or streaming responses, use
-// [NewCompressedStream] to wrap a [celeris.StreamWriter] with on-the-fly
-// gzip or brotli compression. This avoids buffering the entire response
-// body but does not support the expansion guard or MinLength threshold.
-//
-// # Pool Strategy
-//
-// Writer pools eliminate per-request allocations on the hot path:
-//
-// - gzip: [sync.Pool] of *gzip.Writer — Get, Reset, Write, Close, Put.
-// - brotli: [sync.Pool] of *brotli.Writer — same pattern.
-// - deflate: [sync.Pool] of *flate.Writer — same pattern.
-// - zstd: single thread-safe [zstd.Encoder] — EncodeAll, no pool needed.
-// - buffers: [sync.Pool] of *bytes.Buffer for gzip/brotli/deflate output.
-//
-// # StreamWriter Incompatibility
-//
-// This middleware uses [celeris.Context.BufferResponse] which is incompatible
-// with [celeris.Context.StreamWriter]. If the downstream handler uses
-// StreamWriter, BufferResponse returns nil and the streamed response
-// bypasses this middleware entirely. Use [Config].Skip to explicitly
-// exclude streaming endpoints.
-//
-// # Response Buffering (No Streaming Compression)
-//
-// This middleware buffers the entire response body in memory before
-// deciding whether and how to compress. This design enables the expansion
-// guard (flush original if compressed >= original) and correct
-// Content-Length headers, but means the full uncompressed body must fit
-// in memory.
-//
-// For endpoints that produce large responses (file downloads, CSV exports,
-// streaming JSON), use [Config].Skip or [Config].SkipPaths to bypass
-// compression entirely. Consider using the framework's StreamWriter for
-// such endpoints instead.
-//
-// compress.New(compress.Config{
-// SkipPaths: []string{"/api/export", "/files"},
-// Skip: func(c *celeris.Context) bool {
-// return strings.HasPrefix(c.Path(), "/download/")
-// },
-// })
-//
-// Competing frameworks (Echo, Fiber) offer streaming compression by
-// wrapping the response writer, but this prevents expansion detection
-// and accurate Content-Length.
-//
-// # MinLength Threshold
-//
-// Responses smaller than [Config].MinLength bytes (default 256) are not
-// compressed. Set MinLength to 0 to compress all non-empty responses.
-// This avoids wasting CPU on responses too small to benefit from compression.
-//
-// # Excluded Content Types
-//
-// Content types matching [Config].ExcludedContentTypes prefixes are
-// skipped. The default excludes "image/", "video/", and "audio/" since
-// these are typically already compressed.
-//
-// # Vary Header
-//
-// The middleware appends "Accept-Encoding" to the Vary header using
-// [celeris.Context.AddHeader] so that existing Vary values (e.g., Origin
-// from CORS) are preserved. The Vary header is added on all non-skipped
-// responses, including those that pass through uncompressed (below
-// MinLength, excluded content type, etc.), so that caches correctly
-// distinguish responses that vary by encoding.
-//
-// # Compression Expansion Guard
-//
-// If the compressed output is equal to or larger than the original body,
-// the original is sent uncompressed. This prevents pathological expansion
-// on already-compressed or incompressible data.
-//
-// # Ordering with ETag
-//
-// Compress runs OUTSIDE etag middleware. The recommended chain is:
-//
-// server.Use(compress.New()) // outermost
-// server.Use(etag.New()) // inner — ETag computed on uncompressed body
-//
-// # BREACH Attack Warning
-//
-// Compressing HTTPS responses that reflect user input (search queries,
-// form values, URL parameters) alongside secrets (CSRF tokens, session
-// IDs) can leak those secrets via the BREACH attack. Mitigation options:
-// - Exclude sensitive endpoints with [Config].SkipPaths or [Config].Skip
-// - Randomize padding on pages that mix user input and secrets
-// - Separate secret-bearing responses from user-controlled content
-//
-// See http://breachattack.com for details. This applies to any HTTP
-// compression layer, not just this middleware.
-//
-// # Response Size Measurement
-//
-// When metrics or OTel middleware runs before compress (the recommended
-// ordering), response size metrics reflect uncompressed application-level
-// sizes. See middleware/metrics and middleware/otel documentation.
-//
-// # Separate Sub-Module
-//
-// This package is a separate Go module (middleware/compress/go.mod) with
-// its own dependency set. Import it as:
-//
-// import "github.com/goceleris/celeris/middleware/compress"
+// Full guides and examples: https://goceleris.dev/docs/middleware-content
package compress
diff --git a/middleware/cors/doc.go b/middleware/cors/doc.go
index d122406b..6df31468 100644
--- a/middleware/cors/doc.go
+++ b/middleware/cors/doc.go
@@ -1,87 +1,25 @@
// Package cors provides Cross-Origin Resource Sharing (CORS) middleware
-// for celeris.
+// for Celeris.
//
-// The middleware handles preflight OPTIONS requests (detected via
-// OPTIONS method + Access-Control-Request-Method header) and sets the
-// appropriate Access-Control-* response headers. Header values are
-// pre-joined at initialization for zero-alloc responses on the hot path.
+// Call [New] with an optional [Config] to create a middleware handler.
+// With no arguments, all origins are allowed (AllowOrigins: ["*"]).
+// Pass a Config to restrict origins, enable credentials, set MaxAge, and more.
//
-// Allow all origins (default):
+// Key Config fields:
+// - AllowOrigins: static origin list; supports subdomain wildcards ("https://*.example.com").
+// - AllowOriginsFunc: custom callback to validate an origin string.
+// - AllowOriginRequestFunc: like AllowOriginsFunc but receives the full request context.
+// - AllowCredentials: set true to emit Access-Control-Allow-Credentials.
+// - MirrorRequestHeaders: reflect Access-Control-Request-Headers back on preflight.
+// - AllowPrivateNetwork: emit Access-Control-Allow-Private-Network on preflight.
+// - Skip / SkipPaths: bypass CORS processing for selected requests or paths.
//
-// server.Use(cors.New())
+// AllowOriginsFunc and AllowOriginRequestFunc cannot be combined with a wildcard
+// ("*") AllowOrigins entry. Using AllowCredentials with "*" panics at New.
+// Subdomain wildcards with AllowCredentials also panic unless
+// UnsafeAllowCredentialsWithWildcard is set.
//
-// Restrict to specific origins with credentials:
+// # Documentation
//
-// server.Use(cors.New(cors.Config{
-// AllowOrigins: []string{"https://example.com"},
-// AllowCredentials: true,
-// MaxAge: 3600,
-// }))
-//
-// # Dynamic Origin Validation
-//
-// [Config].AllowOriginsFunc validates origins with a custom function.
-// [Config].AllowOriginRequestFunc provides the full request context for
-// tenant-based or header-dependent origin checks. Neither may be combined
-// with a wildcard ("*") AllowOrigins entry.
-//
-// server.Use(cors.New(cors.Config{
-// AllowOriginsFunc: func(origin string) bool {
-// return strings.HasSuffix(origin, ".example.com")
-// },
-// }))
-//
-// # Subdomain Wildcards
-//
-// Origins may contain a single wildcard for subdomain matching
-// (e.g., "https://*.example.com"). Patterns with more than one wildcard
-// panic at initialization. By default, the wildcard matches a single
-// subdomain level only: "https://*.example.com" matches
-// "https://api.example.com" but NOT "https://a.b.example.com". This
-// prevents unintended deep-subdomain matching that could weaken origin
-// restrictions. The depth limit is enforced by counting dots in the
-// wildcard portion (max 0 additional dots for depth 1).
-//
-// # Cache Poisoning Prevention
-//
-// When specific origins (not wildcard "*") are configured, the middleware
-// adds a Vary: Origin header even on non-CORS requests (those without an
-// Origin header). This prevents intermediate caches from serving a
-// response generated for one origin to a different origin.
-//
-// # Header Mirroring
-//
-// When [Config].MirrorRequestHeaders is true, the middleware mirrors the
-// value of the Access-Control-Request-Headers header from the preflight
-// request back in Access-Control-Allow-Headers. Each header name is
-// validated against the RFC 7230 token charset, names longer than 128
-// bytes are silently dropped, and at most 20 header names are accepted.
-// When MirrorRequestHeaders is false (the default), the AllowHeaders
-// list is used (defaults to Origin, Content-Type, Accept, Authorization).
-//
-// # Private Network Access
-//
-// Set [Config].AllowPrivateNetwork to true to enable the Private Network
-// Access spec (Access-Control-Allow-Private-Network header on preflight).
-//
-// # Unsafe Credentials with Wildcard Subdomains
-//
-// Using AllowCredentials with a subdomain wildcard origin
-// (e.g., "https://*.example.com") panics by default because the browser
-// receives the echoed origin (not "*") with credentials, widening the
-// credential scope to every matching subdomain. Set
-// [Config].UnsafeAllowCredentialsWithWildcard to true to suppress the
-// panic if you fully understand the security implications.
-//
-// # Null Origin Warning
-//
-// The literal "null" origin is sent by sandboxed iframes, data: URLs,
-// file:// pages, and redirected requests. Because all of these share the
-// same "null" string, allowing it effectively grants access to any
-// sandboxed context. Treat "null" as an opaque origin and avoid adding
-// it to AllowOrigins unless you fully understand the implications.
-// AllowOriginsFunc is a safer alternative for case-by-case decisions.
-//
-// Using AllowCredentials with a wildcard origin ("*") panics at
-// initialization, as this combination is forbidden by the CORS spec.
+// Full guides and examples: https://goceleris.dev/docs/middleware-security
package cors
diff --git a/middleware/csrf/doc.go b/middleware/csrf/doc.go
index ffcbaaea..06edd305 100644
--- a/middleware/csrf/doc.go
+++ b/middleware/csrf/doc.go
@@ -2,87 +2,31 @@
// for celeris using the double-submit cookie pattern, with optional
// server-side token storage for enhanced security.
//
-// On safe HTTP methods (GET, HEAD, OPTIONS, TRACE by default) the
-// middleware generates or reuses a CSRF token, sets it as a cookie,
-// and stores it in the request context. On unsafe methods (POST, PUT,
-// DELETE, PATCH) it performs defense-in-depth checks (Sec-Fetch-Site,
-// Origin, Referer) then compares the cookie token against the request
-// token using constant-time comparison.
-//
-// Default usage (token in X-CSRF-Token header):
-//
-// server.Use(csrf.New())
-//
-// Custom token lookup from a form field:
-//
-// server.Use(csrf.New(csrf.Config{
-// TokenLookup: "form:_csrf",
-// }))
-//
-// # Retrieving the Token
-//
-// Use [TokenFromContext] to read the token in downstream handlers:
-//
-// token := csrf.TokenFromContext(c)
-//
-// # Server-Side Token Storage
-//
-// For enhanced security, configure [Config].Storage to validate tokens
-// against a server-side store (signed double-submit):
-//
-// store := csrf.NewMemoryStorage()
-// server.Use(csrf.New(csrf.Config{
-// Storage: store,
-// Expiration: 2 * time.Hour,
-// }))
-//
-// # Session Cookies
-//
-// Set [Config].CookieMaxAge to 0 for a browser-session-scoped cookie:
-//
-// server.Use(csrf.New(csrf.Config{
-// CookieMaxAge: 0,
-// }))
-//
-// # Trusted Origins
-//
-// [Config].TrustedOrigins allows cross-origin requests from specific
-// origins. Wildcard subdomain patterns are supported:
-//
-// server.Use(csrf.New(csrf.Config{
-// TrustedOrigins: []string{
-// "https://app.example.com",
-// "https://*.example.com",
-// },
-// }))
-//
-// # Sentinel Errors
-//
-// The package exports sentinel errors ([ErrForbidden], [ErrMissingToken],
-// [ErrTokenNotFound], [ErrOriginMismatch], [ErrRefererMissing],
-// [ErrRefererMismatch], [ErrSecFetchSite]) for use with errors.Is.
-//
-// # Security
-//
-// CookieSecure defaults to false for development convenience. Production
-// deployments MUST set CookieSecure: true to prevent cookie transmission
-// over unencrypted connections.
-//
-// CookieHTTPOnly is always enforced as true regardless of the user-supplied
-// Config value. This prevents client-side JavaScript from reading the CSRF
-// cookie, which is a defense-in-depth measure against XSS token theft.
-//
-// # Method Override Interaction
-//
-// When using the methodoverride middleware (registered via Server.Pre()),
-// the HTTP method is rewritten before CSRF validation. A POST request with
-// X-HTTP-Method-Override: PUT becomes a PUT by the time CSRF runs.
-//
-// IMPORTANT: Do not add PUT, DELETE, or PATCH to SafeMethods if you use
-// method override. Doing so would allow form submissions to bypass CSRF
-// token validation by tunneling through POST → PUT/DELETE/PATCH.
-//
-// The default SafeMethods (GET, HEAD, OPTIONS, TRACE) are safe because
-// methodoverride only overrides POST requests, and the typical override
-// targets (PUT, DELETE, PATCH) are not in the CSRF safe set.
+// On safe methods (GET, HEAD, OPTIONS, TRACE) the middleware generates or
+// reuses a CSRF token, sets it as a cookie, and stores it in the request
+// context. On unsafe methods it performs defense-in-depth checks
+// (Sec-Fetch-Site, Origin, Referer) then compares the cookie token against
+// the submitted token using constant-time comparison.
+//
+// Key exported symbols:
+// - [New] — constructs the middleware; accepts an optional [Config].
+// - [Config] — controls token lookup (header/form/query), cookie attributes,
+// trusted origins, server-side [Config.Storage], and [Config.SingleUseToken].
+// - [TokenFromContext] — retrieves the current CSRF token from a handler.
+// - [HandlerFromContext] — returns the [Handler] for the active middleware
+// instance; use [Handler.DeleteToken] on logout.
+// - [DeleteToken] — package-level convenience wrapper for the above.
+// - Sentinel errors ([ErrForbidden], [ErrMissingToken], [ErrTokenNotFound],
+// [ErrOriginMismatch], [ErrRefererMissing], [ErrRefererMismatch],
+// [ErrSecFetchSite]) for use with errors.Is.
+//
+// Production note: [Config.CookieSecure] defaults to false for development
+// convenience; set it to true in production. [Config.CookieHTTPOnly] is
+// always enforced as true regardless of the supplied value. When using the
+// methodoverride middleware, do not add PUT, DELETE, or PATCH to
+// [Config.SafeMethods] — doing so would allow bypass via POST tunneling.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-security
package csrf
diff --git a/middleware/debug/doc.go b/middleware/debug/doc.go
index 0f5b1602..9922ede0 100644
--- a/middleware/debug/doc.go
+++ b/middleware/debug/doc.go
@@ -35,4 +35,8 @@
//
// Server and Collector are optional; when nil, /routes returns [] and
// /metrics returns 501.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/observability
package debug
diff --git a/middleware/doc.go b/middleware/doc.go
index 6fec739e..88114c16 100644
--- a/middleware/doc.go
+++ b/middleware/doc.go
@@ -1,147 +1,24 @@
-// Package middleware provides production-ready middleware for celeris.
+// Package middleware is the umbrella for celeris's production-ready middleware catalog.
//
-// # Pre-Routing Middleware (Server.Pre)
+// It declares no exported symbols of its own. Each piece of middleware lives in
+// its own subpackage and exposes a New constructor returning a
+// celeris.HandlerFunc (for example cors.New, jwt.New, ratelimit.New,
+// compress.New). Install them at one of two points:
//
-// These run before the router matches the request and can mutate the
-// request method, path, scheme, host, or client IP:
+// - Server.Use installs route middleware that runs after the router matches a
+// request (logging, recovery, auth, CORS, rate limiting, compression, ...).
+// - Server.Pre installs pre-routing middleware that runs before matching and
+// may mutate the request method, path, scheme, host, or client IP
+// (proxy, redirect, rewrite, methodoverride). Pre-routing middleware that
+// writes a response MUST return without calling c.Next().
//
-// proxy — extract real client IP/scheme/host from trusted proxy headers
-// redirect — HTTPS, www, trailing-slash URL normalization
-// rewrite — regex-based URL rewriting (pattern → replacement)
-// methodoverride — override POST method via _method form field or header
+// Ordering matters: each layer should see the context the layers before it
+// established. See the documentation hub below for the recommended install
+// order, per-middleware configuration, and cross-cutting conventions (auth
+// stacking, the Vary header contract, and how the observe/metrics/otel
+// measurement systems relate).
//
-// Order matters: install proxy first (sets real client IP/scheme for all
-// downstream middleware), then redirect (uses scheme from proxy), then
-// rewrite (modifies path after redirect normalizes the URL), then
-// methodoverride (after path is finalized).
+// # Documentation
//
-// Short-circuit contract: writing a response in pre-routing middleware does
-// NOT auto-abort the chain. Custom pre-routing middleware that responds
-// (e.g. a redirect or 4xx error) MUST return WITHOUT calling c.Next() — the
-// shipped redirect middleware does this. If a pre-middleware writes a body
-// AND calls c.Next(), the router will run and may write a second response.
-//
-// # Recommended Middleware Ordering (Server.Use)
-//
-// Install middleware in this order so each layer sees the right context:
-//
-// healthcheck — health probes respond early; place first to skip downstream middleware
-// (install with Server.Use, NEVER with Server.Pre — pre-routing
-// rewrite rules could otherwise retarget the probe paths)
-// requestid — assign request ID first; all downstream logs include it
-// metrics/otel — Prometheus / OpenTelemetry: can also go after logger
-// logger — log every request with the ID from requestid
-// recovery — catch panics from everything below; logger records the 500
-// secure — set OWASP security headers before any response can escape
-// cors — handle preflight; must run before auth rejects OPTIONS
-// bodylimit — reject oversized bodies before parsing begins
-// ratelimit — shed load before expensive auth/business logic
-// circuitbreaker — trip open on error rate spike; after ratelimit, before timeout
-// [auth] — jwt / keyauth / basicauth (see Auth Stacking below)
-// csrf — validate CSRF token after authentication is established
-// session — load session (may depend on authenticated user)
-// debug — intercepts by path prefix (e.g. /debug/); can go anywhere
-// pprof — Go profiling endpoints (loopback-only by default)
-// swagger — OpenAPI spec + UI (CDN-loaded Swagger UI or Scalar)
-// static — static file server with directory browse (after security/auth if protecting)
-// adapters — (utility) stdlib ↔ celeris middleware conversion; not a chain member
-// timeout — bound handler execution; innermost wrapper before the route
-// singleflight — collapse identical in-flight requests; after timeout
-// compress — response compression; wraps etag (computes on uncompressed body)
-// etag — conditional responses (304 Not Modified); innermost transform
-// [handler] — route handler
-//
-// Example production stack:
-//
-// // Pre-routing
-// s.Pre(proxy.New(proxy.Config{TrustedProxies: []string{"10.0.0.0/8"}}))
-// s.Pre(redirect.HTTPSRedirect())
-// s.Pre(rewrite.New(rewrite.Config{Rules: []rewrite.Rule{{Pattern: `^/old/(.*)$`, Replacement: "/new/$1"}}}))
-// s.Pre(methodoverride.New())
-//
-// // Route middleware (install per the ordering list above; entries
-// // commented out below are optional but, when used, must keep their
-// // position in the chain).
-// s.Use(healthcheck.New())
-// s.Use(requestid.New())
-// // s.Use(metrics.New(...)) // optional: Prometheus
-// // s.Use(otel.New(...)) // optional: OpenTelemetry
-// // s.Use(logger.New()) // optional: structured request logs
-// s.Use(recovery.New())
-// s.Use(secure.New())
-// s.Use(cors.New())
-// // s.Use(bodylimit.New(...)) // optional: cap request body
-// // s.Use(ratelimit.New(...)) // optional: shed load
-// s.Use(circuitbreaker.New())
-// // s.Use(jwt.New(...)) // optional: auth (see Auth Stacking)
-// // s.Use(csrf.New()) // optional: after auth
-// // s.Use(session.New(...)) // optional: after auth
-// s.Use(timeout.New(timeout.Config{Timeout: 30 * time.Second}))
-// s.Use(singleflight.New())
-// s.Use(compress.New())
-// s.Use(etag.New())
-//
-// # Measurement System Roles
-//
-// Three complementary systems serve different operational needs:
-//
-// Core Collector (github.com/goceleris/celeris/observe) — Built into the
-// framework. Tracks total requests, error counts, and latency percentiles
-// including unmatched routes and panic recoveries. Zero external dependencies.
-// Use for lightweight internal diagnostics and health checks.
-//
-// Prometheus middleware (github.com/goceleris/celeris/middleware/metrics) — Production
-// metrics pipeline. Emits per-path, per-method, per-status histograms and
-// counters to Prometheus. Integrates with Grafana dashboards and alerting.
-// Use for production monitoring and SLO tracking.
-//
-// OpenTelemetry middleware (github.com/goceleris/celeris/middleware/otel) — Distributed
-// tracing and metrics. Creates spans per request with W3C trace context
-// propagation. Exports to any OTLP-compatible backend (Jaeger, Tempo, etc.).
-// Use for cross-service request correlation and latency breakdown.
-//
-// Counter overlap: each system records the same request independently —
-// observe.Collector.TotalRequests, prometheus celeris_requests_total, and
-// otel http.server.request.duration count are NOT shared. Enabling all
-// three gives three independent views of the same traffic; do NOT add
-// numbers across systems. Pick one as the source of truth for a given
-// chart, alert, or SLO.
-//
-// # Auth Stacking Pattern
-//
-// JWT and keyauth both support ContinueOnIgnoredError, which calls c.Next()
-// when ErrorHandler returns nil (i.e., the error was intentionally ignored).
-// Chain them for JWT-preferred authentication with API key fallback:
-//
-// jwtAuth := jwt.New(jwt.Config{
-// SigningKey: hmacSecret,
-// ContinueOnIgnoredError: true,
-// ErrorHandler: func(c *celeris.Context, err error) error {
-// return nil // ignore JWT failure, let keyauth try
-// },
-// })
-// keyAuth := keyauth.New(keyauth.Config{
-// Validator: func(c *celeris.Context, key string) (bool, error) {
-// return key == apiKey, nil
-// },
-// })
-// api := s.Group("/api", jwtAuth, keyAuth)
-//
-// Requests with a valid JWT proceed after jwtAuth. Requests without a JWT
-// (or with an invalid one) fall through to keyAuth. If neither succeeds,
-// keyAuth returns 401.
-//
-// # Vary Header Convention
-//
-// Several middleware set the Vary response header:
-//
-// - cors: Vary: Origin
-// - compress: Vary: Accept-Encoding
-//
-// All middleware use AddHeader (not SetHeader) for Vary to preserve values
-// set by other middleware. Handlers that need to set Vary MUST also use
-// AddHeader to avoid clobbering middleware-set values:
-//
-// c.AddHeader("vary", "Accept-Language") // correct
-// c.SetHeader("vary", "Accept-Language") // WRONG: clobbers cors/compress Vary
+// Full guides and examples: https://goceleris.dev/docs/middleware
package middleware
diff --git a/middleware/etag/doc.go b/middleware/etag/doc.go
index fea1d4ab..623ca743 100644
--- a/middleware/etag/doc.go
+++ b/middleware/etag/doc.go
@@ -1,136 +1,26 @@
-// Package etag provides automatic ETag generation and conditional response
-// middleware for celeris.
-//
-// The middleware computes a CRC-32 checksum of the response body and sets
-// an ETag header. On subsequent requests with a matching If-None-Match
-// header, it returns 304 Not Modified with no body, saving bandwidth.
-//
-// Basic usage:
-//
-// server.Use(etag.New())
-//
-// Strong ETags (byte-for-byte identical guarantee):
-//
-// server.Use(etag.New(etag.Config{Strong: true}))
-//
-// # Algorithm
-//
-// The ETag value is computed using CRC-32 (IEEE polynomial) of the full
-// response body. By default, weak ETags are generated in the format
-// W/"xxxxxxxx" where xxxxxxxx is the hex-encoded checksum. Strong ETags
-// omit the W/ prefix: "xxxxxxxx".
-//
-// CRC-32 is chosen for speed (single pass, no allocations beyond the
-// fixed-size stack buffer). It is not cryptographically secure, but
-// ETag is a cache validation mechanism, not a security primitive.
-//
-// # Custom Hash Function
-//
-// Use [Config].HashFunc to supply a custom hash. The function receives the
-// full response body and returns the opaque-tag string (without quotes or
-// W/ prefix -- those are added automatically based on the Strong setting):
-//
-// server.Use(etag.New(etag.Config{
-// HashFunc: func(body []byte) string {
-// h := sha256.Sum256(body)
-// return hex.EncodeToString(h[:16])
-// },
-// }))
-//
-// # Handler-Set ETags
-//
-// If the downstream handler already sets an ETag header, the middleware
-// respects it and does not recompute. The existing ETag is still checked
-// against If-None-Match for conditional 304 responses.
-//
-// # Weak Comparison (RFC 7232 Section 2.3.2)
-//
-// If-None-Match uses weak comparison: the W/ prefix is stripped before
-// comparing opaque-tags. This means W/"abc" matches both W/"abc" and
-// "abc", per the RFC.
-//
-// # If-None-Match: *
-//
-// The wildcard value "*" matches any ETag, unconditionally returning 304.
-//
-// # If-Match (Not Supported)
-//
-// This middleware does not handle If-Match (RFC 7232 Section 3.1). If-Match
-// is used for conditional writes (PUT/DELETE) and should be validated at
-// the application layer. The middleware only processes GET/HEAD requests.
-//
-// # If-None-Match Lenient Parsing
-//
-// The If-None-Match parser accepts unquoted tokens in addition to properly
-// quoted ETag values. While RFC 7232 specifies that entity-tags must be
-// quoted strings, real-world clients and proxies occasionally emit bare
-// tokens. The parser handles these gracefully to avoid spurious cache misses.
-//
-// # Method Filtering
-//
-// Only GET and HEAD requests are processed. POST, PUT, DELETE, PATCH,
-// and other methods bypass the middleware entirely (no buffering, no
-// ETag computation) with zero allocations.
-//
-// # 304 Not Modified Response
-//
-// When returning 304, the middleware sends only the ETag header and any
-// other response headers set by the handler. Content-Length is not included
-// because the buffered response body (from which Content-Length would be
-// derived) is discarded before the 304 is sent. This is RFC-compliant --
-// Content-Length is optional on 304 responses (RFC 9110 Section 8.6).
-//
-// # StreamWriter Incompatibility
-//
-// This middleware uses [celeris.Context.BufferResponse] which is incompatible
-// with [celeris.Context.StreamWriter]. If the downstream handler uses
-// StreamWriter, BufferResponse returns nil and the streamed response
-// bypasses this middleware entirely. Use [Config].Skip to explicitly
-// exclude streaming endpoints.
-//
-// # Full-Body Buffering
-//
-// The middleware buffers the entire response body in memory to compute
-// the ETag checksum. For large responses, this increases memory usage.
-// Use [Config].Skip or [Config].SkipPaths to bypass ETag processing
-// for endpoints that return large payloads (file downloads, streaming):
-//
-// server.Use(etag.New(etag.Config{
-// SkipPaths: []string{"/download", "/export"},
-// }))
-//
-// # Middleware Ordering
-//
-// ETag should run INSIDE compression middleware (closer to the handler).
-// This ensures the ETag is computed on the uncompressed body, so clients
-// that re-request without Accept-Encoding still get a cache hit:
-//
-// server.Use(compress.New()) // outer: compresses the response
-// server.Use(etag.New()) // inner: ETag on uncompressed body
-//
-// # Skipping
-//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// path exclusions. SkipPaths uses exact path matching:
-//
-// server.Use(etag.New(etag.Config{
-// SkipPaths: []string{"/health", "/metrics"},
-// }))
-//
-// # Non-2xx and Empty Responses
-//
-// Responses with non-2xx status codes or empty bodies are flushed
-// without ETag computation. Only successful responses with content
-// participate in conditional caching.
-//
-// # Session Middleware Interaction
-//
-// When session middleware runs alongside ETag, 304 Not Modified responses
-// still carry the Set-Cookie header from session refresh. This is correct
-// behavior (sessions must be refreshed for security), but it prevents
-// shared caches (CDN, reverse proxy) from caching 304 responses per
-// RFC 7234 Section 3.1. Browser-level caching is unaffected.
-//
-// For static assets that benefit from shared-cache ETag optimization,
-// consider using SkipPaths on the session middleware to exclude those paths.
+// Package etag provides ETag generation and conditional-response middleware
+// for celeris.
+//
+// [New] returns middleware that buffers the response body of GET and HEAD
+// requests, computes an ETag validator, and sets the ETag header. When a
+// request carries a matching If-None-Match header, it returns 304 Not
+// Modified with no body. Other methods and non-2xx or empty responses pass
+// through untouched. If the downstream handler already set an ETag header,
+// that tag is reused rather than recomputed.
+//
+// By default the validator is a CRC-32 (IEEE) checksum of the body, emitted
+// as a weak tag (W/"xxxxxxxx"). Configure behavior through [Config]:
+// - Strong: emit strong tags ("xxxxxxxx") instead of weak.
+// - HashFunc: supply a custom hash; it returns the opaque-tag, and the
+// quotes and optional W/ prefix are added automatically.
+// - Skip / SkipPaths: bypass buffering for dynamic conditions or for
+// exact paths (e.g. large downloads, streaming endpoints).
+//
+// Because the body is buffered to compute the tag, etag should run INSIDE
+// compression middleware so the validator is computed on the uncompressed
+// body.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-content
package etag
diff --git a/middleware/healthcheck/doc.go b/middleware/healthcheck/doc.go
index e612ae02..8c41324b 100644
--- a/middleware/healthcheck/doc.go
+++ b/middleware/healthcheck/doc.go
@@ -5,29 +5,20 @@
// paths (default "/livez", "/readyz", "/startupz") and returns a JSON
// status response. Non-matching requests pass through with zero overhead.
//
-// Basic usage (all probes return 200):
+// Key exported symbols: [New] constructs the handler; [Config] controls probe
+// paths, per-probe [Checker] functions, the [Config].Skip bypass predicate,
+// and [Config].CheckerTimeout (default [DefaultCheckerTimeout], 5 s). Set
+// [Config].CheckerTimeout to [FastPathTimeout] for trivial checkers that
+// cannot block (runs inline, no goroutine overhead). Panicking checkers are
+// recovered and return 503.
//
-// server.Use(healthcheck.New())
-//
-// Custom readiness check:
-//
-// server.Use(healthcheck.New(healthcheck.Config{
-// ReadyChecker: func(_ *celeris.Context) bool {
-// return db.Ping() == nil
-// },
-// }))
-//
-// Each checker is guarded by [Config].CheckerTimeout (default 5s); if it
-// does not return in time the probe responds 503. Set CheckerTimeout to
-// [FastPathTimeout] for trivial checkers that cannot block (runs inline,
-// no goroutine overhead). Panicking checkers are recovered and return 503.
-//
-// Setting a probe path to "" disables that probe. [Config].Skip bypasses
-// the middleware before path matching.
-//
-// Invalid paths (missing leading '/') or overlapping enabled paths cause
-// a panic at initialization. These are programming errors caught early.
+// Setting a probe path to "" disables that probe. Invalid paths (missing
+// leading '/') or overlapping enabled paths cause a panic at initialization.
//
// Response format: 200 {"status":"ok"} / 503 {"status":"unavailable"}.
// HEAD requests return the status code with no body.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware
package healthcheck
diff --git a/middleware/idempotency/doc.go b/middleware/idempotency/doc.go
index ca705976..cf76943a 100644
--- a/middleware/idempotency/doc.go
+++ b/middleware/idempotency/doc.go
@@ -1,42 +1,21 @@
-// Package idempotency implements the HTTP Idempotency-Key request pattern.
-//
-// # Semantics
-//
-// The middleware looks up the header named by [Config.KeyHeader]
-// (default "Idempotency-Key") and:
-//
-// 1. Validates the key is 1..MaxKeyLength printable ASCII.
-// 2. Attempts to atomically acquire a lock via [store.SetNXer].
-// 3. On acquisition, runs the handler and stores the captured
-// response under the key with [Config.TTL] expiry. The lock
-// entry is replaced by the completed entry atomically.
-// 4. On contention with an in-flight duplicate, returns 409 Conflict
-// via [Config.OnConflict] (no wait-and-retry).
-// 5. On a subsequent request that finds a completed entry, replays
-// the stored response without running the handler.
-//
-// # Body hash
-//
-// [Config.BodyHash] enables SHA-256 hashing of the request body (up
-// to [Config.MaxBodyBytes]) and compares it to the stored hash on
-// replay. Mismatches return 422 Unprocessable Entity — catching
-// clients that reuse a key with different payloads, which the IETF
-// draft recommends rejecting. Disabled by default because hashing
-// large bodies costs memory.
-//
-// # Store requirements
-//
-// The store must implement both [store.KV] and [store.SetNXer]. The
-// default [NewMemoryStore] is in-memory and sharded, suitable for
-// single-instance deployments. For multi-instance deployments, wire
-// a [middleware/session/redisstore.Store] — it satisfies both
-// interfaces. (Wrap with [store.Prefixed] to namespace alongside
-// session data.)
-//
-// # Lock expiry
-//
-// A crashed handler leaves the lock entry behind. Once
-// [Config.LockTimeout] passes, the key becomes acquirable again.
-// Choose LockTimeout ≥ the worst-case handler latency plus network
-// margin to avoid a second execution of a still-running request.
+// Package idempotency implements the HTTP Idempotency-Key request pattern for Celeris.
+//
+// [New] returns a middleware that deduplicates retried writes. When a request
+// carries the header named by [Config.KeyHeader] (default "Idempotency-Key"),
+// the middleware acquires an atomic lock via [KVStore], runs the handler once,
+// persists the response, and replays it on subsequent requests with the same
+// key. Concurrent duplicates return 409 Conflict (overridable via
+// [Config.OnConflict]). Requests without the key header pass through unchanged.
+//
+// Key types: [Config] controls all behaviour; [KVStore] is the store interface
+// ([store.KV] + [store.SetNXer]); [NewMemoryStore] provides a sharded
+// in-memory default suitable for single-instance deployments. For
+// multi-instance deployments, supply a Redis-backed store and namespace it
+// with [store.Prefixed]. Set [Config.BodyHash] to detect key reuse with
+// different payloads (returns 422 on mismatch). [ErrStoreMissingSetNX] is
+// returned by [New] when a provided store lacks atomic SetNX support.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-traffic
package idempotency
diff --git a/middleware/jwt/doc.go b/middleware/jwt/doc.go
index 36c4077d..55fbf115 100644
--- a/middleware/jwt/doc.go
+++ b/middleware/jwt/doc.go
@@ -22,48 +22,19 @@
// JWKSRefresh: 30 * time.Minute,
// }))
//
-// # Retrieving Token and Claims
+// Retrieve the validated token and claims in a handler:
//
// token := jwt.TokenFromContext(c)
// claims, ok := jwt.ClaimsFromContext[jwt.MapClaims](c)
//
-// # Token Lookup
+// Key configuration options: [Config].TokenLookup controls where the token is
+// extracted (comma-separated "source:name[:prefix]" pairs tried in order;
+// default "header:Authorization:Bearer "). [Config].ClaimsFactory creates a
+// fresh [Claims] instance per request for custom struct types. [Config].Skip
+// and [Config].SkipPaths bypass the middleware dynamically or by exact path.
+// Use [SignToken] to create signed tokens for testing or token issuance.
//
-// [Config].TokenLookup supports comma-separated sources tried in order.
-// Format: "source:name[:prefix]". Sources: header, query, cookie, form, param.
+// # Documentation
//
-// # Custom Claims
-//
-// Use [Config].ClaimsFactory for custom struct claims types:
-//
-// jwt.New(jwt.Config{
-// SigningKey: secret,
-// ClaimsFactory: func() jwt.Claims { return &MyClaims{} },
-// })
-//
-// # Creating Tokens
-//
-// token, err := jwt.SignToken(jwt.SigningMethodHS256, jwt.MapClaims{
-// "sub": "user-123",
-// "exp": float64(time.Now().Add(time.Hour).Unix()),
-// }, []byte("secret"))
-//
-// # Security Best Practices
-//
-// Always set "exp" on issued tokens. Use "aud" with [WithAudience] to
-// prevent token confusion. Prefer asymmetric algorithms (RS256, ES256) in
-// multi-service deployments. Store keys in environment variables or a
-// secrets manager. Serve JWKS endpoints over HTTPS.
-//
-// # Algorithm-Key-Type Binding
-//
-// - HS256/HS384/HS512: []byte
-// - RS256/RS384/RS512/PS256/PS384/PS512: *rsa.PublicKey / *rsa.PrivateKey
-// - ES256/ES384/ES512: *ecdsa.PublicKey / *ecdsa.PrivateKey
-// - EdDSA: ed25519.PublicKey / ed25519.PrivateKey
-//
-// # Skipping
-//
-// Set [Config].Skip to bypass dynamically, or [Config].SkipPaths for
-// exact-match path exclusions.
+// Full guides and examples: https://goceleris.dev/docs/middleware-auth
package jwt
diff --git a/middleware/jwt/jwt.go b/middleware/jwt/jwt.go
index 20a532f5..590cd752 100644
--- a/middleware/jwt/jwt.go
+++ b/middleware/jwt/jwt.go
@@ -16,19 +16,28 @@ import (
// errors.Is checks work (e.g. mixed jwt+keyauth stacks).
var ErrUnauthorized = celeris.ErrUnauthorized
+// unauthorizedCause carries a jwt-specific detail message while chaining to
+// celeris.ErrUnauthorized. It lets errors.Is(err, celeris.ErrUnauthorized) match
+// every jwt rejection — consistent with keyauth and basicauth — without changing
+// the text surfaced by HTTPError.Error.
+type unauthorizedCause struct{ msg string }
+
+func (u *unauthorizedCause) Error() string { return u.msg }
+func (u *unauthorizedCause) Unwrap() error { return celeris.ErrUnauthorized }
+
// ErrTokenMissing is returned when no token is found in the request.
-var ErrTokenMissing = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: missing or malformed token")}
+var ErrTokenMissing = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: missing or malformed token"}}
// ErrTokenInvalid is returned when the token fails validation for reasons
// other than expiration or malformation (e.g., bad signature, unknown kid).
-var ErrTokenInvalid = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: invalid or expired token")}
+var ErrTokenInvalid = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: invalid or expired token"}}
// ErrJWTExpired is returned when the token's exp claim is in the past.
-var ErrJWTExpired = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: token has expired")}
+var ErrJWTExpired = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: token has expired"}}
// ErrJWTMalformed is returned when the token cannot be parsed (bad
// encoding, wrong number of segments, etc.).
-var ErrJWTMalformed = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: token is malformed")}
+var ErrJWTMalformed = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: token is malformed"}}
// New creates a JWT authentication middleware with the given config.
func New(config ...Config) celeris.HandlerFunc {
diff --git a/middleware/jwt/unauthorized_match_test.go b/middleware/jwt/unauthorized_match_test.go
new file mode 100644
index 00000000..c61060a1
--- /dev/null
+++ b/middleware/jwt/unauthorized_match_test.go
@@ -0,0 +1,39 @@
+package jwt
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/goceleris/celeris"
+)
+
+// The jwt sentinels must satisfy errors.Is(err, celeris.ErrUnauthorized) so a
+// mixed auth stack (jwt + keyauth + basicauth) can match every 401 with one
+// check, as documented on celeris.ErrUnauthorized.
+func TestSentinelsMatchErrUnauthorized(t *testing.T) {
+ for _, e := range []error{ErrTokenMissing, ErrTokenInvalid, ErrJWTExpired, ErrJWTMalformed} {
+ if !errors.Is(e, celeris.ErrUnauthorized) {
+ t.Errorf("errors.Is(%v, celeris.ErrUnauthorized) = false, want true", e)
+ }
+ // The classifiedError wrapper (used on the real reject paths) must match too.
+ ce := &classifiedError{outer: e, inner: errors.New("detail")}
+ if !errors.Is(ce, celeris.ErrUnauthorized) {
+ t.Errorf("errors.Is(classifiedError{%v}, celeris.ErrUnauthorized) = false, want true", e)
+ }
+ }
+}
+
+// Wrapping ErrUnauthorized must not change the surfaced error text.
+func TestSentinelMessagesUnchanged(t *testing.T) {
+ cases := map[*celeris.HTTPError]string{
+ ErrTokenMissing: "code=401, message=Unauthorized, err=jwt: missing or malformed token",
+ ErrTokenInvalid: "code=401, message=Unauthorized, err=jwt: invalid or expired token",
+ ErrJWTExpired: "code=401, message=Unauthorized, err=jwt: token has expired",
+ ErrJWTMalformed: "code=401, message=Unauthorized, err=jwt: token is malformed",
+ }
+ for e, want := range cases {
+ if got := e.Error(); got != want {
+ t.Errorf("Error() = %q, want %q", got, want)
+ }
+ }
+}
diff --git a/middleware/keyauth/doc.go b/middleware/keyauth/doc.go
index 7c7aa1be..246bb939 100644
--- a/middleware/keyauth/doc.go
+++ b/middleware/keyauth/doc.go
@@ -2,46 +2,30 @@
//
// The middleware extracts an API key from a configurable source (header,
// query parameter, cookie, form field, or URL parameter), validates it via
-// a user-supplied function, and stores the authenticated key in the context
-// store under [ContextKey] ("keyauth_key"). Failed authentication returns
-// 401 with a WWW-Authenticate header.
+// a user-supplied [Config].Validator function, and stores the authenticated
+// key in the context store under [ContextKey] ("keyauth_key"). Failed
+// authentication returns 401 with a WWW-Authenticate header.
+//
+// Key exported symbols:
+// - [New] — constructs the middleware handler from a [Config].
+// - [Config] — controls key lookup, validation, skip rules, realm, auth
+// scheme, RFC 6750 challenge parameters, and success/error callbacks.
+// - [StaticKeys] — constant-time validator for a fixed set of API keys.
+// - [KeyFromContext] — retrieves the validated key from a request context.
+// - [ErrMissingKey], [ErrUnauthorized] — sentinel 401 errors usable with
+// errors.Is.
//
// [Config].Validator is required; omitting it panics at initialization.
+// [Config].KeyLookup supports comma-separated fallback sources, e.g.
+// "header:Authorization:Bearer ,query:api_key". Set [Config].Skip or
+// [Config].SkipPaths to bypass the middleware selectively; OPTIONS requests
+// are always skipped to allow CORS preflight through.
//
-// Simple usage with static keys (constant-time comparison):
+// Security note: keys extracted from query strings appear in access logs,
+// browser history, and Referer headers. Prefer header-based lookup for
+// sensitive environments.
//
-// server.Use(keyauth.New(keyauth.Config{
-// Validator: keyauth.StaticKeys("key-1", "key-2"),
-// }))
+// # Documentation
//
-// Custom validator with context access:
-//
-// server.Use(keyauth.New(keyauth.Config{
-// Validator: func(c *celeris.Context, key string) (bool, error) {
-// return checkDB(c, key)
-// },
-// KeyLookup: "query:api_key",
-// }))
-//
-// Multiple sources can be comma-separated for fallback:
-//
-// KeyLookup: "header:Authorization:Bearer ,query:api_key"
-//
-// Use [KeyFromContext] to retrieve the authenticated key downstream.
-//
-// [Config].ChallengeParams controls RFC 6750 parameters in the
-// WWW-Authenticate header (error, error_description, error_uri, scope).
-//
-// [StaticKeys] uses constant-time comparison via [crypto/subtle] to
-// prevent timing side-channels.
-//
-// [ErrUnauthorized] and [ErrMissingKey] are exported sentinel errors
-// (both 401) usable with errors.Is for upstream error handling.
-//
-// Set [Config].Skip to bypass the middleware dynamically, or
-// [Config].SkipPaths for exact-match path exclusions.
-//
-// Security: API keys in query strings (`query:api_key`) are logged in
-// access logs, browser history, and Referer headers. Prefer header-based
-// key lookup for sensitive environments.
+// Full guides and examples: https://goceleris.dev/docs/middleware-auth
package keyauth
diff --git a/middleware/logger/doc.go b/middleware/logger/doc.go
index 07d17806..8afdd8c7 100644
--- a/middleware/logger/doc.go
+++ b/middleware/logger/doc.go
@@ -1,82 +1,28 @@
// Package logger provides HTTP request logging middleware for celeris.
//
-// The middleware logs each request with configurable fields including
-// method, path, status, latency, and bytes written. It supports
-// both standard slog handlers and the zero-alloc [FastHandler].
-//
-// Basic usage with the default slog output:
-//
-// server.Use(logger.New())
-//
-// Using the zero-alloc FastHandler with color output:
-//
-// mw := logger.New(logger.Config{
-// Output: slog.New(logger.NewFastHandler(os.Stderr, &logger.FastHandlerOptions{
-// Color: true,
-// })),
-// })
-// server.Use(mw)
-//
-// # Body Capture
-//
-// Set [Config].CaptureRequestBody and/or [Config].CaptureResponseBody to
-// log request and response bodies. Bodies are truncated to
-// [Config].MaxCaptureBytes (default 4096).
-//
-// # Sensitive Header Redaction
-//
-// [Config].SensitiveHeaders lists header names whose values are redacted.
-// When nil, [DefaultSensitiveHeaders] is used. Set to an empty slice to
-// disable all redaction.
-//
-// # Request ID Integration
-//
-// The middleware reads the request ID from the context store
-// (key "request_id") first, falling back to the x-request-id header.
-//
-// # Predefined Configurations
-//
-// [CLFConfig] returns a Config for Common Log Format style output.
-// [JSONConfig] returns a Config for structured JSON output.
-// Both return a [Config] value that can be further customized:
-//
-// cfg := logger.CLFConfig()
-// cfg.SkipPaths = []string{"/health"}
-// server.Use(logger.New(cfg))
-//
-// # Design Rationale
-//
-// All output is structured through Go's [log/slog] package. No template
-// strings are provided. The [Config].Fields callback supplies arbitrary
-// extensibility. [FastHandler] formats directly into pooled byte buffers,
-// avoiding fmt.Sprintf and time.Format entirely.
-//
-// # Query Parameter Security
-//
-// Security: LogQueryParams logs raw query strings which may contain
-// sensitive values (OAuth tokens, API keys, session identifiers).
-// Consider using SkipPaths for sensitive endpoints or implementing a
-// custom Fields function that redacts sensitive query parameters.
-//
-// # Skipping
-//
-// Set [Config].Skip to bypass dynamically, or [Config].SkipPaths for
-// exact-match path exclusions.
-//
-// # Middleware Order
-//
-// Register after requestid for request ID inclusion in logs.
-//
-// # Reverse Proxy Integration
-//
-// When running behind a reverse proxy, install the proxy middleware via
-// Server.Pre() so that Logger sees the real client IP:
-//
-// server.Pre(proxy.New(proxy.Config{
-// TrustedProxies: []string{"10.0.0.0/8"},
-// }))
-// server.Use(logger.New()) // now logs the real client IP
-//
-// Without proxy middleware, Logger records the reverse proxy's IP address,
-// not the end user's.
+// [New] returns a [celeris.HandlerFunc] that logs each request with method,
+// path, status, latency, bytes written, client IP, and request ID. All output
+// is routed through Go's [log/slog] package; supply any slog.Handler via
+// [Config].Output, or use [NewFastHandler] for zero-alloc pooled-buffer output.
+//
+// Two preset constructors cover the most common cases: [CLFConfig] produces
+// Common Log Format style output; [JSONConfig] produces structured JSON via
+// slog.JSONHandler. Both return a [Config] that can be further customised before
+// passing to [New].
+//
+// Notable Config fields:
+// - CaptureRequestBody / CaptureResponseBody — log bodies, truncated to MaxCaptureBytes (default 4096).
+// - SensitiveHeaders — header values to redact; nil uses [DefaultSensitiveHeaders].
+// - LogFormValues / SensitiveFormFields — log form fields with optional redaction; use [DefaultSensitiveFormFields] as a safe starting list.
+// - Skip / SkipPaths — bypass the middleware dynamically or for exact-match paths.
+// - Fields / Done — callbacks for custom attributes and post-log hooks.
+// - LogContextKeys — emit arbitrary context-store values as "ctx." attributes.
+//
+// Register after the requestid middleware so request IDs are available in every
+// log entry. When running behind a reverse proxy, install the proxy middleware
+// via Server.Pre() before logger.New() so the real client IP is recorded.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/observability
package logger
diff --git a/middleware/methodoverride/doc.go b/middleware/methodoverride/doc.go
index 617b6156..5fe7aa6b 100644
--- a/middleware/methodoverride/doc.go
+++ b/middleware/methodoverride/doc.go
@@ -1,77 +1,30 @@
-// Package methodoverride provides HTTP method override middleware for
-// celeris.
-//
-// HTML forms can only submit GET and POST requests. This middleware allows
-// clients to tunnel PUT, PATCH, DELETE, and other methods through a POST
-// request by specifying the intended method in a header or form field.
-//
-// # IMPORTANT: Use Server.Pre(), Not Server.Use()
-//
-// This middleware MUST be registered via [celeris.Server.Pre], not
-// [celeris.Server.Use]. With Server.Use, the router has already matched
-// the request based on the original method, making the override ineffective.
-// Using Server.Use will silently produce wrong routing behavior.
-//
-// Register the middleware with [celeris.Server.Pre] so the method is
-// rewritten before routing:
-//
-// s := celeris.New()
-// s.Pre(methodoverride.New())
-//
-// # Override Sources
-//
-// By default, the middleware checks the form field [DefaultFormField]
-// ("_method") first, then the header [DefaultHeader]
-// ("X-HTTP-Method-Override"). Only POST requests are eligible for override
-// (configurable via [Config].AllowedMethods).
-//
-// # Custom Getters
-//
-// Use [HeaderGetter] to read from a specific header only:
-//
-// s.Pre(methodoverride.New(methodoverride.Config{
-// Getter: methodoverride.HeaderGetter("X-Method"),
-// }))
-//
-// Use [FormFieldGetter] to read from a specific form field only:
-//
-// s.Pre(methodoverride.New(methodoverride.Config{
-// Getter: methodoverride.FormFieldGetter("_http_method"),
-// }))
-//
-// Use [FormThenHeaderGetter] to check a custom form field first, then a
-// custom header (same order as the default getter but with custom names):
-//
-// s.Pre(methodoverride.New(methodoverride.Config{
-// Getter: methodoverride.FormThenHeaderGetter("_method", "X-HTTP-Method"),
-// }))
-//
-// [QueryGetter] reads from a URL query parameter. This is provided for
-// parity with Echo but carries security risks: query parameters are
-// embeddable in links and images, potentially enabling cross-site method
-// override attacks. Use only when the security implications are understood.
-//
-// # Target Methods
-//
-// By default, only PUT, DELETE, and PATCH are valid override targets
-// (configurable via [Config].TargetMethods). Override values not in this
-// list are silently ignored, preventing clients from overriding to
-// arbitrary methods such as CONNECT or TRACE.
-//
-// # Validation
-//
-// [Config].AllowedMethods and [Config].TargetMethods must not contain empty
-// or whitespace-only strings. The middleware panics at initialization if
-// this constraint is violated.
-//
-// # Skipping
-//
-// Set [Config].Skip to bypass the middleware dynamically, or
-// [Config].SkipPaths for exact-match path exclusions.
-//
-// # CSRF Middleware Interaction
-//
-// Method override changes the request method before CSRF middleware runs.
-// Ensure that overridden methods (PUT, DELETE, PATCH) are NOT in the CSRF
-// middleware's SafeMethods list. See middleware/csrf documentation for details.
+// Package methodoverride provides HTTP method override middleware for celeris.
+//
+// HTML forms can only submit GET and POST requests. This middleware lets
+// clients tunnel PUT, PATCH, DELETE, and other methods through a POST request
+// by specifying the intended method via a form field or header.
+//
+// Register with [celeris.Server.Pre] (not Server.Use) so the method is
+// rewritten before routing occurs. [New] accepts an optional [Config] to
+// customise behaviour:
+//
+// - [Config].AllowedMethods — original methods eligible for override
+// (default: POST).
+// - [Config].TargetMethods — valid override targets (default: PUT, DELETE,
+// PATCH); values outside this set are silently ignored.
+// - [Config].Getter — function that extracts the override value; built-in
+// helpers are [HeaderGetter], [FormFieldGetter], [FormThenHeaderGetter],
+// and [QueryGetter] (note: QueryGetter carries CSRF risk via embeddable
+// URLs).
+// - [Config].Skip / [Config].SkipPaths — skip the middleware per-request or
+// by exact path.
+//
+// The default getter checks form field [DefaultFormField] ("_method") first,
+// then header [DefaultHeader] ("X-HTTP-Method-Override"). [Config].AllowedMethods
+// and [Config].TargetMethods must not contain empty or whitespace-only strings;
+// the middleware panics at initialisation if this constraint is violated.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-routing-helpers
package methodoverride
diff --git a/middleware/metrics/doc.go b/middleware/metrics/doc.go
index be8332ef..16791ade 100644
--- a/middleware/metrics/doc.go
+++ b/middleware/metrics/doc.go
@@ -4,11 +4,12 @@
// and exposes them in Prometheus text exposition format at a configurable
// endpoint (default "/metrics").
//
-// Basic usage:
+// Register with default settings:
//
// server.Use(metrics.New())
//
-// Custom configuration:
+// Or supply a [Config] to customise the endpoint path, namespace, subsystem,
+// histogram buckets, label extensions, or the backing [prometheus.Registry]:
//
// server.Use(metrics.New(metrics.Config{
// Path: "/prom",
@@ -32,19 +33,9 @@
// - {ns}_{sub}_active_requests (Gauge): in-flight requests
//
// The subsystem segment is omitted when [Config].Subsystem is empty.
-//
-// # Custom Labels
-//
-// Use [Config].LabelFuncs to add custom label dimensions. Functions are
-// called after c.Next() returns:
-//
-// server.Use(metrics.New(metrics.Config{
-// LabelFuncs: map[string]func(*celeris.Context) string{
-// "region": func(c *celeris.Context) string {
-// return c.Header("x-region")
-// },
-// },
-// }))
+// Base labels on all metrics are method, path, and status. Add custom
+// dimensions via [Config].LabelFuncs; keys must not conflict with the
+// three reserved base labels.
//
// # Cardinality Protection
//
@@ -53,10 +44,7 @@
//
// # Histogram Buckets
//
-// [DefaultBuckets] provides fine-grained latency boundaries:
-//
-// []float64{0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 5}
-//
+// [DefaultBuckets] provides fine-grained sub-millisecond latency boundaries.
// Override with [Config].Buckets for duration histograms or
// [Config].SizeBuckets for request/response size histograms.
//
@@ -73,4 +61,8 @@
// (metrics before compress), this records the uncompressed application-level
// size. If metrics runs after compress, it records the compressed network-level
// size. Be aware of this when interpreting response size dashboards.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/observability
package metrics
diff --git a/middleware/otel/doc.go b/middleware/otel/doc.go
index 2827f0cd..d9da26e9 100644
--- a/middleware/otel/doc.go
+++ b/middleware/otel/doc.go
@@ -1,45 +1,27 @@
// Package otel provides OpenTelemetry tracing and metrics middleware
// for celeris.
//
-// The middleware creates a server span per request, propagates context
-// via configured propagators, and records request duration and active
-// request count as OTel metrics.
-//
-// Basic usage with global providers:
-//
-// server.Use(otel.New())
-//
-// Explicit providers:
-//
-// server.Use(otel.New(otel.Config{
-// TracerProvider: tp,
-// MeterProvider: mp,
-// }))
-//
-// Downstream handlers can access the span via [SpanFromContext]:
-//
-// span := otel.SpanFromContext(c)
-//
-// Spans are named "METHOD /route/pattern" by default; override with
+// [New] returns a [celeris.HandlerFunc] that creates a server span per
+// request, propagates context via configured propagators, and records
+// request duration and active request count as OTel metrics. By default
+// it reads the global TracerProvider, MeterProvider, and TextMapPropagator;
+// supply a [Config] to use explicit providers.
+//
+// Downstream handlers retrieve the active span with [SpanFromContext].
+// Span names default to "METHOD /route/pattern"; override with
// [Config].SpanNameFormatter. Attributes follow OTel semconv v1.32.0.
//
-// PII controls: client.address is opt-in ([Config].CollectClientIP);
-// user_agent.original is opt-out ([Config].CollectUserAgent).
-//
-// Filtering: use [Config].Skip, [Config].SkipPaths, or [Config].Filter
-// to exclude endpoints from tracing.
-//
// Metrics recorded: http.server.request.duration (s),
// http.server.active_requests, http.server.request.body.size (By),
// http.server.response.body.size (By). Set [Config].DisableMetrics for
-// tracing-only mode. Use [Config].CustomAttributes and
-// [Config].CustomMetricAttributes for per-request attribute injection.
+// tracing-only mode.
+//
+// PII controls: client.address is opt-in ([Config].CollectClientIP);
+// user_agent.original is opt-out ([Config].CollectUserAgent).
+// Use [Config].Skip, [Config].SkipPaths, or [Config].Filter to exclude
+// endpoints from tracing.
//
-// # Response Size and Compression
+// # Documentation
//
-// The http.response.body.size metric records c.BytesWritten() at the point
-// the OTel middleware runs. With the recommended ordering (otel before
-// compress), this reflects uncompressed application-level sizes. Changing
-// the middleware order to place otel after compress would record compressed
-// sizes instead.
+// Full guides and examples: https://goceleris.dev/docs/observability
package otel
diff --git a/middleware/pprof/doc.go b/middleware/pprof/doc.go
index 083edfc3..5ec7525d 100644
--- a/middleware/pprof/doc.go
+++ b/middleware/pprof/doc.go
@@ -5,54 +5,17 @@
// (default "/debug/pprof") and dispatches to the matching pprof handler.
// Non-matching requests pass through to the next handler with zero overhead.
//
-// # Security
-//
-// By default, access is restricted to loopback addresses (127.0.0.1, ::1)
-// via [Config].AuthFunc. This uses the raw TCP peer address, NOT
-// X-Forwarded-For. Behind a reverse proxy, set AuthFunc to a scheme that
-// does not rely on RemoteAddr (e.g., shared secret header).
-//
-// Profiling endpoints expose sensitive runtime internals. Never expose them
-// publicly in production without proper authentication.
-//
-// # Endpoints
-//
-// All endpoints are relative to the configured prefix:
-//
-// {prefix}/ — index page listing available profiles
-// {prefix}/cmdline — command-line arguments
-// {prefix}/profile — CPU profile (accepts ?seconds=N)
-// {prefix}/symbol — symbol lookup
-// {prefix}/trace — execution trace (accepts ?seconds=N)
-// {prefix}/allocs — allocation profile
-// {prefix}/block — block profile
-// {prefix}/goroutine — goroutine stacks
-// {prefix}/heap — heap profile
-// {prefix}/mutex — mutex contention profile
-// {prefix}/threadcreate — thread creation profile
-//
-// # Ordering
-//
-// Place pprof after the debug middleware in the middleware chain. Since pprof
-// intercepts by path prefix, it can be installed at any position, but placing
-// it after debug avoids shadowing debug endpoints when both share the
-// /debug/ prefix namespace.
-//
-// # Basic usage
+// Use [New] with an optional [Config] to mount the profiling endpoints:
//
// server.Use(pprof.New())
//
-// # Custom AuthFunc
-//
-// server.Use(pprof.New(pprof.Config{
-// AuthFunc: func(c *celeris.Context) bool {
-// return c.Header("x-pprof-token") == os.Getenv("PPROF_TOKEN")
-// },
-// }))
+// [Config].Prefix controls the URL prefix (default "/debug/pprof").
+// [Config].AuthFunc gates access; the default restricts to loopback addresses
+// (127.0.0.1 and ::1) using the raw TCP peer address — set a custom AuthFunc
+// when running behind a reverse proxy. [Config].Skip and [Config].SkipPaths
+// provide request-level bypass logic.
//
-// # Skipping
+// # Documentation
//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// exact-match path exclusions. Skipped requests call c.Next() without
-// invoking the auth check or serving profiles.
+// Full guides and examples: https://goceleris.dev/docs/observability
package pprof
diff --git a/middleware/protobuf/doc.go b/middleware/protobuf/doc.go
index 9dedd889..7ea31d4a 100644
--- a/middleware/protobuf/doc.go
+++ b/middleware/protobuf/doc.go
@@ -1,50 +1,26 @@
// Package protobuf provides Protocol Buffers serialization for celeris.
//
-// Protocol Buffers was removed from the celeris core to avoid forcing
-// google.golang.org/protobuf as a transitive dependency. This package
-// provides equivalent functionality as standalone functions.
-//
-// # Basic Usage
-//
-// Write a protobuf response:
-//
-// protobuf.Write(c, 200, &myProto)
-//
-// Parse a protobuf request:
-//
-// var msg pb.MyMessage
-// if err := protobuf.BindProtoBuf(c, &msg); err != nil {
-// return err
-// }
-//
-// Auto-detect content type:
-//
-// var msg pb.MyMessage
-// if err := protobuf.Bind(c, &msg); err != nil {
-// return err
-// }
-//
-// # Content Negotiation
-//
-// Use [Respond] to serve protobuf or JSON based on the Accept header:
-//
-// protobuf.Respond(c, 200, &myProto, myJSONStruct)
-//
-// # Middleware with Custom Options
-//
-// Install the middleware for custom marshal/unmarshal options:
-//
-// s.Use(protobuf.New(protobuf.Config{
-// MarshalOptions: proto.MarshalOptions{Deterministic: true},
-// }))
-//
-// Then use [FromContext] in handlers:
-//
-// pb := protobuf.FromContext(c)
-// pb.Write(200, &myProto)
-//
-// # Content Types
-//
-// Both "application/x-protobuf" (primary) and "application/protobuf" are
-// recognized. Responses use "application/x-protobuf".
+// Protocol Buffers support was split from the celeris core to avoid pulling
+// google.golang.org/protobuf as a transitive dependency. This package provides
+// equivalent functionality as standalone helpers and an optional middleware.
+//
+// Key exports:
+// - [Write] — marshal a proto.Message and write it with status code.
+// - [BindProtoBuf] — read the request body and unmarshal it as protobuf.
+// - [Bind] — like BindProtoBuf but first checks the Content-Type header,
+// returning [ErrNotProtoBuf] when it is not a protobuf content type.
+// - [Respond] — content-negotiate between protobuf and JSON using the
+// Accept header, honoring q=0 exclusions per RFC 7231 §5.3.1.
+// - [New] / [Config] — optional middleware that stashes marshal/unmarshal
+// options in the request context for retrieval via [FromContext].
+// - [PoolEvictions] — monotonic counter for buffers discarded from the
+// internal marshal pool; wire into metrics to detect large messages.
+//
+// Both "application/x-protobuf" ([ContentType]) and "application/protobuf"
+// ([ContentTypeAlt]) are accepted on inbound requests. Responses written by
+// [Write] always use "application/x-protobuf".
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-content
package protobuf
diff --git a/middleware/proxy/doc.go b/middleware/proxy/doc.go
index 92640ae6..4716192d 100644
--- a/middleware/proxy/doc.go
+++ b/middleware/proxy/doc.go
@@ -1,121 +1,32 @@
-// Package proxy extracts real client IP, scheme, and host from trusted
+// Package proxy extracts the real client IP, scheme, and host from trusted
// reverse proxy headers.
//
// When a celeris server sits behind a load balancer or reverse proxy
-// (e.g. Nginx, Cloudflare, AWS ALB), the TCP peer address is the proxy,
-// not the end user. This middleware inspects X-Forwarded-For, X-Real-Ip,
-// X-Forwarded-Proto, and X-Forwarded-Host headers -- but only when the
-// immediate peer is in the configured [Config].TrustedProxies list.
+// (e.g. Nginx, Cloudflare, AWS ALB), the TCP peer is the proxy, not the end
+// user. [New] returns a middleware that inspects X-Forwarded-For, X-Real-Ip,
+// X-Forwarded-Proto, and X-Forwarded-Host -- but only when the immediate peer
+// is in the configured [Config.TrustedProxies] list. An empty TrustedProxies
+// list is the safe default: the middleware becomes a no-op and trusts nothing,
+// preventing IP spoofing. There is no "trust all" toggle; specify CIDRs (or
+// "0.0.0.0/0") explicitly. Invalid entries panic at [New].
//
-// # Security Model
+// Run it via [celeris.Server.Pre] so the overrides take effect before routing
+// and before any middleware reads ClientIP, Scheme, or Host:
//
-// An empty TrustedProxies list is the safe default: the middleware becomes
-// a no-op and never trusts any forwarded header. This prevents IP spoofing
-// when the server is exposed directly to the internet.
-//
-// There is no TrustAllProxies option. Trusting all proxies defeats the
-// purpose of the right-to-left walk and opens the door to trivial
-// IP spoofing. If you need to trust all proxies in a controlled
-// environment, specify the CIDR explicitly (e.g. "0.0.0.0/0").
-//
-// # Disable* Convention
-//
-// [Config] uses the Disable* pattern for ForwardedProto and ForwardedHost.
-// The Go zero value (false) means enabled, so a minimal Config literal
-// automatically processes both headers:
-//
-// server.Pre(proxy.New(proxy.Config{
-// TrustedProxies: []string{"10.0.0.0/8"},
-// }))
-//
-// To opt out of one or both, set the corresponding Disable* field:
-//
-// server.Pre(proxy.New(proxy.Config{
-// TrustedProxies: []string{"10.0.0.0/8"},
-// DisableForwardedProto: true,
-// }))
-//
-// # TrustedProxies
-//
-// Accepts CIDR notation ("10.0.0.0/8") and bare IPs ("10.0.0.1").
-// Bare IPs are expanded to /32 (IPv4) or /128 (IPv6) at init time.
-// Invalid entries cause a panic so misconfigurations surface immediately.
-//
-// Common provider CIDR ranges:
-// - Cloudflare: https://www.cloudflare.com/ips/
-// - AWS CloudFront: https://ip-ranges.amazonaws.com/ip-ranges.json
-// - GCP: https://cloud.google.com/load-balancing/docs/https#target-proxies
-//
-// # TrustedHeaders
-//
-// By default, "x-forwarded-for" and "x-real-ip" are inspected. These two
-// headers have built-in logic: XFF is walked right-to-left, and X-Real-IP
-// is validated with net/netip.ParseAddr.
-//
-// Any other header name added to TrustedHeaders (e.g. "cf-connecting-ip",
-// "true-client-ip") is treated as a single-value IP header. The value is
-// parsed with netip.ParseAddr and used only if valid. Custom headers are
-// checked after XFF and X-Real-IP, in order of appearance.
-//
-// # X-Forwarded-For Walk
-//
-// The middleware walks the X-Forwarded-For chain right-to-left, skipping
-// entries that match a trusted network. The first untrusted IP is taken as
-// the real client IP. This algorithm is resilient against left-side
-// spoofing because attacker-controlled entries appear on the left, while
-// each trusted proxy appends to the right.
-//
-// If a malformed (non-IP) entry is encountered during the walk, traversal
-// stops and no client IP override is applied, falling back to the peer
-// address.
-//
-// # X-Forwarded-Host Validation
-//
-// When X-Forwarded-Host processing is enabled (the default), the value is validated
-// before being applied. Values containing \r, \n, \x00, /, \, ?, #, or @
-// are rejected to prevent header injection and path traversal. Hosts
-// exceeding 253 bytes (the DNS maximum per RFC 1035) are also rejected.
-//
-// # IPv4-Mapped IPv6
-//
-// IPv4-mapped IPv6 addresses (::ffff:10.0.0.1) are normalized via
-// netip.Addr.Unmap so that a trusted network specified as 10.0.0.0/8
-// correctly matches the mapped form.
-//
-// # ForwardedProto and ForwardedHost
-//
-// By default (Disable* fields false), X-Forwarded-Proto overrides
-// [celeris.Context.Scheme] and X-Forwarded-Host overrides
-// [celeris.Context.Host]. Only "http" and "https" are accepted for proto;
-// other values are silently ignored.
-//
-// # RFC 7239 (Forwarded)
-//
-// The standard Forwarded header (RFC 7239) is not currently implemented.
-// Most real-world proxies emit X-Forwarded-For, and the middleware focuses
-// on that de facto standard. RFC 7239 support may be added in a future
-// release.
-//
-// # Runs via Server.Pre
-//
-// This middleware is designed to run via [celeris.Server.Pre] so that the
-// overrides take effect before routing and before any downstream middleware
-// reads ClientIP, Scheme, or Host.
-//
-// s := celeris.New(celeris.Config{Addr: ":8080"})
// s.Pre(proxy.New(proxy.Config{
// TrustedProxies: []string{"10.0.0.0/8", "172.16.0.0/12"},
// }))
//
-// # Downstream Middleware
-//
-// Several middleware depend on proxy for correct values:
-// - logger: logs c.ClientIP() — without proxy, logs the proxy's IP
-// - ratelimit: keys rate limits by c.ClientIP() — without proxy, all
-// clients behind the same proxy share one rate-limit bucket
+// [Config.TrustedHeaders] selects which headers to inspect (default
+// "x-forwarded-for" and "x-real-ip"); X-Forwarded-For is walked right-to-left
+// to defeat left-side spoofing, while any other listed header is treated as a
+// single-value IP. The Disable* fields use the zero-value-enabled convention:
+// [Config.DisableForwardedProto] and [Config.DisableForwardedHost] both default
+// to false, so a minimal Config processes both headers. Use [Config.SkipPaths]
+// or [Config.Skip] to bypass the middleware. RFC 7239 (Forwarded) is not
+// implemented.
//
-// # Skip
+// # Documentation
//
-// Use [Config].SkipPaths for exact path matches or [Config].Skip for
-// dynamic bypass logic.
+// Full guides and examples: https://goceleris.dev/docs/middleware-security
package proxy
diff --git a/middleware/ratelimit/doc.go b/middleware/ratelimit/doc.go
index 6e51753f..46e91cd1 100644
--- a/middleware/ratelimit/doc.go
+++ b/middleware/ratelimit/doc.go
@@ -1,78 +1,36 @@
-// Package ratelimit provides token-bucket rate limiting middleware for
-// celeris.
-//
-// The limiter uses a sharded token-bucket algorithm keyed by client IP
-// (by default). Each key gets an independent bucket that refills at the
-// configured rate up to the burst capacity. A background goroutine
-// periodically evicts expired buckets.
-//
-// Basic usage with defaults (10 RPS, burst 20):
-//
-// server.Use(ratelimit.New())
-//
-// Human-readable rate string (100 per minute, burst 200):
-//
-// server.Use(ratelimit.New(ratelimit.Config{
-// Rate: "100-M",
-// Burst: 200,
-// }))
-//
-// Custom limits keyed by API key:
-//
-// server.Use(ratelimit.New(ratelimit.Config{
-// RPS: 100,
-// Burst: 200,
-// KeyFunc: func(c *celeris.Context) string {
-// return c.Header("x-api-key")
-// },
-// }))
-//
-// The middleware sets X-RateLimit-Limit, X-RateLimit-Remaining, and
-// X-RateLimit-Reset headers on every allowed response, and Retry-After
-// on denied responses (429 Too Many Requests). Set DisableHeaders to
-// suppress all rate limit headers.
-//
-// [Config].SlidingWindow enables a sliding window counter algorithm for
-// smoother rate limiting near window boundaries.
-//
-// [Config].RateFunc allows per-request rate selection, with distinct
-// limiters cached up to [Config].MaxDynamicLimiters (default 1024).
-//
-// [ParseRate] parses rate strings in "-" format (S, M, H, D)
-// into RPS and burst values, returning an error for malformed input.
-//
-// [ErrTooManyRequests] is the exported sentinel error (429) returned on
-// deny, usable with errors.Is for error handling in upstream middleware.
-//
-// Set [Config].Skip to bypass the middleware dynamically, or
-// [Config].SkipPaths for exact-match path exclusions.
-//
-// [Config].Store allows plugging in an external storage backend (e.g., Redis)
-// that implements the [Store] interface. The store handles its own rate logic.
-//
-// [Config].SkipFailedRequests and [Config].SkipSuccessfulRequests refund
-// tokens for requests that fail (status >= 400) or succeed (status < 400),
-// respectively. Store backends must implement [StoreUndo] for refunds.
-//
-// [Config].CleanupContext controls the lifetime of background goroutines.
-// When the context is cancelled, cleanup goroutines stop. If nil, they run
-// until the process exits.
-//
-// Note: [New] spawns one or more background goroutines for bucket eviction.
-// Set CleanupContext to a cancellable context to ensure they are stopped
-// when the middleware is no longer needed.
-//
-// # Reverse Proxy Integration
-//
-// The default KeyFunc uses c.ClientIP(). When behind a reverse proxy,
-// install the proxy middleware via Server.Pre() so rate limits apply to
-// real client IPs, not the proxy's IP:
-//
-// server.Pre(proxy.New(proxy.Config{
-// TrustedProxies: []string{"10.0.0.0/8"},
-// }))
-// server.Use(ratelimit.New()) // rate-limits by real client IP
-//
-// Without proxy middleware, all clients behind the same proxy share a
-// single rate-limit bucket.
+// Package ratelimit provides token-bucket and sliding-window rate limiting
+// middleware for Celeris.
+//
+// [New] returns a [celeris.HandlerFunc] that enforces per-key request limits
+// using a sharded token-bucket algorithm by default. Keys default to the client
+// IP via [celeris.Context.ClientIP]; supply [Config.KeyFunc] to key on anything
+// else (API key, user ID, etc.).
+//
+// Core configuration fields: [Config.RPS] and [Config.Burst] set the static
+// rate; [Config.Rate] accepts human-readable strings ("100-M", "1000-H") parsed
+// by [ParseRate]. [Config.RateFunc] selects a per-request rate string, with
+// distinct limiters cached up to [Config.MaxDynamicLimiters] (default 1024).
+// [Config.SlidingWindow] switches to a sliding-window counter for smoother
+// limiting near window boundaries. [Config.Store] plugs in an external backend
+// (e.g. Redis) that implements [Store]; [StoreUndo] is the optional extension
+// for token refunds used by [Config.SkipFailedRequests] and
+// [Config.SkipSuccessfulRequests].
+//
+// On every allowed response the middleware sets X-RateLimit-Limit,
+// X-RateLimit-Remaining, and X-RateLimit-Reset headers; denied responses
+// (429) also receive Retry-After. Set [Config.DisableHeaders] to suppress all
+// rate-limit headers. [Config.ErrorHandler] customises the 429 response (it
+// supersedes the deprecated [Config.LimitReached]). The sentinel error
+// [ErrTooManyRequests] is passed to ErrorHandler and is usable with errors.Is.
+// [ErrDynamicLimitersExhausted] is returned when MaxDynamicLimiters is full.
+//
+// [ValidateConfig] checks a [Config] for errors without panicking, useful when
+// loading configuration from files or untrusted sources before calling [New].
+// [New] spawns background goroutines for bucket eviction; set
+// [Config.CleanupContext] to a cancellable context to stop them when the
+// middleware is no longer needed.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-traffic
package ratelimit
diff --git a/middleware/recovery/doc.go b/middleware/recovery/doc.go
index 1ddf65dd..c3282efa 100644
--- a/middleware/recovery/doc.go
+++ b/middleware/recovery/doc.go
@@ -1,55 +1,35 @@
// Package recovery provides panic recovery middleware for celeris.
//
-// The middleware catches panics from downstream handlers, logs the
-// stack trace via slog, and returns a configurable error response
-// instead of crashing the server.
+// The middleware catches panics from downstream handlers, logs the stack trace
+// via slog, and returns a configurable error response instead of crashing the
+// server. Register it as early as possible in the middleware chain so it wraps
+// all other handlers.
//
// Basic usage with defaults (4 KB stack trace, JSON 500 response):
//
// server.Use(recovery.New())
//
-// Custom error handler and stack size:
-//
-// server.Use(recovery.New(recovery.Config{
-// StackSize: 8192,
-// ErrorHandler: func(c *celeris.Context, err any) error {
-// return c.String(500, "Something went wrong")
-// },
-// }))
-//
-// Set [Config].StackSize to 0 to disable stack trace capture (the panic
-// value, method, and path are still logged). Set [Config].DisableLogStack
-// to true to suppress panic logging entirely.
-//
-// # Special Panic Types
-//
-// Panics with [http.ErrAbortHandler] are re-panicked rather than
-// recovered, preserving the standard library's abort semantics.
-//
-// Broken pipe and ECONNRESET errors are detected automatically and
-// logged at WARN level without a stack trace. Use
-// [Config].BrokenPipeHandler to customize the response for these cases.
-//
-// # Nested Recovery
-//
-// If [Config].ErrorHandler itself panics, the middleware falls back to
-// a default 500 Internal Server Error response.
-//
-// # Log Level
-//
-// Set [Config].LogLevel to control the slog level for normal panic log
-// entries (default: [slog.LevelError]). Broken pipe panics are always
-// logged at [slog.LevelWarn] regardless of this setting.
-//
-// # Sentinel Errors
-//
-// The package exports sentinel errors for use with errors.Is:
-// - [ErrPanic]: generic panic recovery
-// - [ErrBrokenPipe]: broken pipe or ECONNRESET
-// - [ErrPanicContextCancelled]: panic after the request context was cancelled
-// - [ErrPanicResponseCommitted]: panic after the response has been committed
-//
-// # Middleware Order
-//
-// Register after logger/metrics so they see the 500 status from recovered panics.
+// [New] accepts an optional [Config] to customise behaviour. Key fields:
+// - [Config].ErrorHandlerErr — preferred error handler (receives a typed error).
+// - [Config].ErrorHandler — legacy handler (receives any); kept for compatibility.
+// - [Config].BrokenPipeHandler — custom handler for broken pipe / ECONNRESET panics.
+// - [Config].StackSize — max bytes for stack trace capture (default 4096; 0 disables).
+// - [Config].DisableLogStack — suppress all panic log output when true.
+// - [Config].LogLevel — slog level for normal panic entries (default [slog.LevelError]).
+// - [Config].Logger — slog logger (default [slog.Default]).
+// - [Config].Skip / [Config].SkipPaths — skip recovery for selected requests.
+//
+// Panics with [http.ErrAbortHandler] are re-panicked to preserve standard
+// library abort semantics. Broken pipe and ECONNRESET panics are logged at
+// WARN level without a stack trace.
+//
+// The package exports sentinel errors for use with [errors.Is]:
+// - [ErrPanic]: generic panic recovery.
+// - [ErrBrokenPipe]: broken pipe or ECONNRESET.
+// - [ErrPanicContextCancelled]: panic after request context was cancelled.
+// - [ErrPanicResponseCommitted]: panic after response has been committed.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/error-handling
package recovery
diff --git a/middleware/redirect/doc.go b/middleware/redirect/doc.go
index 8000a523..55314db0 100644
--- a/middleware/redirect/doc.go
+++ b/middleware/redirect/doc.go
@@ -1,113 +1,25 @@
// Package redirect provides HTTP redirect middleware for common URL
-// normalization patterns in celeris.
+// normalization patterns in Celeris.
//
-// Nine constructor functions cover redirect and rewrite scenarios:
+// Nine constructor functions return a [celeris.HandlerFunc] and accept an
+// optional [Config] to override the redirect status code or skip requests:
//
-// # Redirect Functions
+// - [HTTPSRedirect] — redirects HTTP to HTTPS
+// - [WWWRedirect] — redirects non-www to www
+// - [NonWWWRedirect] — redirects www to non-www
+// - [TrailingSlashRedirect] — adds a trailing slash when missing
+// - [RemoveTrailingSlashRedirect] — strips a trailing slash
+// - [HTTPSWWWRedirect] — redirects to HTTPS + www in one hop
+// - [HTTPSNonWWWRedirect] — redirects to HTTPS + non-www in one hop
+// - [TrailingSlashRewrite] — adds a trailing slash in-place (no redirect)
+// - [RemoveTrailingSlashRewrite] — strips a trailing slash in-place (no redirect)
//
-// - [HTTPSRedirect] — redirects HTTP traffic to HTTPS
-// - [WWWRedirect] — redirects non-www to www subdomain
-// - [NonWWWRedirect] — redirects www to non-www host
-// - [TrailingSlashRedirect] — adds trailing slash when missing
-// - [RemoveTrailingSlashRedirect] — strips trailing slash
+// All constructors default to HTTP 301. Set [Config].Code to 308 to
+// preserve the original request method across the redirect. Query strings
+// are always preserved. Register with [celeris.Server.Pre] so normalization
+// runs before route lookup.
//
-// # Combined Redirect Functions
+// # Documentation
//
-// - [HTTPSWWWRedirect] — redirects to HTTPS + www in a single redirect
-// - [HTTPSNonWWWRedirect] — redirects to HTTPS + non-www in a single redirect
-//
-// # Rewrite Functions
-//
-// - [TrailingSlashRewrite] — adds trailing slash in-place (no redirect)
-// - [RemoveTrailingSlashRewrite] — strips trailing slash in-place (no redirect)
-//
-// Each constructor accepts an optional [Config] to override the redirect
-// status code, skip specific paths, or provide a dynamic skip function.
-// The rewrite functions accept [Config] for skip logic but ignore the Code
-// field since they do not send a redirect response.
-//
-// # Redirect Code (301 vs 308)
-//
-// The default redirect code is 301 (Moved Permanently). This is appropriate
-// for GET requests but can cause problems for POST/PUT/DELETE: browsers and
-// HTTP clients are allowed to change the request method to GET when following
-// a 301 redirect (RFC 7231 §6.4.2). Set [Config].Code to 308 (Permanent
-// Redirect) to guarantee the client preserves the original request method
-// (RFC 7538 §3). Use 307 for the temporary equivalent that also preserves
-// the method.
-//
-// # Query String Preservation
-//
-// All redirect functions preserve the original query string. The query is
-// appended to the redirect URL when non-empty.
-//
-// # Short-Circuit Behavior
-//
-// When a redirect occurs, the middleware returns immediately via
-// [celeris.Context.Redirect] without calling c.Next(). Downstream
-// handlers are not executed.
-//
-// # Empty Host
-//
-// If [celeris.Context.Host] returns an empty string (malformed request),
-// all redirect and rewrite functions pass through to c.Next() without
-// modification. This prevents generating malformed redirect URLs.
-//
-// # Root Path Handling
-//
-// The trailing slash variants treat the root path "/" as a no-op. "/" is
-// never modified by [TrailingSlashRedirect] (already has a slash) or
-// [RemoveTrailingSlashRedirect] (root must keep its slash).
-//
-// # Pre-Routing Middleware
-//
-// This middleware is designed to run via [celeris.Server.Pre] so that URL
-// normalization occurs before route lookup:
-//
-// server.Pre(redirect.HTTPSRedirect())
-// server.Pre(redirect.TrailingSlashRedirect())
-//
-// # Redirect Loops
-//
-// Be careful not to combine conflicting redirect middleware. For example,
-// using both [TrailingSlashRedirect] and [RemoveTrailingSlashRedirect]
-// creates an infinite redirect loop. Similarly, using both [WWWRedirect]
-// and [NonWWWRedirect] causes a loop. The combined constructors
-// [HTTPSWWWRedirect] and [HTTPSNonWWWRedirect] avoid the double-redirect
-// that would occur from chaining [HTTPSRedirect] with [WWWRedirect] or
-// [NonWWWRedirect].
-//
-// # Reverse Proxy Interaction
-//
-// When running behind a TLS-terminating reverse proxy, install the proxy
-// middleware via Server.Pre() BEFORE redirect middleware. Without it,
-// Scheme() returns "http" (the backend transport), causing an infinite
-// redirect loop with HTTPSRedirect.
-//
-// # Choosing a Constructor
-//
-// The package provides 9 constructors organized into three categories:
-//
-// Redirect (sends 3xx response):
-//
-// HTTPSRedirect, WWWRedirect, NonWWWRedirect,
-// TrailingSlashRedirect, RemoveTrailingSlashRedirect
-//
-// Combined redirect (avoids double-redirect):
-//
-// HTTPSWWWRedirect, HTTPSNonWWWRedirect
-//
-// Rewrite (modifies path in-place, no redirect):
-//
-// TrailingSlashRewrite, RemoveTrailingSlashRewrite
-//
-// Use redirect when clients should update their URLs (SEO, bookmarks).
-// Use rewrite when normalization is internal (the client URL stays the same).
-// Use combined constructors when both scheme and host need changing.
-//
-// # Skipping
-//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// exact-match path exclusions. Skipped requests call c.Next() without
-// any redirect.
+// Full guides and examples: https://goceleris.dev/docs/middleware-routing-helpers
package redirect
diff --git a/middleware/requestid/doc.go b/middleware/requestid/doc.go
index f81fe435..cf32b8b5 100644
--- a/middleware/requestid/doc.go
+++ b/middleware/requestid/doc.go
@@ -1,64 +1,22 @@
// Package requestid provides request ID middleware for celeris.
//
-// The middleware reads the request ID from the incoming X-Request-Id
-// header (configurable). If the incoming ID is absent or invalid, a
-// new UUID v4 is generated using a buffered random source that batches
-// 256 UUIDs per crypto/rand syscall. The ID is set on both the response
-// header and the context store under [ContextKey] ("request_id").
+// [New] reads the request ID from the incoming header (default: "x-request-id").
+// If absent or invalid (non-printable ASCII, >128 bytes), a UUID v4 is generated
+// using a buffered random source that batches 256 UUIDs per crypto/rand syscall.
+// The ID is written to the response header and stored in the context under
+// [ContextKey] ("request_id").
//
-// Basic usage:
+// Key exported symbols:
+// - [New] — constructor; accepts an optional [Config].
+// - [Config] — Header, Generator, DisableTrustProxy, EnableStdContext, Skip, SkipPaths.
+// - [FromContext] — retrieve the ID from a [celeris.Context] in downstream handlers.
+// - [FromStdContext] — retrieve the ID from a stdlib [context.Context] (requires Config.EnableStdContext).
+// - [CounterGenerator] — monotonic "{prefix}-{N}" generator; zero syscalls after init.
+// - [ContextKey] — the context-store key ("request_id").
//
-// server.Use(requestid.New())
+// Register before logger so every log line carries the request ID.
//
-// Custom header and deterministic generator for testing:
+// # Documentation
//
-// server.Use(requestid.New(requestid.Config{
-// Header: "x-trace-id",
-// Generator: requestid.CounterGenerator("req"),
-// }))
-//
-// # Input Validation
-//
-// Incoming IDs are validated: only printable ASCII (0x20-0x7E) is
-// accepted, with a maximum length of 128 characters. Invalid or
-// oversized IDs are silently replaced with a fresh UUID.
-//
-// # DisableTrustProxy
-//
-// [Config].DisableTrustProxy controls whether the inbound request header
-// is accepted. When false (default), a valid inbound header is propagated
-// as-is. When true, the inbound header is always ignored and a fresh ID
-// is generated:
-//
-// server.Use(requestid.New(requestid.Config{
-// DisableTrustProxy: true,
-// }))
-//
-// # Retrieving the Request ID
-//
-// Use [FromContext] to retrieve the request ID from downstream handlers:
-//
-// id := requestid.FromContext(c)
-//
-// Use [FromStdContext] to retrieve it from a stdlib [context.Context]:
-//
-// id := requestid.FromStdContext(ctx)
-//
-// # Skipping
-//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// path exclusions. SkipPaths uses exact path matching.
-//
-// # Custom Generator Validation
-//
-// Custom generator output is validated with the same rules as inbound
-// headers. If the generator returns an invalid or empty string, it is
-// retried up to 3 times before falling back to the built-in UUID.
-//
-// [CounterGenerator] returns a monotonic ID generator ("{prefix}-{N}")
-// with zero syscalls after initialization.
-//
-// # Middleware Order
-//
-// Register first -- all downstream middleware can access the request ID.
+// Full guides and examples: https://goceleris.dev/docs/observability#request-ids
package requestid
diff --git a/middleware/rewrite/doc.go b/middleware/rewrite/doc.go
index 25054c23..0a100fa1 100644
--- a/middleware/rewrite/doc.go
+++ b/middleware/rewrite/doc.go
@@ -1,122 +1,35 @@
-// Package rewrite provides URL rewrite middleware for celeris using
-// regular expression pattern matching.
-//
-// The middleware matches the request path against a set of regex rules
-// and either rewrites the path in-place (silent rewrite) or sends an
-// HTTP redirect response.
-//
-// Basic usage (silent rewrite -- use anchored patterns):
-//
-// server.Pre(rewrite.New(rewrite.Config{
-// Rules: []rewrite.Rule{
-// {Pattern: "^/old$", Replacement: "/new"},
-// },
-// }))
-//
-// Redirect mode:
-//
-// server.Pre(rewrite.New(rewrite.Config{
-// Rules: []rewrite.Rule{
-// {Pattern: "^/old$", Replacement: "/new"},
-// },
-// RedirectCode: 301,
-// }))
-//
-// # Regex and Capture Groups
-//
-// Rule patterns are Go regular expressions compiled with [regexp.MustCompile].
-// The replacement string supports capture group substitution ($1, $2, ...)
-// using [regexp.Regexp.ReplaceAllString] semantics:
-//
-// server.Pre(rewrite.New(rewrite.Config{
-// Rules: []rewrite.Rule{
-// {Pattern: `/users/(\d+)/posts`, Replacement: "/api/v2/users/$1/posts"},
-// },
-// }))
-//
-// # First-Match-Wins
-//
-// Rules are evaluated in the order provided. The first matching regex wins
-// and subsequent rules are not checked:
-//
-// Rules: []rewrite.Rule{
-// {Pattern: "/a/.*", Replacement: "/alpha"}, // checked first
-// {Pattern: "/b/.*", Replacement: "/beta"}, // checked second
-// }
-//
-// # Silent Rewrite vs Redirect
-//
-// When RedirectCode is 0 (default), the middleware calls [celeris.Context.SetPath]
-// to modify the request path in-place. The client URL remains unchanged and
-// downstream handlers see the rewritten path.
-//
-// When RedirectCode is a valid redirect status (301, 302, 303, 307, 308),
-// the middleware sends an HTTP redirect response. The redirect URL preserves
-// the original scheme, host, and query string. Downstream handlers are not
-// executed.
-//
-// # Pre-Routing Middleware
-//
-// This middleware is designed to run via [celeris.Server.Pre] so that URL
-// rewriting occurs before route lookup:
+// Package rewrite provides pre-routing URL rewrite middleware for celeris,
+// matching the request path against ordered regular-expression rules.
+//
+// Construct the middleware with [New] and a [Config], then register it via
+// [celeris.Server.Pre] so rewriting happens before route lookup. Each [Rule]
+// has a Pattern (a Go regexp) and a Replacement that supports capture-group
+// substitution ($1, $2, ...). Rules are evaluated in order and the first
+// match wins.
+//
+// A rule rewrites silently or redirects depending on RedirectCode. When zero
+// (the default), the path is rewritten in place via [celeris.Context.SetPath]
+// and downstream handlers see the new path. When set to a redirect status
+// (301, 302, 303, 307, 308), the middleware sends an HTTP redirect preserving
+// the scheme, host, and query string. RedirectCode may be set on [Config] as
+// the default or overridden per [Rule]. A rule can be scoped with Methods and
+// Host; both default to matching everything. Use [Config].Skip or
+// [Config].SkipPaths to bypass requests.
+//
+// [New] compiles patterns once via [regexp.MustCompile] and panics on an empty
+// Rules slice, an invalid pattern, or an invalid non-zero RedirectCode.
//
// server.Pre(rewrite.New(rewrite.Config{
// Rules: []rewrite.Rule{
-// {Pattern: "/old", Replacement: "/new"},
+// {Pattern: `^/api/v1/(.*)$`, Replacement: "/api/v2/$1"},
// },
// }))
//
-// # Query String Preservation
-//
-// In redirect mode, the original query string is appended to the redirect
-// URL. In silent rewrite mode, the query string is unmodified since only
-// the path is rewritten.
-//
-// # Conditional Rewriting
-//
-// Rules can be restricted to specific HTTP methods or hosts:
-//
-// server.Pre(rewrite.New(rewrite.Config{
-// Rules: []rewrite.Rule{
-// {
-// Pattern: "^/api/v1/(.*)$",
-// Replacement: "/api/v2/$1",
-// Methods: []string{"GET", "HEAD"},
-// },
-// {
-// Pattern: "^/admin/(.*)$",
-// Replacement: "/internal/$1",
-// Host: "admin.example.com",
-// },
-// },
-// }))
-//
-// When Methods is empty, the rule matches all methods. When Host is
-// empty, the rule matches all hosts.
-//
-// # Init-Time Validation
-//
-// [New] panics if Rules is empty, if RedirectCode is non-zero and not a
-// valid redirect status, or if any rule pattern is an invalid regex (via
-// [regexp.MustCompile]).
-//
-// # Security
-//
-// Regex patterns are compiled once at init time via [regexp.MustCompile],
-// not per-request. Avoid catastrophic backtracking patterns (e.g.,
-// `(a+)+$`) which can cause high CPU during compilation.
-//
-// In redirect mode, the redirect URL is constructed from [celeris.Context.Host]
-// and [celeris.Context.Scheme], which are derived from request headers.
-// Without a reverse proxy that validates the Host header, an attacker
-// can send a crafted Host to produce an open redirect. Ensure your
-// deployment validates the Host header upstream (e.g., via the proxy
-// middleware) or use silent rewrite mode (RedirectCode: 0) which only
-// modifies the internal path.
+// In redirect mode the redirect URL is built from the request's Host and
+// Scheme headers; validate the Host header upstream (or use silent rewrite)
+// to avoid open redirects.
//
-// # Skipping
+// # Documentation
//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// exact-match path exclusions. Skipped requests call c.Next() without
-// any rewrite.
+// Full guides and examples: https://goceleris.dev/docs/middleware-routing-helpers
package rewrite
diff --git a/middleware/secure/doc.go b/middleware/secure/doc.go
index 7c7025a4..6df1c704 100644
--- a/middleware/secure/doc.go
+++ b/middleware/secure/doc.go
@@ -1,9 +1,8 @@
// Package secure provides OWASP security headers middleware for celeris.
//
-// The middleware sets a comprehensive suite of HTTP security headers on
-// every response. All non-empty header values are pre-computed into a flat
-// slice at initialization. The hot path iterates this slice with zero
-// allocations.
+// The middleware sets a comprehensive suite of HTTP security headers on every
+// response. All non-empty header values are pre-computed into a flat slice at
+// initialization; the hot path iterates this slice with zero allocations.
//
// Default headers set on every response:
//
@@ -18,56 +17,16 @@
// - X-Permitted-Cross-Domain-Policies: "none"
// - Origin-Agent-Cluster: "?1"
//
-// Cross-Origin-Embedder-Policy ("require-corp") and X-Download-Options
-// ("noopen") are OPT-IN (off by default, #338): COEP-by-default breaks
-// cross-origin resource loads (matching helmet, which leaves it off), and
-// X-Download-Options only affected legacy Internet Explorer. Content-Security-
-// Policy and Permissions-Policy are likewise only emitted when their [Config]
-// fields are non-empty.
+// Cross-Origin-Embedder-Policy and X-Download-Options are opt-in (off by
+// default). Content-Security-Policy and Permissions-Policy are only emitted
+// when their [Config] fields are non-empty.
//
-// # Usage
+// Use [New] to create the middleware. Pass a [Config] to override defaults.
+// Set any string field to [Suppress] ("-") to omit that individual header.
+// Use [Config].DisableHSTS to omit Strict-Transport-Security entirely.
+// Use [Config].Skip or [Config].SkipPaths to bypass the middleware per request.
//
-// Default configuration (all OWASP-recommended headers):
+// # Documentation
//
-// server.Use(secure.New())
-//
-// Custom configuration:
-//
-// server.Use(secure.New(secure.Config{
-// ContentSecurityPolicy: "default-src 'self'",
-// HSTSMaxAge: 31536000,
-// HSTSPreload: true,
-// }))
-//
-// # Suppressing Individual Headers
-//
-// Set any string field to [Suppress] ("-") to omit that header:
-//
-// server.Use(secure.New(secure.Config{
-// XFrameOptions: secure.Suppress,
-// }))
-//
-// For HSTS, set [Config].DisableHSTS to true to omit the header entirely.
-// HSTSMaxAge defaults to 2 years (63072000 seconds) whether or not other
-// fields are customized — there is no zero-value trap.
-//
-// # HSTS Preload Validation
-//
-// When [Config].HSTSPreload is true, validate() panics at initialization
-// if HSTSMaxAge < 31536000 or HSTSExcludeSubdomains is true, enforcing
-// HSTS preload list requirements.
-//
-// # Skipping
-//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// exact-match path exclusions. Skipped requests receive no security
-// headers at all.
-//
-// # CORS Interaction
-//
-// When using CORS middleware alongside secure, note that the default
-// CrossOriginResourcePolicy (same-origin) blocks cross-origin resource
-// loading; APIs serving cross-origin requests should set
-// CrossOriginResourcePolicy: "cross-origin". COEP is off by default so it no
-// longer needs suppressing; enable it explicitly only when isolating the page.
+// Full guides and examples: https://goceleris.dev/docs/middleware-security
package secure
diff --git a/middleware/session/doc.go b/middleware/session/doc.go
index 0796a5b8..ba3e735e 100644
--- a/middleware/session/doc.go
+++ b/middleware/session/doc.go
@@ -2,78 +2,34 @@
// celeris.
//
// Sessions are identified by a cookie (default), header, or query parameter,
-// and data is stored server-side in a pluggable [Store]. The default
-// [MemoryStore] uses sharded maps with a background cleanup goroutine,
+// and data is stored server-side in a pluggable [Store]. The built-in
+// [NewMemoryStore] uses sharded maps with a background cleanup goroutine,
// suitable for single-instance deployments.
//
-// Basic usage with defaults (in-memory store, 24h sessions):
+// Attach the middleware with defaults (in-memory store, 24 h sessions):
//
// server.Use(session.New())
//
-// Custom cookie name and timeouts:
-//
-// server.Use(session.New(session.Config{
-// CookieName: "myapp_sid",
-// IdleTimeout: 15 * time.Minute,
-// AbsoluteTimeout: 2 * time.Hour,
-// }))
-//
-// # Accessing the Session
-//
-// Use [FromContext] to retrieve the session from downstream handlers:
+// Retrieve and mutate the session in downstream handlers via [FromContext]:
//
// s := session.FromContext(c)
// s.Set("user", "admin")
// name, ok := s.Get("user")
//
-// Modified sessions are automatically saved after the handler chain returns.
-// Call [Session.Destroy] to invalidate a session, or [Session.Regenerate] to
-// issue a new session ID.
-//
-// # Session Fixation Prevention
-//
-// Applications MUST call [Session.Regenerate] (or [Session.Reset]) after
-// any authentication state change to prevent session fixation attacks:
-//
-// func loginHandler(c *celeris.Context) error {
-// // ... validate credentials ...
-// s := session.FromContext(c)
-// if err := s.Regenerate(); err != nil {
-// return err
-// }
-// s.Set("user", authenticatedUser)
-// return nil
-// }
-//
-// # Pluggable Extractors
+// Modified sessions are saved automatically after the handler chain returns.
+// Call [Session.Destroy] to invalidate a session, [Session.Regenerate] to
+// issue a new session ID (required after any authentication state change to
+// prevent session fixation), or [Session.SetIdleTimeout] to override the
+// per-session idle window ("remember me" flows).
//
// [CookieExtractor], [HeaderExtractor], [QueryExtractor], and
-// [ChainExtractor] control where the session ID is read from. When a
-// non-cookie extractor is used, the session ID is returned in a response
-// header named after [Config].CookieName so API clients can capture it.
-//
-// # Out-of-Band Access
-//
-// [NewHandler] returns a [Handler] providing both the middleware and
-// out-of-band session access via [Handler.GetByID] for admin tools or
-// background jobs.
-//
-// # Timeout Semantics
-//
-// IdleTimeout (default 30m) controls server-side expiry per session.
-// AbsoluteTimeout (default 24h) caps total session lifetime. Set
-// AbsoluteTimeout to -1 to disable it. [Session.SetIdleTimeout]
-// overrides idle timeout for individual sessions ("remember me" flows).
-//
-// # Custom Stores
-//
-// Implement the [Store] interface to back sessions with any storage
-// backend. All [Store] methods receive a [context.Context] propagated
-// from the request for cancellation and deadline support.
+// [ChainExtractor] control where the session ID is read from. For
+// out-of-band access (admin tools, background jobs) use [NewHandler], which
+// exposes the middleware via [Handler.Middleware] and direct lookup via
+// [Handler.GetByID]. Implement the [Store] interface to back sessions with
+// any storage backend.
//
-// # Security
+// # Documentation
//
-// CookieSecure defaults to false for development convenience. Production
-// deployments MUST set CookieSecure: true to prevent cookie transmission
-// over unencrypted connections.
+// Full guides and examples: https://goceleris.dev/docs/data-stores
package session
diff --git a/middleware/singleflight/doc.go b/middleware/singleflight/doc.go
index 085deaeb..2e815e79 100644
--- a/middleware/singleflight/doc.go
+++ b/middleware/singleflight/doc.go
@@ -1,139 +1,26 @@
-// Package singleflight provides request deduplication middleware for celeris.
+// Package singleflight provides request-coalescing middleware for celeris.
//
-// When multiple identical requests arrive concurrently, only the first
-// (the "leader") executes the handler chain. Subsequent requests (the
-// "waiters") block until the leader completes, then receive a copy of
-// the leader's response. This prevents the thundering herd problem where
-// a popular endpoint receives a burst of identical requests that all hit
-// the backend simultaneously.
+// When several identical requests arrive concurrently, only the first (the
+// "leader") executes the handler chain; the rest (the "waiters") block until
+// the leader finishes and then receive a copy of its response. This absorbs
+// thundering-herd bursts on hot endpoints so they hit the backend once.
//
-// Basic usage:
+// Install it with [New], optionally passing a [Config]. The zero-value
+// configuration deduplicates on method + path + sorted query string +
+// Authorization + Cookie, so requests from different authenticated users are
+// never coalesced. Use [Config.KeyFunc] to change the key, and [Config.Skip]
+// or [Config.SkipPaths] to exclude requests (for example non-idempotent
+// methods or large-response endpoints). Waiter responses carry an
+// "x-singleflight: HIT" header.
//
// server.Use(singleflight.New())
//
-// Custom key function:
+// Singleflight buffers the leader's response, so install it after timeout
+// middleware and before response transforms such as compress or etag. It is
+// intended for idempotent reads; a custom KeyFunc that returns user-specific
+// data must incorporate user identity to avoid cross-user leakage.
//
-// server.Use(singleflight.New(singleflight.Config{
-// KeyFunc: func(c *celeris.Context) string {
-// return c.Path() // ignore query parameters
-// },
-// }))
+// # Documentation
//
-// # Algorithm
-//
-// The middleware maintains an in-memory map of in-flight keys. For each
-// incoming request:
-//
-// 1. Compute the deduplication key via [Config].KeyFunc.
-// 2. Lock the group and check the map.
-// 3. If the key exists, the request is a waiter: unlock, wait for the
-// leader to finish, then replay the captured response.
-// 4. If the key is absent, the request is the leader: register the key,
-// unlock, buffer the response, execute c.Next(), capture the result,
-// remove the key from the map, and wake all waiters.
-//
-// The embedded singleflight group uses no external dependencies.
-//
-// # Default Key
-//
-// The default key function produces: method + "\x00" + path + "\x00" +
-// sorted query string + "\x00" + Authorization header + "\x00" + Cookie
-// header. Query parameters are sorted via [url.Values.Encode] so that
-// ?a=1&b=2 and ?b=2&a=1 produce the same key. Multi-value query
-// parameters are also sorted within each key so that ?a=2&a=1 and
-// ?a=1&a=2 produce the same key. When the request has no query string,
-// the query component is omitted entirely (no parsing overhead).
-// Authorization and Cookie components are omitted when absent.
-//
-// # x-singleflight Response Header
-//
-// Waiter responses include the header "x-singleflight: HIT" so that
-// callers (and observability tools) can distinguish coalesced responses
-// from leader responses. The leader response does not carry this header.
-//
-// # Middleware Ordering
-//
-// Singleflight should run AFTER timeout middleware (so each coalesced
-// request respects its own timeout) and BEFORE transform middleware
-// like compress or etag (so the response is captured before transformation):
-//
-// server.Use(timeout.New(...)) // outermost
-// server.Use(singleflight.New()) // dedup on uncompressed response
-// server.Use(compress.New()) // innermost
-// server.Use(etag.New())
-//
-// # Idempotency
-//
-// Singleflight is designed for idempotent read endpoints (GET, HEAD).
-// Using it on non-idempotent methods (POST, PUT, DELETE) may cause
-// unintended behavior: only one request executes and all waiters receive
-// the same response. For most applications, skip non-idempotent methods:
-//
-// singleflight.New(singleflight.Config{
-// Skip: func(c *celeris.Context) bool {
-// m := c.Method()
-// return m != "GET" && m != "HEAD"
-// },
-// })
-//
-// If your endpoint modifies state, either skip it with [Config].SkipPaths
-// or use a [Config].KeyFunc that differentiates by request body or session.
-//
-// # Error Propagation
-//
-// If the leader's handler returns an error, all waiters receive the same
-// error. The error is propagated as-is (including [celeris.HTTPError]
-// with its status code).
-//
-// # Panic Propagation
-//
-// If the leader's handler panics, the panic value is captured and
-// re-panicked in every waiter goroutine (and in the leader after
-// cleanup). This ensures recovery middleware further up the chain
-// catches the panic in every request context.
-//
-// # Non-2xx Responses
-//
-// Non-2xx responses (404, 500, etc.) are coalesced just like 2xx
-// responses. The middleware does not distinguish between success and
-// failure status codes — it deduplicates all in-flight requests for
-// the same key.
-//
-// # Security: Authenticated Endpoints
-//
-// The default key function includes Authorization and Cookie request
-// headers, so requests from different authenticated users produce different
-// keys and are NOT coalesced. This prevents cross-user data leakage.
-//
-// If you provide a custom KeyFunc, ensure it incorporates user identity
-// for any endpoint that returns user-specific data. Failing to do so
-// will cause one user's response (including Set-Cookie headers and
-// personalized content) to be replayed to other concurrent users.
-//
-// # Waiter Timeout
-//
-// Waiters block unconditionally until the leader completes. There is no
-// independent timeout per waiter — a waiter whose context deadline expires
-// will still wait for the leader to finish. To bound waiter wait time,
-// place timeout middleware OUTSIDE singleflight (the recommended ordering):
-//
-// server.Use(timeout.New(...)) // bounds total request time
-// server.Use(singleflight.New()) // waiter wait is bounded by timeout
-//
-// This is the same limitation as [golang.org/x/sync/singleflight].
-//
-// # Memory
-//
-// The leader's response body is deep-copied for each waiter. For large
-// responses with many concurrent waiters, this multiplies memory usage.
-// Use Skip or SkipPaths to exclude large-response endpoints.
-//
-// # Skipping
-//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// path exclusions. SkipPaths uses exact path matching:
-//
-// server.Use(singleflight.New(singleflight.Config{
-// SkipPaths: []string{"/admin", "/webhook"},
-// }))
+// Full guides and examples: https://goceleris.dev/docs/middleware-traffic
package singleflight
diff --git a/middleware/sse/doc.go b/middleware/sse/doc.go
index a14c44cd..7c576571 100644
--- a/middleware/sse/doc.go
+++ b/middleware/sse/doc.go
@@ -1,89 +1,30 @@
// Package sse provides Server-Sent Events (SSE) middleware for celeris.
//
-// SSE enables unidirectional server-to-client streaming over HTTP. This
-// middleware manages event formatting, heartbeat keep-alives, reconnection
-// via Last-Event-ID, and client disconnect detection.
+// SSE enables unidirectional server-to-client streaming over HTTP. Use [New]
+// with a [Config] to attach an SSE handler to any route. The [Config.Handler]
+// field receives a [*Client] per connection; call [Client.Send] or
+// [Client.SendData] to push [Event] values, and watch [Client.Context] for
+// disconnect.
//
-// # Basic Usage
+// For fan-out to many subscribers, use [NewBroker] ([BrokerConfig]): call
+// [Broker.Subscribe] in each handler and [Broker.Publish] from any goroutine.
+// [Broker.Publish] formats each event once via [PreparedEvent] and never
+// blocks on a slow subscriber.
//
-// server.GET("/events", sse.New(sse.Config{
-// Handler: func(client *sse.Client) {
-// ticker := time.NewTicker(time.Second)
-// defer ticker.Stop()
-// for {
-// select {
-// case <-client.Context().Done():
-// return
-// case t := <-ticker.C:
-// if err := client.Send(sse.Event{
-// Event: "time",
-// Data: t.Format(time.RFC3339),
-// }); err != nil {
-// return
-// }
-// }
-// }
-// },
-// }))
+// Backpressure is controlled per-client via [Config.MaxQueueDepth] and
+// [Config.OnSlowClient] (drop/close policy), and per-broker-subscriber via
+// [BrokerConfig.SubscriberBuffer].
//
-// # Reconnection
+// Reconnection replay is available via [Config.ReplayStore]: supply
+// [NewRingBuffer] for in-process replay or [NewKVReplayStore] for
+// distributed replay backed by a KV store. The middleware calls
+// [Client.LastEventID] and replays missed events automatically.
//
-// When a client reconnects, browsers send a Last-Event-ID header. Use
-// [Client.LastEventID] to resume from where the client left off:
+// Heartbeat comments are sent every [DefaultHeartbeatInterval] (15 s) by
+// default; configure with [Config.HeartbeatInterval]. The middleware works
+// with all celeris engines and handles stream lifecycle internally.
//
-// Handler: func(client *sse.Client) {
-// lastID := client.LastEventID()
-// events := fetchEventsSince(lastID)
-// for _, e := range events {
-// client.Send(e)
-// }
-// },
+// # Documentation
//
-// For automatic replay see [Config.ReplayStore] + [NewRingBuffer] /
-// [NewKVReplayStore]: the middleware will Append every Send and replay
-// missed events on the next reconnect with no work from the handler.
-//
-// # Backpressure: two layers, two purposes
-//
-// celeris exposes per-subscriber backpressure at TWO distinct layers:
-//
-// - [Config.MaxQueueDepth] / [Config.OnSlowClient] is the per-Client
-// queue. When set, [Client.Send] enqueues onto a bounded channel
-// that a per-Client drain goroutine writes to the wire. The user's
-// [Config.OnSlowClient] policy fires when that queue overflows.
-// Use this layer when a single subscriber drives the load — e.g.
-// a long-lived per-user feed where Send is the only producer.
-//
-// - [Broker] / [BrokerConfig.SubscriberBuffer] / [OnSlowSubscriber]
-// is the per-subscriber queue INSIDE the broker. [Broker.Publish]
-// fans out via these queues; per-subscriber drain goroutines call
-// [Client.WritePreparedEvent] (which writes synchronously to the
-// wire, bypassing Client.MaxQueueDepth). Use this layer for
-// fan-out from a single publisher to N subscribers — the broker
-// formats each event ONCE and never blocks on a single slow
-// subscriber.
-//
-// The two layers are orthogonal. A Broker.Subscribe'd client can also
-// have a non-zero MaxQueueDepth — the broker uses WritePreparedEvent
-// (which is always synchronous) so the Client.queue lies dormant
-// unless something else calls [Client.Send] directly. In that case
-// the Client.queue absorbs Send-bursts independently of the broker
-// fan-out. There is no scenario where both queues fight each other.
-//
-// # Heartbeat
-//
-// By default, a heartbeat comment is sent every 15 seconds to detect
-// disconnected clients. Configure via [Config.HeartbeatInterval] or
-// disable with a negative value.
-//
-// # Engine Compatibility
-//
-// SSE works with all celeris engines (std, epoll, io_uring). The middleware
-// handles [celeris.Context.Detach] internally — callers do not need to manage
-// the event loop lifecycle.
-//
-// # CORS
-//
-// For cross-origin EventSource connections, configure CORS middleware on
-// the SSE endpoint to allow the appropriate origin.
+// Full guides and examples: https://goceleris.dev/docs/sse
package sse
diff --git a/middleware/static/doc.go b/middleware/static/doc.go
index 90f0c760..f4ba8655 100644
--- a/middleware/static/doc.go
+++ b/middleware/static/doc.go
@@ -1,113 +1,23 @@
// Package static serves static files from the OS filesystem or an [fs.FS].
//
-// Basic usage with an OS directory:
-//
-// server.Use(static.New(static.Config{Root: "./public"}))
-//
-// With an embedded filesystem:
-//
-// //go:embed assets/*
-// var assets embed.FS
-// server.Use(static.New(static.Config{FS: assets}))
-//
-// With a URL prefix:
-//
-// server.Use(static.New(static.Config{
-// Root: "./public",
-// Prefix: "/static",
-// }))
-//
-// # Prefix Matching
-//
-// The Prefix is matched at segment boundaries. A prefix of "/api" will match
-// "/api/file.txt" but NOT "/api-docs/file.txt". This prevents unintended
-// path collisions.
-//
-// # Directory Browsing
-//
-// When Browse is enabled and the request path resolves to a directory without
-// an index file, an HTML directory listing is rendered. Filenames in the
-// listing are URL-encoded in href attributes and HTML-escaped in display
-// text to prevent XSS via crafted filenames.
-//
-// # SPA Mode
-//
-// When [Config].SPA is true the middleware operates in single-page
-// application mode: requests for non-existent files serve the index file
-// (default index.html) instead of falling through to the next handler.
-// This allows client-side routers to handle arbitrary URL paths. Existing
-// files and directories are still served normally.
-//
-// # Caching
-//
-// The middleware sets Last-Modified and ETag headers based on file metadata.
-// Conditional requests (If-Modified-Since, If-None-Match) return 304 Not
-// Modified when the client already has a fresh copy.
-//
-// The ETag is a weak validator computed from the file's modification time
-// and size: W/"-". For embed.FS files where ModTime
-// is zero, caching headers are omitted.
-//
-// # Cache-Control
-//
-// [Config].MaxAge sets the Cache-Control max-age directive. When MaxAge is
-// greater than zero the response includes a "public, max-age=N" header
-// (where N is seconds). When MaxAge is zero (the default) no Cache-Control
-// header is added. MaxAge works alongside ETag and Last-Modified: browsers
-// that have a cached copy within the max-age window skip the network
-// entirely, and once the window expires they can still use conditional
-// requests to validate freshness.
-//
-// # Range Requests
-//
-// OS files support range requests via the core [celeris.Context.FileFromDir]
-// method. For fs.FS files, the middleware implements range request handling
-// inline, supporting single byte ranges (bytes=start-end). When the underlying
-// fs.File implements [io.ReadSeeker], range requests seek to the requested
-// offset and read only the required bytes instead of loading the entire file
-// into memory.
-//
-// # Content-Type Detection
-//
-// Content types are determined by file extension via [mime.TypeByExtension].
-// When the extension is unknown or absent, the middleware sniffs the first 512
-// bytes using [http.DetectContentType] (for fs.FS files) or delegates to the
-// core file-serving method (for OS files).
-//
-// # Pre-Compressed Files
-//
-// When [Config].Compress is true, the middleware checks for pre-compressed
-// variants of the requested file before serving the original. It inspects
-// the Accept-Encoding request header and looks for a .br (Brotli) or .gz
-// (gzip) file alongside the original. Brotli is preferred when both are
-// accepted. The response includes Content-Encoding and Vary headers.
-//
-// Pre-compressed file serving only applies to OS filesystem (Root), not
-// fs.FS. This pairs well with build tools that generate .br and .gz files
-// at deploy time (e.g. Vite, esbuild, or a post-build compression step).
-//
-// # Security
-//
-// OS files are served through [celeris.Context.FileFromDir], which prevents
-// directory traversal and symlink escape. For fs.FS files, paths are cleaned
-// before opening.
-//
-// # Method Filtering
-//
-// Only GET and HEAD requests are processed. All other methods pass through
-// to the next handler.
-//
-// # Ordering
-//
-// Place the static middleware after security and authentication middleware
-// if you need to protect static files. Place it before compress and etag
-// if you want framework-level caching/compression (though the static
-// middleware sets its own ETag from file metadata, which the etag middleware
-// will preserve).
-//
-// # Skipping
-//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// exact-match path exclusions. Skipped requests call c.Next() without
-// serving any files.
+// Use [New] with a [Config] to mount the middleware. Set [Config].Root for an
+// OS directory or [Config].FS for an embedded/virtual filesystem (mutually
+// exclusive; FS takes precedence when both are set). Key options:
+//
+// - [Config].Prefix — URL path prefix, matched at segment boundaries.
+// - [Config].Index — directory index filename (default "index.html").
+// - [Config].Browse — enable HTML directory listings.
+// - [Config].SPA — single-page app mode: unknown paths serve the index file.
+// - [Config].MaxAge — Cache-Control max-age duration (zero = no header).
+// - [Config].Compress — serve pre-compressed .br/.gz variants when accepted.
+// - [Config].Skip / [Config].SkipPaths — skip the middleware dynamically or
+// by exact path match.
+//
+// Only GET and HEAD requests are processed; all others pass through. The
+// middleware sets Last-Modified, ETag (weak, mtime+size), and optional
+// Cache-Control headers and handles conditional requests (304 Not Modified).
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/static-files
package static
diff --git a/middleware/store/doc.go b/middleware/store/doc.go
index b7e16b0a..a4e8875a 100644
--- a/middleware/store/doc.go
+++ b/middleware/store/doc.go
@@ -1,42 +1,44 @@
// Package store defines the unified byte-level key-value interface shared
-// by celeris middleware (session, csrf, ratelimit, jwt, cache, idempotency).
+// by celeris middleware (session, csrf, cache, idempotency, SSE replay).
//
-// # Interfaces
+// # Core interface
//
// [KV] is the minimal required surface: Get, Set with TTL, Delete. All
// implementations must be safe for concurrent use and honor the contract
-// that Get returns (nil, [ErrNotFound]) on a missing key.
-//
-// Optional extension interfaces ([GetAndDeleter], [Scanner], [PrefixDeleter],
-// [SetNXer], [Scripter]) surface backend capabilities. Middleware feature-
-// detect these via type assertion and fall back to emulation or no-op
-// semantics when a backend does not implement them.
-//
-// # Reference backends
-//
-// - [MemoryKV] — in-memory, sharded implementation. Implements every
-// optional extension. Used as the default store and in tests.
-// - middleware/session/redisstore — Redis-backed via driver/redis.
-// Implements KV + Scanner; GetAndDeleter when Redis >= 6.2.
-// - middleware/session/postgresstore — PostgreSQL-backed via driver/postgres.
-// Implements KV only; Reset via TRUNCATE (exposed as io.Closer).
-// - middleware/csrf/redisstore — Redis-backed CSRF storage.
-// Implements KV + GetAndDeleter (GETDEL).
-// - middleware/ratelimit/redisstore — Redis-backed ratelimit store.
-// Implements ratelimit.Store via EVALSHA; not a KV adapter.
-//
-// # Response wire format
-//
-// [EncodedResponse] + [ResponseWireVersion] define a byte-efficient
-// snapshot format used by cache and idempotency middleware to persist
-// captured HTTP responses. The format is versioned to allow forward
-// compatibility; decoders reject unknown versions.
-//
-// # Contract summary
-//
-// - Get returns (nil, [ErrNotFound]) on miss. (nil, nil) is forbidden.
-// - Set with ttl == 0 stores with no expiry.
-// - Delete is idempotent: deleting a missing key returns nil.
-// - Values returned by Get are caller-owned; backends must copy.
-// - All methods are safe for concurrent use.
+// that Get returns (nil, [ErrNotFound]) on a missing key; returning
+// (nil, nil) is forbidden.
+//
+// Optional extension interfaces surface backend capabilities; middleware
+// feature-detects them via type assertion and falls back to emulation
+// or no-op semantics when a backend does not implement them:
+//
+// - [GetAndDeleter] — atomic GET+DEL (e.g. Redis GETDEL); used by csrf.
+// - [Scanner] — key enumeration by prefix; used by session Reset.
+// - [PrefixDeleter] — bulk delete by prefix; used by cache InvalidatePrefix.
+// - [SetNXer] — atomic set-if-not-exists; used by idempotency lock acquisition.
+// - [Counter] — atomic monotonic counter (INCR); used by SSE replay for cross-process IDs.
+// - [Scripter] — server-side atomic scripts (EVALSHA); used by ratelimit Redis adapter.
+//
+// # Reference implementations
+//
+// - [MemoryKV] / [NewMemoryKV] — in-memory sharded store. Implements KV,
+// GetAndDeleter, Scanner, PrefixDeleter, SetNXer, and Counter.
+// Default store for single-process deployments and tests.
+// - middleware/session/redisstore — Redis-backed; KV + Scanner + GetAndDeleter.
+// - middleware/session/postgresstore — PostgreSQL-backed; KV only.
+// - middleware/csrf/redisstore — Redis-backed CSRF; KV + GetAndDeleter.
+// - middleware/ratelimit/redisstore — Redis-backed ratelimit via EVALSHA; implements Scripter.
+//
+// # Utilities
+//
+// [Prefixed] wraps any [KV] so all keys are transparently namespaced with a
+// prefix — useful when sharing one backend among multiple middleware.
+// [EncodeJSON] / [DecodeJSON] are convenience helpers for adapters that
+// persist structured payloads through the byte-level [KV] surface.
+// [EncodedResponse] + [ResponseWireVersion] define the versioned wire format
+// used by cache and idempotency middleware to persist captured HTTP responses.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/data-stores
package store
diff --git a/middleware/swagger/doc.go b/middleware/swagger/doc.go
index 7b21048f..40dbf93f 100644
--- a/middleware/swagger/doc.go
+++ b/middleware/swagger/doc.go
@@ -1,159 +1,34 @@
-// Package swagger provides OpenAPI specification viewer middleware for
-// celeris.
-//
-// The middleware serves a Swagger UI (or Scalar / ReDoc) page and the raw
-// OpenAPI spec at configurable URL paths. It supports both JSON and YAML
-// specs with automatic content-type detection.
-//
-// Basic usage with an embedded spec:
-//
-// import "embed"
+// Package swagger provides OpenAPI specification viewer middleware for celeris.
+//
+// [New] returns a [celeris.HandlerFunc] that serves an interactive API
+// reference page and the raw OpenAPI spec (JSON or YAML, auto-detected) under
+// a configurable base path. By default it registers {BasePath}/ for the UI,
+// {BasePath}/spec for the raw spec, and redirects {BasePath} to {BasePath}/;
+// other paths and non-GET/HEAD methods pass through or return 405.
+//
+// [Config] is the entry point. Provide the spec via Config.SpecContent (an
+// embedded byte slice) or Config.SpecURL (an externally hosted spec, in which
+// case no /spec endpoint is registered). Config.Renderer selects the frontend
+// ([RendererSwaggerUI] (default), [RendererScalar], or [RendererReDoc]), and
+// Config.Options passes renderer-specific settings as a JSON-serializable map.
+//
+// [UIConfig] (Config.UI) tunes the Swagger UI renderer — DocExpansion, Title,
+// and OAuth2 pre-configuration via [OAuth2Config]; most of its fields are
+// ignored by Scalar and ReDoc. Use [IntPtr] to set UIConfig.DefaultModelsExpandDepth,
+// which is an *int so an explicit zero is distinguishable from unset. For
+// air-gapped deployments, set Config.AssetsPath to serve bundled assets locally
+// instead of from a CDN.
+//
+// The middleware has no built-in authentication; OpenAPI specs may expose
+// internal API structure, so place it after auth middleware to protect the
+// endpoints.
//
// //go:embed openapi.json
// var spec []byte
//
-// server.Use(swagger.New(swagger.Config{
-// SpecContent: spec,
-// }))
-//
-// Custom base path and UI options:
-//
-// server.Use(swagger.New(swagger.Config{
-// SpecContent: spec,
-// BasePath: "/docs",
-// UI: swagger.UIConfig{
-// DocExpansion: "full",
-// DeepLinking: true,
-// PersistAuthorization: true,
-// DefaultModelsExpandDepth: 1,
-// Title: "My API",
-// },
-// }))
-//
-// Using Scalar instead of Swagger UI:
-//
-// server.Use(swagger.New(swagger.Config{
-// SpecContent: spec,
-// Renderer: swagger.RendererScalar,
-// }))
-//
-// Using ReDoc instead of Swagger UI:
-//
-// server.Use(swagger.New(swagger.Config{
-// SpecContent: spec,
-// Renderer: swagger.RendererReDoc,
-// }))
-//
-// Note that [UIConfig] options (DocExpansion, DeepLinking, PersistAuthorization,
-// DefaultModelsExpandDepth) apply only to Swagger UI and are ignored when
-// Renderer is Scalar or ReDoc.
-//
-// # Renderer-Specific Options
-//
-// Use [Config].Options to pass renderer-specific configuration as a
-// JSON-serializable map. For ReDoc these are passed to Redoc.init(),
-// for Scalar they become the data-configuration attribute, and for
-// Swagger UI they are passed to SwaggerUIBundle().
-//
-// ReDoc dark theme example:
-//
-// server.Use(swagger.New(swagger.Config{
-// SpecContent: spec,
-// Renderer: swagger.RendererReDoc,
-// Options: map[string]any{
-// "theme": map[string]any{
-// "colors": map[string]any{"primary": map[string]any{"main": "#32329f"}},
-// },
-// "expandResponses": "200,201",
-// "hideDownloadButton": true,
-// },
-// }))
-//
-// When Options is nil, each renderer uses its own defaults.
-//
-// # OAuth2 Pre-Configuration
-//
-// Swagger UI supports pre-filling the OAuth2 authorization dialog. Set
-// [UIConfig].OAuth2 to configure client credentials:
-//
-// server.Use(swagger.New(swagger.Config{
-// SpecContent: spec,
-// UI: swagger.UIConfig{
-// OAuth2RedirectURL: "https://example.com/oauth2-redirect",
-// OAuth2: &swagger.OAuth2Config{
-// ClientID: "my-client-id",
-// AppName: "My Application",
-// Scopes: []string{"read:api", "write:api"},
-// UsePKCE: true,
-// },
-// },
-// }))
-//
-// All [OAuth2Config] values are embedded in the HTML page source. Only
-// public-client material (ClientID, scopes, app name) is supported;
-// confidential client secrets cannot be kept secret in a browser. Use
-// PKCE (UsePKCE: true) for the recommended public-client flow.
-//
-// OAuth2 fields are ignored when Renderer is not [RendererSwaggerUI].
-//
-// External spec URL (no /spec endpoint registered):
-//
-// server.Use(swagger.New(swagger.Config{
-// SpecURL: "https://petstore.swagger.io/v2/swagger.json",
-// }))
-//
-// # Air-Gapped / Self-Hosted Assets
-//
-// By default, CSS and JavaScript are loaded from public CDNs. For
-// environments without internet access, set [Config].AssetsPath to a
-// local URL prefix and serve the bundled assets with a static file
-// middleware:
-//
-// server.Use(static.New(static.Config{
-// Root: "./swagger-ui-dist",
-// Prefix: "/swagger-assets",
-// }))
-// server.Use(swagger.New(swagger.Config{
-// SpecContent: spec,
-// AssetsPath: "/swagger-assets",
-// }))
-//
-// # Content-Type Detection
-//
-// When serving the spec via the /spec endpoint, the middleware
-// auto-detects whether the content is JSON or YAML. Detection checks
-// the [Config].SpecFile extension first (.json, .yaml, .yml), then
-// inspects the first non-whitespace byte of the content ('{' or '['
-// indicates JSON; anything else is assumed YAML).
-//
-// # Endpoints
-//
-// The middleware registers:
-//
-// - {BasePath}/ — renders the UI page (HTML)
-// - {BasePath}/spec — serves the raw spec (JSON or YAML)
-// - {BasePath} — redirects to {BasePath}/
-//
-// Requests to other paths pass through. Only GET and HEAD are handled;
-// other methods return 405.
-//
-// # Ordering
-//
-// The swagger middleware intercepts by path prefix and can be placed at any
-// position in the [celeris.Server.Use] chain. Place it after authentication
-// middleware if you want to protect the spec and UI endpoints.
-//
-// # Skipping
-//
-// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for
-// exact-match path exclusions. Skipped requests call c.Next() without
-// serving the UI or spec.
+// server.Use(swagger.New(swagger.Config{SpecContent: spec}))
//
-// # Security
+// # Documentation
//
-// The middleware has no built-in authentication. OpenAPI specs may reveal
-// internal API structure. Protect the endpoints with upstream auth middleware
-// or network-level controls. When using CDN-loaded assets, ensure your CSP
-// policy allows cdn.jsdelivr.net (Scalar) or unpkg.com (Swagger UI) in
-// script-src and style-src.
+// Full guides and examples: https://goceleris.dev/docs/middleware-content
package swagger
diff --git a/middleware/timeout/doc.go b/middleware/timeout/doc.go
index d83fa2aa..e845a456 100644
--- a/middleware/timeout/doc.go
+++ b/middleware/timeout/doc.go
@@ -1,54 +1,34 @@
// Package timeout provides request timeout middleware for celeris.
//
-// The middleware wraps each request's context with a deadline. If the
-// downstream handler does not complete before the deadline, the context
-// is cancelled and a configurable error response is returned.
-//
-// Basic usage with the default 5-second timeout:
-//
-// server.Use(timeout.New())
-//
-// Custom timeout and error handler:
-//
-// server.Use(timeout.New(timeout.Config{
-// Timeout: 10 * time.Second,
-// ErrorHandler: func(c *celeris.Context, err error) error {
-// return c.JSON(503, map[string]string{"error": "timed out"})
-// },
-// }))
-//
-// Set [Config].Preemptive to true to run the handler in a separate
-// goroutine with response buffering. In preemptive mode, the middleware
-// returns the timeout error immediately when the deadline expires, even
-// if the handler is blocked on a non-context-aware operation.
-//
-// Set [Config].TimeoutFunc to compute a timeout dynamically per request.
-// The static [Config].Timeout is used as a fallback when TimeoutFunc is
-// nil or returns a non-positive duration.
-//
-// In cooperative mode (default), the handler runs in the request
-// goroutine. The context deadline is set, but the handler must check
-// c.Context().Done() to respect the timeout. Cooperative mode has no
-// extra goroutine overhead and no response buffering cost.
-//
-// [Config].ErrorHandler receives the triggering error
-// (context.DeadlineExceeded, a matched TimeoutErrors entry, or a
-// panic-wrapped error). If it panics, [ErrServiceUnavailable] is
-// returned as a last-resort fallback.
-//
-// [Config].TimeoutErrors lists errors that should be treated as
-// timeouts even if the deadline has not been reached (e.g., database
-// query timeouts).
-//
-// [ErrServiceUnavailable] is the exported sentinel error (503) returned
-// when no custom error handler is configured, usable with errors.Is.
-//
-// Set [Config].Skip to bypass the middleware dynamically, or
-// [Config].SkipPaths for exact-match path exclusions.
-//
-// Warning: In preemptive mode, if the handler does not exit promptly
-// after context cancellation, the middleware goroutine blocks waiting
-// for the handler to complete. This ties up the connection and prevents
-// the Context from being returned to the pool. Handlers MUST check
-// c.Context().Done() and return quickly on cancellation.
+// [New] returns a [celeris.HandlerFunc] that wraps each request's context
+// with a deadline. If the downstream handler does not complete within the
+// configured duration, the context is cancelled and a configurable error
+// response is returned.
+//
+// Key exported symbols:
+//
+// - [New] — constructs the middleware from a [Config].
+// - [Config] — options: [Config.Timeout] (static duration, default 5 s),
+// [Config.TimeoutFunc] (per-request duration; overrides Timeout when > 0),
+// [Config.ErrorHandler] (called on timeout; default returns 503),
+// [Config.TimeoutErrors] (treat matching upstream errors as timeouts),
+// [Config.Preemptive] (run handler in a goroutine; see below),
+// [Config.Skip] / [Config.SkipPaths] (bypass the middleware).
+// - [ErrServiceUnavailable] — sentinel 503 error; aliases
+// celeris.ErrServiceUnavailable for cross-middleware errors.Is matching.
+//
+// Cooperative mode (default, Preemptive: false): the handler runs in the
+// request goroutine with the context deadline set. Handlers must observe
+// c.Context().Done() to respect the cancellation. No goroutine or buffer
+// overhead.
+//
+// Preemptive mode (Preemptive: true): the handler runs in a spawned goroutine
+// with the response buffered. When the deadline expires the middleware waits
+// for the goroutine to exit, discards the buffered response, and invokes the
+// error handler. Handlers MUST exit promptly on context cancellation to avoid
+// blocking the connection. Incompatible with streaming (StreamWriter).
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/middleware-traffic
package timeout
diff --git a/middleware/websocket/doc.go b/middleware/websocket/doc.go
index f84919ce..9fa885a0 100644
--- a/middleware/websocket/doc.go
+++ b/middleware/websocket/doc.go
@@ -1,7 +1,12 @@
// Package websocket provides a zero-dependency native WebSocket middleware
// for celeris, implementing RFC 6455.
//
-// # Basic Echo Server
+// Register the middleware on a route with [New] and a [Config] whose
+// [Config.Handler] runs once per upgraded connection. The handler receives
+// a [*Conn] and should block until the connection is done; it is closed
+// automatically when the handler returns. Non-WebSocket requests pass
+// through to the next handler, and HTTP/2 requests get 426 Upgrade Required
+// (hijacking is impossible over multiplexed streams).
//
// server.GET("/ws", websocket.New(websocket.Config{
// Handler: func(c *websocket.Conn) {
@@ -10,226 +15,41 @@
// if err != nil {
// break
// }
-// if err := c.WriteMessage(mt, msg); err != nil {
-// break
-// }
-// }
-// },
-// }))
-//
-// # Origin Checking
-//
-// By default, a same-origin check is enforced (the Origin header must
-// match the Host header). To allow all origins:
-//
-// websocket.New(websocket.Config{
-// CheckOrigin: func(c *celeris.Context) bool { return true },
-// Handler: myHandler,
-// })
-//
-// To restrict to specific origins:
-//
-// websocket.New(websocket.Config{
-// CheckOrigin: func(c *celeris.Context) bool {
-// return c.Header("origin") == "https://example.com"
-// },
-// Handler: myHandler,
-// })
-//
-// # JSON Messaging
-//
-// websocket.New(websocket.Config{
-// Handler: func(c *websocket.Conn) {
-// var msg MyType
-// if err := c.ReadJSON(&msg); err != nil {
-// return
-// }
-// c.WriteJSON(msg)
-// },
-// })
-//
-// # Subprotocol Negotiation
-//
-// websocket.New(websocket.Config{
-// Subprotocols: []string{"graphql-transport-ws"},
-// Handler: func(c *websocket.Conn) {
-// proto := c.Subprotocol()
-// // handle based on negotiated protocol
-// },
-// })
-//
-// # HTTP/2 Limitation
-//
-// WebSocket requires HTTP/1.1 connection hijacking. HTTP/2 multiplexes
-// streams over a single TCP connection, making hijack impossible. This
-// middleware returns 426 Upgrade Required for HTTP/2 requests.
-//
-// # Concurrency
-//
-// All write methods ([Conn.WriteMessage], [Conn.WriteText], [Conn.WriteBinary],
-// [Conn.WriteJSON], [Conn.WritePing]) are internally serialized and safe for
-// concurrent use from multiple goroutines. A single goroutine may call
-// [Conn.ReadMessage] while others write concurrently.
-//
-// [Conn.SetPingHandler], [Conn.SetPongHandler], and [Conn.SetCloseHandler]
-// must be called before starting the read loop.
-//
-// # Keepalive (Ping/Pong)
-//
-// Detect dead connections with periodic pings:
-//
-// Handler: func(c *websocket.Conn) {
-// c.SetPongHandler(func(data []byte) error {
-// c.SetReadDeadline(time.Now().Add(60 * time.Second))
-// return nil
-// })
-// c.SetReadDeadline(time.Now().Add(60 * time.Second))
-//
-// go func() {
-// ticker := time.NewTicker(30 * time.Second)
-// defer ticker.Stop()
-// for range ticker.C {
-// if err := c.WritePing(nil); err != nil {
-// return
-// }
+// c.WriteMessage(mt, msg)
// }
-// }()
-//
-// for {
-// mt, msg, err := c.ReadMessage()
-// if err != nil { break }
-// c.WriteMessage(mt, msg)
-// }
-// },
-//
-// # Compression (permessage-deflate)
-//
-// Enable RFC 7692 permessage-deflate compression:
-//
-// websocket.New(websocket.Config{
-// EnableCompression: true,
-// Handler: myHandler,
-// })
-//
-// Compression is negotiated during the upgrade handshake. Messages above
-// the compression threshold (default 128 bytes) are compressed transparently.
-//
-// # Streaming Large Messages
-//
-// For large messages, use [Conn.NextReader] and [Conn.NextWriter] to avoid
-// buffering the entire message in memory:
-//
-// mt, reader, _ := c.NextReader()
-// io.Copy(dst, reader)
-//
-// writer, _ := c.NextWriter(websocket.TextMessage)
-// writer.Write(chunk1)
-// writer.Write(chunk2)
-// writer.Close()
-//
-// # ReadMessage vs ReadMessageReuse
-//
-// [Conn.ReadMessage] returns an owned copy (safe to retain).
-// [Conn.ReadMessageReuse] returns a reused buffer (zero-alloc, only valid
-// until the next read call). Use ReadMessageReuse for echo servers and
-// message-forwarding proxies where the data is processed immediately.
-//
-// # Access Request Data
-//
-// Route params, query params, and headers are captured at upgrade time:
-//
-// // Route: /ws/:room
-// c.Param("room") // route parameter
-// c.Query("token") // query parameter
-// c.Header("origin") // request header
-//
-// # Engine-integrated mode vs hijack mode
-//
-// On the std engine, WebSocket connections are upgraded via Go's standard
-// connection hijacking — the handler runs on a goroutine and reads/writes
-// directly on a *net.TCPConn. On the native engines (epoll, io_uring), the
-// connection stays in the event loop after upgrade: inbound chunks are
-// delivered to the handler goroutine via an internal chanReader (which
-// applies TCP-level backpressure on overflow), and outbound writes go
-// through the engine's per-connection write buffer (cs.writeBuf).
-//
-// The same Handler signature works for both modes — the middleware picks
-// the engine-integrated path automatically when the engine supports it
-// and falls back to hijack on the std engine.
-//
-// # Backpressure semantics
-//
-// On the engine path, the WebSocket conn maintains a bounded chanReader
-// between the event loop and the handler goroutine. When the buffer fills
-// past the high-water mark (75% of [Config.MaxBackpressureBuffer]), the
-// engine pauses inbound delivery for that connection (epoll: drops
-// EPOLLIN; io_uring: cancels the in-flight RECV). The kernel then closes
-// the TCP receive window, slowing the peer at the network level. When the
-// buffer drains below 25%, the engine resumes inbound delivery.
-//
-// On the std (hijack) path, backpressure is handled directly by the
-// kernel's TCP stack via [net.Conn.Read] returning when the kernel buffer
-// has data — no middleware-level buffering happens.
-//
-// In healthy operation [Conn.BackpressureDropped] returns 0. A non-zero
-// value indicates the engine pause/resume mechanism is malfunctioning.
-//
-// # IdleTimeout semantics
-//
-// On the std (hijack) path, [Config.IdleTimeout] is enforced via
-// [net.Conn.SetReadDeadline], which is reset before each blocking read.
-// On the engine path, the WS middleware extends an absolute deadline via
-// [Context.SetWSIdleDeadline] after each successful frame read, and the
-// engine's idle sweep closes connections whose deadline has expired.
-// Both paths converge to the same observable behavior.
-//
-// # WriteControl deadline semantics
-//
-// [Conn.WriteControl] applies the supplied deadline to the channel-based
-// write semaphore (so a stalled large NextWriter cannot indefinitely block
-// pings/pongs). On the std path, the deadline is also pinned to the
-// underlying [net.Conn] via [net.Conn.SetWriteDeadline] so a peer that
-// has stopped reading cannot stall the actual flush. On the engine path,
-// writes go into the engine's write buffer and never block at the
-// syscall level — only the lock-acquisition deadline applies.
-//
-// # Fan-out (Hub)
-//
-// For broadcasting a single message to N connections, use [Hub] with
-// [PreparedMessage]. The frame is encoded once per uncompressed /
-// compressed variant and reused across every [Conn.WritePreparedMessage]
-// dispatch — so per-message wire-encoding cost is O(1) regardless of
-// subscriber count, while per-Conn write throughput remains the
-// engine's normal write path.
-//
-// hub := websocket.NewHub(websocket.HubConfig{
-// OnSlowConn: func(c *websocket.Conn, err error) websocket.HubPolicy {
-// return websocket.HubPolicyClose // boot misbehaving peers
-// },
-// })
-// server.GET("/ws", websocket.New(websocket.Config{
-// Handler: func(c *websocket.Conn) {
-// unregister := hub.Register(c)
-// defer unregister()
-// // your read loop here
// },
// }))
-// // Publishers anywhere in the app:
-// hub.Broadcast(websocket.TextMessage, []byte(`{"type":"tick"}`))
-//
-// Hub.Close drains every in-flight Broadcast (via an internal
-// inflight WaitGroup) before tearing down conns, so a shutdown that
-// synchronises on Close cannot race a still-fanning-out message.
-//
-// Authorization MUST happen before [Hub.Register]. Hub broadcasts go
-// to every registered connection unfiltered; if a per-conn ACL is
-// required, use [Hub.BroadcastFilter] with a pure predicate.
-//
-// PreparedMessage rejects control opcodes (Ping/Pong/Close) at
-// construction time — control frames have RFC 6455 §5.5 size and
-// fragmentation constraints that the cache-and-reuse model can't
-// satisfy. Use [Conn.WriteControl] per-conn for those.
//
-// See also: middleware/sse for the equivalent on Server-Sent Events,
-// where the same broker pattern lives as [middleware/sse.Broker].
+// On [Conn], [Conn.ReadMessage] / [Conn.WriteMessage] (plus the
+// [Conn.WriteText], [Conn.WriteBinary], [Conn.ReadJSON], and [Conn.WriteJSON]
+// helpers) cover the common case. [Conn.ReadMessageReuse] returns a buffer
+// valid only until the next read for zero-alloc echo/proxy loops, while
+// [Conn.NextReader] and [Conn.NextWriter] stream large messages without
+// buffering them whole. [Conn.WriteControl], [Conn.WritePing], and the
+// [Conn.SetPingHandler] / [Conn.SetPongHandler] / [Conn.SetCloseHandler]
+// hooks handle keepalive and shutdown. All write methods are internally
+// serialized and safe for concurrent use; a single goroutine may read while
+// others write.
+//
+// [Config] options control origin checking ([Config.CheckOrigin], default
+// same-origin), subprotocol negotiation ([Config.Subprotocols]),
+// permessage-deflate compression ([Config.EnableCompression], RFC 7692),
+// idle timeouts, and backpressure tuning for the native-engine path. Route
+// params, query values, and headers captured at upgrade time are available
+// via [Conn.Param], [Conn.Query], and [Conn.Header].
+//
+// The same Handler works on every engine. On the std engine the connection
+// is hijacked for direct I/O; on native engines (epoll, io_uring) it stays
+// in the event loop with TCP-level backpressure. [Conn.BackpressureDropped]
+// returns 0 in healthy operation.
+//
+// For broadcasting one message to many connections, register each [*Conn]
+// with a [Hub] and publish via [Hub.Broadcast], [Hub.BroadcastFilter], or
+// [Hub.BroadcastPrepared] with a [PreparedMessage] (encoded once, reused
+// across subscribers). Slow connections are handled by [HubConfig.OnSlowConn]
+// returning a [HubPolicy]. Authorization must happen before [Hub.Register].
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/websocket
package websocket
diff --git a/observe/doc.go b/observe/doc.go
index 0b16932f..c73ec92b 100644
--- a/observe/doc.go
+++ b/observe/doc.go
@@ -7,4 +7,8 @@
//
// For Prometheus and debug endpoint integration, see the
// middleware/metrics package.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/observability
package observe
diff --git a/probe/doc.go b/probe/doc.go
index e14c8e21..c701f957 100644
--- a/probe/doc.go
+++ b/probe/doc.go
@@ -1,9 +1,15 @@
-// Package probe provides capability detection for io_uring and epoll.
+// Package probe provides kernel capability detection for the io_uring engine.
//
-// At engine startup, the celeris io_uring engine calls Detect() to discover
-// which kernel features are available (multishot accept/recv, provided
-// buffer rings, fixed-file tables, SEND_ZC, etc.) and assigns a Tier from
-// the resulting Capabilities. Application code typically does not call
-// this package directly; inspect [celeris.Server.EngineInfo] at runtime
-// for the active feature set.
+// At engine startup, [Probe] (or [ProbeWith] for testing) discovers which
+// kernel features are available — multishot accept/recv, provided buffer
+// rings, fixed-file tables, SEND_ZC, SQPOLL, and more — and returns an
+// [engine.CapabilityProfile] that includes the selected [engine.Tier].
+// Application code typically does not call this package directly; inspect
+// [celeris.Server.EngineInfo] at runtime for the active feature set.
+// [DiagnosticReport] and [FormatDiagnostic] can surface the profile as
+// human-readable text for troubleshooting.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/engines
package probe
diff --git a/protocol/h1/doc.go b/protocol/h1/doc.go
index 403354e2..b65b35d9 100644
--- a/protocol/h1/doc.go
+++ b/protocol/h1/doc.go
@@ -6,4 +6,8 @@
// that alias the connection's read buffer — callers must materialize
// (clone) any value they retain past the next [ParseRequest] call on the
// same connection.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/engines
package h1
diff --git a/protocol/h2/doc.go b/protocol/h2/doc.go
index 035d1960..2f36e2f2 100644
--- a/protocol/h2/doc.go
+++ b/protocol/h2/doc.go
@@ -9,4 +9,8 @@
//
// Application code should not import these packages directly — interact
// with HTTP/2 through the celeris [Server] and [Context] types.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/engines
package h2
diff --git a/resource/doc.go b/resource/doc.go
index 11580ccf..08322e94 100644
--- a/resource/doc.go
+++ b/resource/doc.go
@@ -1,4 +1,21 @@
// Package resource defines server configuration, resource limits, and default
// presets. The top-level celeris.Config is the primary user-facing type; this
// package provides the internal representation used by engine implementations.
+//
+// Key exported symbols:
+// - [Config] — internal server configuration with [Config.Validate] and
+// [Config.WithDefaults] helpers used by engine implementations.
+// - [Resources] — optional user overrides for buffer counts and concurrency
+// limits; zero values fall back to engine defaults.
+// - [ResolvedResources] — the fully resolved values after applying defaults,
+// user overrides, and hard caps; consumed by engines at startup.
+// - [WorkloadHint] — optional operator hint ([WorkloadLowConcurrency] /
+// [WorkloadHighConcurrency])
+// that influences which I/O backend the adaptive engine starts on.
+// - [NextPowerOf2] — utility used by engine packages to size io_uring rings
+// and similar power-of-two kernel structures.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs/configuration
package resource
diff --git a/server.go b/server.go
index 515f1e99..b379aa94 100644
--- a/server.go
+++ b/server.go
@@ -538,8 +538,9 @@ func (s *Server) ResumeAccept() error {
return ac.ResumeAccept()
}
-// Collector returns the metrics collector, or nil if the server has not been
-// started or if Config.DisableMetrics is true.
+// Collector returns the metrics collector. It is created eagerly in New, so it
+// is non-nil before Start is called; it returns nil only when
+// Config.DisableMetrics is true.
func (s *Server) Collector() *observe.Collector {
return s.collector
}
diff --git a/test/perfmatrix/doc.go b/test/perfmatrix/doc.go
index e6bfe8d5..8ba898f6 100644
--- a/test/perfmatrix/doc.go
+++ b/test/perfmatrix/doc.go
@@ -15,29 +15,13 @@
// report/ csv / markdown / benchstat / pprof index writers
// profiling/ per-cell pprof capture helpers
//
-// # Running
-//
-// Invoke the mage targets from the repo root:
-//
-// mage matrixBench # full release-gate (10 × 10s), ~2.5 days
-// mage matrixBenchDeep # maximum-rigor (10 × 15s, 3s warmup), ~3.5 days
-// mage matrixBenchStrict # -race + checkptr + fail-fast (3 × 5s), ~4-8h
-// mage matrixBenchQuick # dev-loop (3 × 5s, static only), ~1 hour
-// mage matrixBenchDrivers # driver cells only (10 × 10s)
-// mage matrixBenchProfile # full matrix with pprof capture per cell
-// mage matrixBenchSince # HEAD vs PERFMATRIX_REF, fails on >2% regression
-//
-// matrixBenchStrict is the release-gate confidence check. The runner
-// and every in-process engine / server are built with -race and
-// -gcflags=-d=checkptr=2; GORACE=halt_on_error=1 + GOTRACEBACK=crash
-// + runner -fail-fast abort the moment any data race, use-after-free,
-// or invalid unsafe.Pointer conversion surfaces — the bug class that
-// otherwise sits buried for hours of churn load before the consequence
-// fires.
-//
// # Cell identifiers
//
// Each cell is uniquely addressed by "/".
// Server names follow "-[-upgrade][+async]"; see
// servers/celeris for the full celeris naming scheme.
+//
+// # Documentation
+//
+// Full guides and examples: https://goceleris.dev/docs
package perfmatrix
diff --git a/test/redisspec/doc.go b/test/redisspec/doc.go
index b723b5c4..9bf67b01 100644
--- a/test/redisspec/doc.go
+++ b/test/redisspec/doc.go
@@ -14,21 +14,12 @@
// - Section 7: Wire edge cases (split reads, flooding, binary keys, concurrency)
// - Section 8: Protocol fuzzing (fuzz_test.go)
//
-// # Running
-//
-// The tests are gated by the `redisspec` build tag and the CELERIS_REDIS_ADDR
-// environment variable. Both must be present for any test to execute:
-//
-// CELERIS_REDIS_ADDR='127.0.0.1:6379' \
-// go test -tags redisspec -count=1 -timeout=300s -v ./test/redisspec/...
-//
-// A default Redis 7.2 instance with no authentication is sufficient for all
-// tests except the AUTH section, which requires CELERIS_REDIS_PASSWORD.
+// Tests are gated by the `redisspec` build tag and the CELERIS_REDIS_ADDR
+// environment variable. Every test uses raw TCP connections and the
+// [github.com/goceleris/celeris/driver/redis/protocol] package for RESP
+// encoding/decoding — no third-party Redis client is imported.
//
-// # Design
+// # Documentation
//
-// Every test uses raw TCP connections and the celeris
-// [github.com/goceleris/celeris/driver/redis/protocol] package for RESP
-// encoding/decoding. No third-party Redis client library is imported. This
-// isolates the verification to pure protocol-level correctness.
+// Full guides and examples: https://goceleris.dev/docs
package redisspec
diff --git a/validation/doc.go b/validation/doc.go
index c439f39f..0ad4734f 100644
--- a/validation/doc.go
+++ b/validation/doc.go
@@ -5,31 +5,21 @@
// no-op stubs in disabled.go, so the counters and the unix-socket
// endpoint are stripped at compile time — no allocations, no atomic
// adds, no goroutines. Validation builds (-tags=validation) compile
-// against assertions.go and endpoint.go, exposing the counters as
-// atomic.Uint64 and serving a JSON snapshot over
-// /tmp/celeris-validation.sock.
+// against assertions.go and endpoint.go, exposing named [Counter]
+// variables (e.g. [PanicCount], [RatelimitTokenViolations]) as
+// atomic.Uint64 and serving a JSON snapshot over [SocketPath] via
+// [StartEndpoint].
//
-// External property-test harnesses read the socket on every poll to
-// feed property predicates. The counter shape is stable across both
-// build modes via the [Counters] struct and the [Snapshot] function —
-// the only thing that changes is whether reads return live atomics
-// or the zero value.
+// [Snapshot] returns a [Counters] struct whose shape is stable across
+// both build modes — reads return live atomics under -tags=validation
+// and zero values otherwise. External property-test harnesses poll
+// [Snapshot] or read [SocketPath] on every tick to feed predicates.
//
-// # Call-pattern convention
+// Unconditional event counts (e.g. recovered panics) are recorded via
+// thin helpers such as [RecordPanic]; these carry no build tag and
+// inline to nothing in production builds.
//
-// Two call patterns coexist and SHOULD be used consistently:
+// # Documentation
//
-// - For unconditional event counts (panic recovered, etc.) call the
-// RecordX() helper defined in hooks.go (e.g. validation.RecordPanic()).
-// Helpers carry no build tag — they delegate to Counter.Add, which
-// inlines to nothing under !validation.
-// - For predicate-bearing checks (a value violates an invariant) call
-// the per-package validate*() helper defined in that package's
-// validation_check.go (under //go:build validation) with a no-op
-// stub in validation_default.go (under //go:build !validation).
-// Examples: middleware/jwt.validateAdmission,
-// middleware/ratelimit.validateBucket.
-//
-// Bare validation.Counter.Add(1) outside a validate*() helper is a
-// bug — use a RecordX() helper instead.
+// Full guides and examples: https://goceleris.dev/docs
package validation