Skip to main content

Architecture

featherbit runs three concerns inside one process: the data plane (the HTTP listener that serves client traffic), the admin API (a separate axum server with the embedded UI), and the hot-reload watcher. All three share one state object.

Request flow

HTTP request
→ server::listener matches a route (first match wins, in config order)
→ builds a Context from the request
→ CompiledGraph::execute() walks the policy's nodes
following declared output-port edges (success/outcome/error)
→ the final Context.response is sent to the client

In detail:

  1. The data-plane server buffers the request body and scans the route table for the first route whose match rule (path, method, headers, host) accepts the request. Unmatched requests get a JSON 404.
  2. A fresh Context is built: request populated from the incoming request, response empty (status_code 0), message and errors empty.
  3. The route's compiled graph executes: starting at the entry node, each plugin runs and the walk follows the output port its result names — success on a normal Ok, the plugin's own declared outcome port (e.g. denied, redirect) on a deliberate alternate result, or its error edge (or the catch-all handler) on failure — until a terminal client node is reached. See Policies and graphs and Error handling.
  4. The resulting Context.response is converted to an HTTP response. A status_code of 0 (never set by any node) is treated as 200.

Shared state and locking

SharedState is wrapped in an Arc and cloned into every server task. It holds:

FieldContents
systemImmutable system.yaml config, fixed for the process lifetime
gatewayCurrent gateway.yaml config behind a RwLock, mutated by admin API CRUD
routesRwLock-protected route table: each route paired with an Arc<CompiledGraph> of its policy, in declaration order
config_pathPath to gateway.yaml, needed for reload-from-disk
metricsProcess-wide Prometheus registry

The locking model keeps the request path cheap:

  • The data plane only takes short read locks on routes, and only while matching the request. The lock is released before graph execution starts, so a slow upstream call never blocks a config reload.
  • Write locks are taken only by the admin API and the hot-reload paths, when swapping in a freshly compiled route table.
  • Route recompilation never happens on the request path.

Policy compilation: validate → compile → swap

Every configuration load follows the same three steps:

  1. Validate — each policy's graph structure is checked (validate_policy): listener and client nodes present, edges reference existing nodes, no duplicate inputs, no orphans. All violations are collected and reported together.
  2. Compile — each valid policy is turned into a CompiledGraph with instantiated plugin objects and edges indexed by source port. Policies shared by multiple routes are compiled once and shared via Arc.
  3. Swap — the new route table replaces the old one under a write lock.

This runs at startup (the process exits on invalid config — fail fast), after admin API mutations (reload), and when the file watcher or POST /api/config/reload re-reads gateway.yaml from disk (reload_from_disk). A failed reload has no side effects: if parsing, validation, or compilation fails, the existing route table stays in place and traffic keeps flowing on the last good configuration.

Module map

ModuleResponsibility
src/main.rsEntry point, CLI flags, startup orchestration, logging init
src/config/YAML loading, ${ENV_VAR:-default} interpolation, config structs
src/context/The Context object (request, response, message, errors)
src/graph/engine.rsCompiles PolicyConfig into CompiledGraph; executes the node walk
src/graph/validation.rsStructural validation of policy graphs before compilation
src/routing/Path/method/header/host route matching
src/plugins/native/Built-in plugins
src/plugins/script/Lua scripting runtime, Context↔Lua marshalling
src/server/Data-plane HTTP listener, request dispatch
src/admin/Admin API (axum), Basic Auth middleware, embedded UI serving
src/metrics/Prometheus metrics registry
src/hot_reload/File watcher triggering reload on gateway.yaml changes
src/state.rsSharedState: lock-protected config and compiled route table

Configuration files

  • system.yaml — listener bind/port, timeouts, logging, admin API settings. Loaded once at startup.
  • gateway.yaml — routes and policies. Hot-reloaded on change.

All YAML values in both files support ${ENV_VAR:-default} interpolation — system.yaml resolves at load time, gateway.yaml keeps the placeholders in the stored config (so the Admin API and UI never see resolved secrets) and resolves them as policies compile. See Configuration.

TLS termination (with hot-reloaded certs, mTLS, and SNI), HTTP/2, WebSocket proxying, L4 TCP/UDP stream proxying, etcd-backed clustering, and graceful shutdown are all implemented — see the TLS, stream, and deployment guides. The remaining gaps are tracked honestly on the roadmap.