Vapora/adrs/adr-003-axum-backend.ncl
Jesús Pérez 75e5ebd9a2
Some checks failed
Documentation Lint & Validation / Markdown Linting (push) Has been cancelled
Documentation Lint & Validation / Validate mdBook Configuration (push) Has been cancelled
Documentation Lint & Validation / Content & Structure Validation (push) Has been cancelled
mdBook Build & Deploy / Build mdBook (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Documentation Lint & Validation / Lint & Validation Summary (push) Has been cancelled
mdBook Build & Deploy / Documentation Quality Check (push) Has been cancelled
mdBook Build & Deploy / Deploy to GitHub Pages (push) Has been cancelled
mdBook Build & Deploy / Notification (push) Has been cancelled
chore: ontology sync + 4 NCL ADRs + landing page update
on+re:
  - core.ncl: 5 new Practice nodes (notification-channels,
    vapora-capabilities, agent-hot-reload-stable-identity,
    merkle-audit-trail, notification-channels) + 5 new edges;
    knowledge-graph-execution-history updated with HNSW+BM25+RRF
  - state.ncl: production-readiness blocker/catalyst updated (hot-reload
    complete, BudgetManager/LLMRouter still require restart);
    ontoref-integration catalyst updated (vapora-ontology/reflection
    crates, api-catalog.json, nickel contracts)

  ADRs (NCL):
  - adr-013: KG hybrid search — HNSW+BM25+RRF, rejected in-process scan
  - adr-014: capability packages — AgentDefinition→vapora-shared,
    DashMap shard-before-await constraint
  - adr-015: Merkle audit trail — SHA-256 hash chain, rejected HMAC
  - adr-016: agent hot-reload — stable_id=role, learning_profiles survive
    drain, BudgetManager excluded from reload scope

  landing page:
  - 2 new feature boxes: VCS-Agnostic Worktree (jj/git), Ontology Protocol
  - KG box: 20→28 tests, HNSW+BM25+RRF description
  - Agents box: 71→82 tests, hot-reload + stable_id
  - tech stack: Rust 21→23 crates, added jj, Radicle, ontoref badges
  - status badge: 620→691 tests
2026-04-07 21:06:48 +01:00

78 lines
4.2 KiB
XML

let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-003",
title = "Axum as the Sole HTTP Framework for vapora-backend",
status = 'Accepted,
date = "2024-11-01",
context = "vapora-backend exposes 40+ REST endpoints plus WebSocket connections for real-time updates. The framework choice determines the async model, middleware composition, and extractor ergonomics for the entire API surface. As of 2026-03-27, axum 0.8.8 is in use (the markdown ADR references 0.8.6, which is stale). The Tower ecosystem is the chosen middleware stack.",
decision = "Axum is the only HTTP framework allowed in vapora-backend. No Actix-Web, Rocket, or Warp. All HTTP handlers use Axum extractors and Router composition. Middleware uses the Tower layer model. WebSocket upgrades use axum::extract::ws.",
rationale = [
{
claim = "Axum is Tokio-native with zero abstraction over the async runtime",
detail = "vapora runs a Tokio multi-threaded runtime. Axum's Handler trait is directly implemented over Tokio futures — no bridging layer, no actor model overhead. This eliminates the async runtime impedance mismatch that Actix-Web introduces.",
},
{
claim = "Tower middleware composes predictably with all Axum routes",
detail = "CorsLayer, TraceLayer, CompressionLayer, and authentication middleware are composed via ServiceBuilder, not framework-specific macros. This means middleware ordering is explicit, testable, and portable to other Tower-based services.",
},
{
claim = "Type-safe extractors eliminate runtime deserialization panics",
detail = "Json<T>, Path<T>, State<T> extractors fail at compile time if the handler signature doesn't match — not at runtime. This catches API contract violations before deployment.",
},
],
consequences = {
positive = [
"All API surface is tested via axum-test's TestClient without a real TCP socket",
"Tower middleware applies uniformly to all routes, including WebSocket upgrade paths",
"Adding new endpoints requires only adding a handler fn and a route entry — no boilerplate registration",
"IntoResponse impl on VaporaError provides consistent error serialization across all handlers",
],
negative = [
"Axum's 0.8.x API introduced breaking changes from 0.7 (extractor signatures, Router typing) — upgrades require wholesale migration",
"Axum lacks built-in request body size limiting — must be added via RequestBodyLimitLayer",
],
},
alternatives_considered = [
{
option = "Actix-Web",
why_rejected = "Actor model adds coordination overhead not needed for stateless API handlers. Different async patterns from Tokio primitives make integration with NATS JetStream and SurrealDB clients awkward.",
},
{
option = "Rocket",
why_rejected = "Synchronous-first design. async support was added as an afterthought, leading to executor boundary issues in Tokio-native code.",
},
],
constraints = [
{
id = "axum-only-http-framework",
claim = "vapora-backend must not import actix-web, rocket, or warp",
scope = "vapora-backend",
severity = 'Hard,
check = { tag = 'Cargo, crate = "vapora-backend", forbidden_deps = ["actix-web", "rocket", "warp"] },
rationale = "Multiple HTTP frameworks in one binary create conflicting async executor registrations, duplicated middleware chains, and inconsistent error serialization.",
},
{
id = "all-handlers-via-axum-router",
claim = "All HTTP endpoints must be registered via axum::Router — no raw hyper service registration",
scope = "vapora-backend/src/main.rs, vapora-backend/src/api/",
severity = 'Hard,
check = { tag = 'Grep, pattern = "axum::Router", paths = ["crates/vapora-backend/src/main.rs"], must_be_empty = false },
rationale = "Bypassing the Axum router skips middleware layers (tracing, CORS, auth) applied at the Router level.",
},
],
related_adrs = ["adr-002"],
ontology_check = {
decision_string = "axum 0.8.x is the sole HTTP framework in vapora-backend; Tower middleware stack; no actix-web/rocket/warp",
invariants_at_risk = [],
verdict = 'Safe,
},
}