Help us shape the future of Go! Take the 2026 Go Developer Survey

vov

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 17 Imported by: 1

README

vov

Declarative Go backend framework

Documentation

Overview

Package vov is a declarative, batteries-included Go backend framework.

The endpoint declaration is the single source of truth. An Endpoint carries its method, path, handler, and middleware as plain data, and every other part of the framework — routing today, and policy, the route manifest, and generated tests later — is a consumer of that declaration rather than a parallel structure that can drift from it.

An App assembles a set of endpoints onto a standard *http.ServeMux, exposes that mux for raw registration, and owns the server lifecycle (signal handling, graceful shutdown, cleanup hooks) so it does not have to be rewritten for every service. It serves with a standard *http.Server the caller can fully configure. The App is only an assembler over the declarations; the declarations remain usable without it.

Index

Constants

View Source
const DefaultAddress = ":8080"

DefaultAddress is the listen address used when AppConfig.Address is empty.

View Source
const DefaultShutdownTimeout = 15 * time.Second

DefaultShutdownTimeout bounds how long App.Run waits for in-flight requests and cleanup hooks to finish before giving up.

View Source
const DefaultStackName = "default"

DefaultStackName is the key in [AppConfig.Stacks] whose stack applies to every endpoint that does not name one.

Variables

This section is empty.

Functions

func ContextWithUser added in v0.1.0

func ContextWithUser(ctx context.Context, u User) context.Context

ContextWithUser returns a copy of ctx carrying u. vov calls this for every endpoint that requires auth; apps rarely need it outside tests.

func LoadEnv added in v0.1.0

func LoadEnv(dst any) error

LoadEnv fills dst from the process environment, where dst is a pointer to a struct whose fields carry `env` tags:

type Config struct {
    DatabaseURL string        `env:"DATABASE_URL,required"`
    Port        int           `env:"PORT" envDefault:"8080"`
    Timeout     time.Duration `env:"TIMEOUT" envDefault:"30s"`
    Debug       bool          `env:"DEBUG"`
}

The struct is both the declaration and the access surface, so what a service requires and how it reads it cannot drift apart. Call it before NewApp: the dependencies that need configuration are built before the endpoints that use them.

A field with no `env` tag is left alone, so a config struct may hold values that come from somewhere else. Nested structs are walked. A missing optional variable leaves the field at its zero value; declare the field as a pointer to tell "unset" from "set to the zero value", as Ptr does elsewhere in vov.

An unset variable and one set to the empty string are treated the same: the default applies, and a required variable is reported missing.

Every problem is collected and returned together as an EnvError — one boot failure listing all of them beats fixing one variable per restart.

func Manifest added in v0.1.0

func Manifest(routes []Route) string

Manifest renders routes as the text to check in beside the code.

It exists because of a gap no test tier can cover. Generated tests assert that the code does what the declaration says, so an endpoint whose policy is quietly loosened stays green — the tests move with it. A checked-in manifest turns that same change into a modified line in a pull request, which is the only place a human sees it.

The output is deterministic: routes are sorted by path, and each route's methods appear in a fixed order, so the file changes only when a declaration changes and not when the Routes slice is reordered.

Every line carries its own method and path rather than being grouped under a URL heading, because a diff shows a handful of lines without their surroundings — a changed line has to say for itself which endpoint it is.

The manifest covers declared routes only. Anything registered straight on App.Mux is outside the framework and cannot appear here.

func Ptr added in v0.1.0

func Ptr[T any](v T) *T

Ptr returns a pointer to v. It lets the pointer-typed fields of Server be set inline instead of through a temporary variable:

vov.Server{ReadTimeout: vov.Ptr(30 * time.Second)}

Types

type App added in v0.1.0

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

App assembles a set of Endpoint declarations onto a standard http.ServeMux, holds the http.Server it will serve with, and owns the server lifecycle. Construct it with NewApp.

func NewApp added in v0.1.0

func NewApp(cfg AppConfig) (*App, error)

