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:
- 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.
- A fresh Context is built:
requestpopulated from the incoming request,responseempty (status_code0),messageanderrorsempty. - The route's compiled graph executes: starting at the entry node, each plugin runs and the walk follows the output port its result names —
successon a normalOk, 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 terminalclientnode is reached. See Policies and graphs and Error handling. - The resulting
Context.responseis converted to an HTTP response. Astatus_codeof0(never set by any node) is treated as200.
Shared state and locking
SharedState is wrapped in an Arc and cloned into every server task. It holds:
| Field | Contents |
|---|---|
system | Immutable system.yaml config, fixed for the process lifetime |
gateway | Current gateway.yaml config behind a RwLock, mutated by admin API CRUD |
routes | RwLock-protected route table: each route paired with an Arc<CompiledGraph> of its policy, in declaration order |
config_path | Path to gateway.yaml, needed for reload-from-disk |
metrics | Process-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:
- 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. - Compile — each valid policy is turned into a
CompiledGraphwith instantiated plugin objects and edges indexed by source port. Policies shared by multiple routes are compiled once and shared viaArc. - 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
| Module | Responsibility |
|---|---|
src/main.rs | Entry 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.rs | Compiles PolicyConfig into CompiledGraph; executes the node walk |
src/graph/validation.rs | Structural 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.rs | SharedState: 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.