NewApp validates the configuration, builds a mux, registers every handler on it, and resolves the http.Server to serve with. It fails closed: an endpoint with an empty path, a nil handler, or a route that duplicates an earlier one is a construction error, not a boot-time surprise. (Genuinely conflicting — but non-identical — mux patterns are still reported by the underlying http.ServeMux.)

func (*App) Handler added in v0.2.0

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

Handler returns what the app serves: the mux wrapped in AppConfig.ServerWrappers. This is the handler to hand to httptest, or to mount inside a larger server when App.Run is not being used.

func (*App) Manifest added in v0.1.0

func (a *App) Manifest() string

Manifest renders the app's routes — see Manifest.

func (*App) Mux added in v0.1.0

func (a *App) Mux() *http.ServeMux

Mux returns the underlying *http.ServeMux, for registering routes vov does not model. Handlers registered on it bypass the framework's endpoint management and do not appear in Manifest.

It is the registration surface, not the serving one: it does not include AppConfig.ServerWrappers, which wrap it. To exercise the app as it is actually served — in a test, or behind another server — use App.Handler.

func (*App) OnShutdown added in v0.1.0

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

OnShutdown registers a cleanup function to run during graceful shutdown, after the HTTP server has stopped accepting connections and in-flight requests have drained. Hooks run in registration order, sharing the shutdown deadline. Safe to call concurrently.

func (*App) Routes added in v0.1.0

func (a *App) Routes() []Route

Routes returns the route declarations the app was built from, in the order they were given. The slice is a copy, so a caller cannot reshape the app's routing by editing it.

func (*App) Run added in v0.1.0

func (a *App) Run() error

Run starts the HTTP server and blocks until an interrupt (SIGINT) or termination (SIGTERM) signal arrives, or the server stops on its own. On signal it stops accepting connections, waits for in-flight requests to drain, and runs the registered shutdown hooks, all bounded by ShutdownTimeout.

type AppConfig added in v0.1.0

type AppConfig struct {
	// Routes are the URLs to serve, in order. Each groups every method of one
	// URL — see [Route]. A URL may be declared only once: two Routes sharing a
	// Path is a construction error, since it would split one URL's definition
	// across two places.
	Routes []Route

	// RequireGlobals are the [Global] holders that must be populated before this
	// app may be built. Listing one turns "forgot to call Set" from a panic on
	// the first request that happens to touch it into an error at construction,
	// before the server listens.
	//
	// vov never reads what a holder contains; it only asks whether it is ready.
	RequireGlobals []Readiness

	// Stacks are the named middleware combinations endpoints can be wrapped in,
	// each split into a Pre and Post half by the auth seam — see
	// [MiddlewareStack]. The stack under [DefaultStackName] applies to every
	// endpoint that does not name another; an endpoint naming one that was never
	// declared is a construction error.
	//
	// Stacks apply to endpoints only. Routes registered straight on [App.Mux]
	// are outside the framework and get no middleware from it.
	MiddlewareStacks map[string]MiddlewareStack

	// ServerWrappers wrap the whole assembled handler, outermost first, and
	// therefore see every request the server receives — not only the ones that
	// reach an endpoint. They wrap in both directions: a wrapper observes the
	// response on the way out as well as the request on the way in, which is
	// what makes access logging and panic recovery possible here.
	//
	// That is the difference from MiddlewareStacks, and it is not a shade of
	// meaning. http.ServeMux answers two kinds of request itself, before
	// dispatching to anything registered: a path no route declares (404), and a
	// method the matched path does not declare (405). No endpoint middleware
	// runs for either. A CORS preflight is exactly the second kind — OPTIONS
	// against a path registered for GET and PATCH — so a cross-origin browser
	// app cannot work unless something outside the mux answers it.
	//
	// The same is true of the things an operator most wants to be unconditional:
	// panic recovery, request-id stamping, access logging. An unmatched path
	// that panics or goes unlogged is precisely the request worth a record.
	//
	// Because it wraps the mux, it also covers routes registered straight on
	// [App.Mux]. That is unavoidable rather than chosen: the mux decides its own
	// 404 internally, so anything that can see that decision is necessarily
	// outside everything the mux serves.
	//
	// A wrapper runs before routing, so it cannot know which endpoint matched:
	// no path parameters, no AuthMode, no declared roles. It is also invisible to
	// [Manifest], which documents endpoint policy. Keep authorization out of it —
	// a rule declared here cannot be reviewed there.
	ServerWrappers []Middleware

	// Authenticator resolves the user a request acts as. Endpoints require an
	// authenticated user unless they declare [NoAuth], so this is required
	// unless every endpoint opts out — [NewApp] rejects a configuration that
	// needs an authenticator and has none, rather than starting a server whose
	// every protected route answers 401.
	Authenticator Authenticator

	// Address is the TCP listen address, e.g. ":8080". Defaults to
	// [DefaultAddress] when empty.
	Address string

	// ShutdownTimeout bounds graceful shutdown. Defaults to
	// [DefaultShutdownTimeout] when zero.
	ShutdownTimeout time.Duration

	// Server optionally tunes the http.Server vov serves with — timeouts, TLS,
	// connection hooks, byte limits, and so on. It mirrors http.Server but drops
	// Addr and Handler, which vov owns (one place for each, no conflicting
	// values). Its defaulted timeout fields are pointers: a nil field takes vov's
	// default, an explicit value — including 0, meaning "no timeout" — is honored.
	// Nil means all defaults. See [Server].
	Server *Server
}

AppConfig is the declarative input to NewApp. Apart from Handlers, its fields are the app-wide operational preferences; more are added here as they are needed rather than imagined up front.

type AuthMode added in v0.1.0

type AuthMode string

AuthMode says whether an Endpoint needs an authenticated user.

The zero value — an endpoint that says nothing — means AuthModeRequired. An endpoint is opened only by naming AuthModeNone, so forgetting to declare auth protects a route rather than exposing it. Everything here is arranged around that: see [AuthMode.required].

const (
	// AuthModeRequired demands an authenticated user. It is also what the zero
	// value means, so it rarely needs writing.
	AuthModeRequired AuthMode = "required"

	// AuthModeNone opts out: no user is resolved and none is required. Reach for
	// it deliberately — it is the visible, greppable token that says a route is
	// open.
	AuthModeNone AuthMode = "none"
)

type AuthResponse added in v0.2.0

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

AuthResponse is the part of the response an Authenticator may touch: its headers, and nothing else. vov owns the status and the body, because vov is what decides whether the outcome is 401, 403, 402, or the handler's own response — so an authenticator can add a header without any risk of writing a response that collides with the one about to be sent.

The motivating case is a credential that must be revoked as it is rejected. A long-lived signed cookie belonging to an account that no longer exists should be cleared while it is refused; otherwise the browser re-sends the dead credential on every request until it expires on its own, which for a 30-day session means 30 days.

Rotating a session on successful authentication — sliding expiry — is the same mechanism, on the other branch.

func (AuthResponse) Header added in v0.2.0

func (a AuthResponse) Header() http.Header

Header returns the response headers, for anything AuthResponse.SetCookie does not cover — a WWW-Authenticate challenge, for instance.

func (AuthResponse) SetCookie added in v0.2.0

func (a AuthResponse) SetCookie(c *http.Cookie)

SetCookie adds a Set-Cookie header, like http.SetCookie does for a ResponseWriter. Clearing a cookie is the same call with MaxAge set to -1 and the Name and Path that were used to write it.

type Authenticator added in v0.1.0

type Authenticator func(resp AuthResponse, r *http.Request) (User, error)

Authenticator resolves the User a request acts as. The app supplies it, because only the app knows where credentials live — a session cookie, a bearer token, a signed header — and how to look them up.

It receives an AuthResponse so that it can set headers on the way past — clearing a revoked cookie, rotating a live one. Headers set there survive whatever vov writes next, including a refusal.

Return values are distinguished on purpose:

(user, nil) → the request is authenticated as user
(nil, nil)  → no credentials were presented; vov answers 401
(nil, err)  → the lookup itself failed (database down, and so on); vov
              answers 500, because a broken dependency is not a bad password

Return an untyped nil User for "no user". A typed nil pointer is a non-nil interface, and vov would call IsAuthenticated on it.

type Endpoint added in v0.1.0

type Endpoint struct {
	// Handler serves the request. It is the standard net/http signature; the
	// objects an app builds at boot are reached through a [Global] holder rather
	// than being threaded through here.
	Handler http.HandlerFunc

	// MiddlewareStack names the [MiddlewareStack] from
	// [AppConfig.MiddlewareStacks] that wraps this endpoint. Empty selects
	// [DefaultStackName], so the common case says nothing. Naming a stack
	// replaces the default outright rather than adding to it; a name that was
	// never declared is a construction error.
	MiddlewareStack string

	// AuthMode declares this endpoint's authentication requirement. The zero
	// value requires an authenticated user, so a method that says nothing is
	// protected; [AuthModeNone] opts out. It is per-method on purpose: a URL may
	// be readable by anyone and writable only by an authenticated user.
	AuthMode AuthMode

	// RolesAnyOf restricts the endpoint to users holding at least one of them —
	// any-of, because a role is an identity and any of the listed identities
	// will do: "an admin or an owner may do this". Empty means no restriction.
	RolesAnyOf []string

	// PermissionsAllOf restricts the endpoint to users holding every one of
	// them — all-of, because a permission is a capability and each listed one is
	// needed. Every entry is checked. Empty means no restriction.
	//
	// The two may be declared together; a user must then satisfy both. Neither
	// is checked on an endpoint declaring [AuthModeNone], which resolves no
	// user, so that combination is a construction error rather than a
	// requirement that silently does nothing.
	PermissionsAllOf []string

	// MinTier restricts the endpoint to users whose [User.Tier] is at least this
	// high. Zero — the default — places no restriction, so only endpoints behind
	// a paywall mention it.
	//
	// A user who is refused here gets **402 Payment Required**, not 403, because
	// the two say different things to a client: 403 is a denial, 402 is a price.
	// A frontend can act on that — show the upgrade panel rather than a generic
	// error — which is why tier is a declaration of its own rather than a
	// permission.
	//
	// Reach for it only for access that money actually unlocks. Ordinal gating
	// that payment cannot resolve belongs in [Endpoint.PermissionsAllOf], whose
	// refusal is honest about being final.
	MinTier int
}

Endpoint is one method of one URL: the handler, plus the facts that describe how it is served — all as plain data. It lives inside an Endpoints group, which supplies the path and the method, so an Endpoint says only what is specific to that method.

It is deliberately a value type with no exported methods and no hidden state, so it can be read, listed, and (later) checked by tools without constructing an App. Its zero value declares nothing: a method field left empty on a group is a method that URL does not answer.

type Endpoints added in v0.1.0

type Endpoints struct {
	// Any answers every method that no specific field below claims. It is for
	// catch-alls — a static file tree, a single-page-app fallback — not for
	// ordinary endpoints, which should name their method so the declaration
	// says what the URL supports.
	Any Endpoint

	GET Endpoint

	// HEAD rarely needs declaring: net/http already answers HEAD with the GET
	// endpoint, and reports it in Allow. Declare it only to handle HEAD
	// differently from GET.
	HEAD Endpoint

	POST    Endpoint
	PUT     Endpoint
	PATCH   Endpoint
	DELETE  Endpoint
	OPTIONS Endpoint
}

Endpoints is every method of one URL, declared together.

It is separate from Route so that it can live beside the handlers that implement it — in the same file, next to the code it dispatches to — while the AppConfig.Routes slice stays a thin mapping from URL to endpoint group. The grouping is then visible in both places: the route list says which URLs exist, and the handler file says what one URL answers.

Methods are fields rather than map keys, so they cannot be misspelled, an editor completes them, and a single go-to-definition shows a URL's whole surface. Each carries its own Endpoint, so — unlike a Django class view — a URL's methods need not share configuration: one may take a different middleware stack or opt out of auth without affecting its siblings.

A method left empty is simply not registered, and requests using it get the 405 and Allow header that net/http derives from the methods that are.

type EnvError added in v0.1.0

type EnvError struct {
	Problems []string
}

EnvError reports everything wrong with the environment at once. Its Problems name variables and say what was expected; they never contain a variable's value, because the environment is where credentials live and an error string ends up in logs.

func (*EnvError) Error added in v0.1.0

func (e *EnvError) Error() string

type Global added in v0.1.0

type Global[T any] struct {
	// contains filtered or unexported fields
}

Global is a typed, write-once holder for an object an application builds at boot and needs from many handlers: a struct of a database handle, a logger, service clients.

The name is literal. This is process-wide state, with the coupling and the test-substitution hazards that implies, and it says so rather than dressing itself up as dependency injection — vov injects nothing, constructs nothing, and never looks inside what it holds.

It exists so that handlers can reach those objects while keeping the standard net/http signature. The alternatives both cost more than they give: threading a dependency type through the framework makes every declaration mention it and forces one shared type on every handler, and stashing values in the request context trades a compile error for a runtime assertion.

Declare one holder per application, next to the type it holds:

type Deps struct{ DB *sql.DB; Log *slog.Logger }

var deps = vov.NewGlobal[*Deps]()

func main() {
    deps.Set(&Deps{DB: db, Log: log})
    app, err := vov.NewApp(vov.AppConfig{RequireGlobals: []vov.Readiness{deps}, ...})
}

func listTasks(w http.ResponseWriter, r *http.Request) {
    d := deps.Get()
    d.Log.Debug("listing")
}

Get is typed, so nothing is asserted at the call site and a wrong field is a compile error. What it does not do — and cannot — is put a handler's dependencies in its signature: reaching for a holder is invisible to a reader of the declaration. That is the price of the standard handler shape.

A Global is safe for concurrent use. Handlers may call Get on every request; it takes a read lock and returns the stored value.

func NewGlobal added in v0.1.0

func NewGlobal[T any]() *Global[T]

NewGlobal returns an empty holder. Populate it with Global.Set once the value exists — typically after configuration has been loaded and before NewApp is called.

func (*Global[T]) Describe added in v0.1.0

func (g *Global[T]) Describe() string

Describe names the held type, for error messages. It satisfies Readiness.

func (*Global[T]) Get added in v0.1.0

func (g *Global[T]) Get() T

Get returns the stored value. It panics if Set has not been called, rather than handing back a zero value that would fail later as a nil dereference somewhere less obvious.

Pass the holder to AppConfig.RequireGlobals to turn that panic into a construction error instead, caught before the server ever listens.

func (*Global[T]) Ready added in v0.1.0

func (g *Global[T]) Ready() bool

Ready reports whether Set has been called. It satisfies Readiness.

func (*Global[T]) Set added in v0.1.0

func (g *Global[T]) Set(v T)

Set stores v. It panics if called twice: a global swapped while requests are in flight would let two requests see different worlds, and every legitimate use sets it exactly once during start-up.

type Middleware added in v0.1.0

type Middleware func(http.Handler) http.Handler

Middleware wraps an http.Handler, following the standard net/http idiom. It is declared as data — inside a MiddlewareStack — rather than pre-applied to the handler, so that what wraps a route stays readable off the declaration.

type MiddlewareStack added in v0.1.0

type MiddlewareStack struct {
	// Pre runs outside the auth guard, outermost first, for every request to the
	// endpoint — including the ones the guard rejects. That is what keeps a 401
	// logged and stamped. Request ids, logging, panic recovery, CORS, and rate
	// limiting belong here; so does anything an endpoint needs while declaring
	// [NoAuth], since Post does not run there.
	Pre []Middleware

	// Post runs inside the auth guard, outermost first, so it can read the user
	// with [UserFrom]: audit logging, tenant scoping, per-user rate limits.
	//
	// It runs only on endpoints that require auth. An endpoint declaring
	// [NoAuth] has no user, so Post is skipped for it — a stack shared between
	// protected and open endpoints keeps working, but middleware an open
	// endpoint depends on must live in Pre.
	Post []Middleware
}

MiddlewareStack is one named combination of middleware, split by the seam where the request's user becomes known.

Stacks are declared once in [AppConfig.Stacks] and selected by name from an endpoint. Real services converge on a handful of combinations — public, authenticated, admin, webhook — so naming them puts each combination in one place and turns "what wraps this route?" into a word a reviewer can read off the declaration.

type Readiness added in v0.1.0

type Readiness interface {
	Ready() bool
	Describe() string
}

Readiness is anything that can report whether it has been populated. A *Global satisfies it, which is what lets AppConfig.RequireGlobals check holders without knowing the type inside them — the reason none of vov's other types need a type parameter.

type Route added in v0.1.0

type Route struct {
	// Path is the URL path pattern, using net/http ServeMux syntax, e.g.
	// "/projects/{projectId}/rounds". It must be non-empty and begin with "/".
	Path string

	// Endpoints are the methods this URL answers. Declaring none is a
	// construction error: a Route that serves nothing is a mistake, not a
	// placeholder.
	Endpoints Endpoints
}

Route binds one URL to the Endpoints that serve it.

type Server added in v0.1.0

type Server struct {

	// DisableGeneralOptionsHandler, if true, passes "OPTIONS *" requests to the Handler,
	// otherwise responds with 200 OK and Content-Length: 0.
	DisableGeneralOptionsHandler bool

	// TLSConfig optionally provides a TLS configuration for use
	// by ServeTLS and ListenAndServeTLS. Note that this value is
	// cloned by ServeTLS and ListenAndServeTLS, so it's not
	// possible to modify the configuration with methods like
	// tls.Config.SetSessionTicketKeys. To use
	// SetSessionTicketKeys, use Server.Serve with a TLS Listener
	// instead.
	TLSConfig *tls.Config

	// default: 15 * time.Second
	// ReadTimeout is the maximum duration for reading the entire
	// request, including the body. A zero or negative value means
	// there will be no timeout.
	//
	// Because ReadTimeout does not let Handlers make per-request
	// decisions on each request body's acceptable deadline or
	// upload rate, most users will prefer to use
	// ReadHeaderTimeout. It is valid to use them both.
	ReadTimeout *time.Duration

	// default: 10 * time.Second
	// ReadHeaderTimeout is the amount of time allowed to read
	// request headers. The connection's read deadline is reset
	// after reading the headers and the Handler can decide what
	// is considered too slow for the body. If zero, the value of
	// ReadTimeout is used. If negative, or if zero and ReadTimeout
	// is zero or negative, there is no timeout.
	ReadHeaderTimeout *time.Duration

	// default: 15 * time.Second
	// WriteTimeout is the maximum duration before timing out
	// writes of the response. It is reset whenever a new
	// request's header is read. Like ReadTimeout, it does not
	// let Handlers make decisions on a per-request basis.
	// A zero or negative value means there will be no timeout.
	WriteTimeout *time.Duration

	// default: 60 * time.Second
	// IdleTimeout is the maximum amount of time to wait for the
	// next request when keep-alives are enabled. If zero, the value
	// of ReadTimeout is used. If negative, or if zero and ReadTimeout
	// is zero or negative, there is no timeout.
	IdleTimeout *time.Duration

	// MaxHeaderBytes controls the maximum number of bytes the
	// server will read parsing the request header's keys and
	// values, including the request line. It does not limit the
	// size of the request body.
	// If zero, DefaultMaxHeaderBytes is used.
	MaxHeaderBytes int

	// MaxHeaderValueCount controls the maximum number of header
	// values that the server is willing to parse from a request.
	// If zero, DefaultMaxHeaderValueCount is used.
	// Note that comma-separated values in a single header line are
	// counted once, while values sent as multiple header lines are
	// counted multiple times.
	MaxHeaderValueCount int

	// TLSNextProto optionally specifies a function to take over
	// ownership of the provided TLS connection when an ALPN
	// protocol upgrade has occurred. The map key is the protocol
	// name negotiated. The Handler argument should be used to
	// handle HTTP requests and will initialize the Request's TLS
	// and RemoteAddr if not already set. The connection is
	// automatically closed when the function returns.
	// If TLSNextProto is not nil, HTTP/2 support is not enabled
	// automatically.
	//
	// Historically, TLSNextProto was used to disable HTTP/2 support.
	// The Server.Protocols field now provides a simpler way to do this.
	TLSNextProto map[string]func(*http.Server, *tls.Conn, http.Handler)

	// ConnState specifies an optional callback function that is
	// called when a client connection changes state. See the
	// ConnState type and associated constants for details.
	ConnState func(net.Conn, http.ConnState)

	// ErrorLog specifies an optional logger for errors accepting
	// connections, unexpected behavior from handlers, and
	// underlying FileSystem errors.
	// If nil, logging is done via the log package's standard logger.
	ErrorLog *log.Logger

	// BaseContext optionally specifies a function that returns
	// the base context for incoming requests on this server.
	// The provided Listener is the specific Listener that's
	// about to start accepting requests.
	// If BaseContext is nil, the default is context.Background().
	// If non-nil, it must return a non-nil context.
	BaseContext func(net.Listener) context.Context

	// ConnContext optionally specifies a function that modifies
	// the context used for a new connection c. The provided ctx
	// is derived from the base context and has a ServerContextKey
	// value.
	ConnContext func(ctx context.Context, c net.Conn) context.Context

	// HTTP2 configures HTTP/2 connections.
	HTTP2 *http.HTTP2Config

	// Protocols is the set of protocols accepted by the server.
	//
	// If Protocols includes UnencryptedHTTP2, the server will accept
	// unencrypted HTTP/2 connections. The server can serve both
	// HTTP/1 and unencrypted HTTP/2 on the same address and port.
	//
	// If Protocols is nil, the default is usually HTTP/1 and HTTP/2.
	// If TLSNextProto is non-nil and does not contain an "h2" entry,
	// the default is HTTP/1 only.
	Protocols *http.Protocols

	// DisableClientPriority specifies whether client-specified priority, as
	// specified in RFC 9218, should be respected or not.
	//
	// This field only takes effect if using HTTP/2, and if no custom write
	// scheduler is defined for the HTTP/2 server. Otherwise, this field is a
	// no-op.
	//
	// If set to true, requests will be served in a round-robin manner, without
	// prioritization.
	DisableClientPriority bool
}

A Server defines parameters for running an HTTP server. The zero value for Server is a valid configuration.

func (*Server) ToNetHTTPServer added in v0.1.0

func (s *Server) ToNetHTTPServer(addr string, handler http.Handler) *http.Server

type User added in v0.1.0

type User interface {
	// IsAuthenticated reports whether the request carries a valid principal.
	IsAuthenticated() bool

	// HasRole reports whether the user holds the named role.
	HasRole(role string) bool

	// HasPermission reports whether the user holds the named permission.
	HasPermission(permission string) bool

	// Tier reports the user's paid tier: 0 for free, and higher numbers for
	// higher subscriptions. It is ordinal, so an endpoint declaring
	// [Endpoint.MinTier] admits every tier at or above it and a subscription
	// upgrade never removes access.
	//
	// A model with nothing to sell returns 0, which is honest and fail-closed:
	// an endpoint that later gates on tier refuses rather than opening.
	Tier() int
}

User is the principal a request acts as. It is behavioral and deliberately tiny: vov asks it yes-or-no questions and never reads its storage. Capability lives on the model — the app decides what a role or a permission means — while policy lives on the endpoint declaration, where it can be read and reviewed.

A model that does not use roles or permissions answers false, which is both honest and fail-closed: an endpoint that later requires one is refused rather than accidentally opened.

Every method is called on the user its Authenticator returned, once per request. A model whose answers need I/O — a paid-until check, a permission table — should resolve them while it is being built and answer from memory, so a route requiring two permissions does not become two queries.

func UserFrom added in v0.1.0

func UserFrom(ctx context.Context) (User, bool)

UserFrom returns the User resolved for the request, and whether there was one. Handlers on endpoints that require auth can rely on ok being true.

vov intentionally returns the interface rather than a generic concrete type: the guard only ever asks yes/no questions, so only the interface belongs here. Apps that want their own type back wrap this in a one-line typed accessor.

Directories

Path Synopsis
mcp module

Jump to

Keyboard shortcuts

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