BuildConnect
Add Chio Middleware to Your HTTP Framework
Add Chio policy enforcement and signed receipts to an existing axum, Express, FastAPI, or net/http service with in-process middleware.
Prerequisites
http.Handler wrappers).Middleware vs Reverse Proxy
Both shapes enforce the same policy with the same kernel and produce the same signed HttpReceipt records. The choice is deployment topology. The middleware path runs the evaluator inside your service process; the proxy path runs it in a separate binary in front of your service.
| Dimension | Middleware (this guide) | Reverse proxy (chio api protect) |
|---|---|---|
| Processes | One. Evaluator inside your service. | Two. Chio in a separate binary in front of the upstream. |
| Network hop | None in Rust; localhost sidecar elsewhere. | One extra hop (client to Chio, Chio to upstream). |
| Code changes | Add a dependency and wire middleware. | None. Upstream is unmodified. |
| Identity context | Rich. Framework-parsed route params, auth state, session handles. | Header-only. Chio sees what is on the wire. |
| Best for | Services you own and can ship with a Chio dependency. | Services you cannot modify, polyglot fleets, multi-tenant edges. |
The rest of this guide walks the middleware path. If the table leans you the other way, head to Protect an API instead.
Rust: axum, warp, tonic via chio-tower
chio-tower ships a tower::Layer that wraps any inner service with chio evaluation. Because axum, tonic, and most modern Rust HTTP stacks build on tower::Service, wiring is identical across them. The crate exports:
ChioLayer— the towerLayeryou wrap your router with.ChioServiceis the innerServiceit produces.ChioEvaluator— holds the kernel keypair, policy hash, identity extractor, route resolver, and fail-open flag. Exposed so you can evaluate directly and return anEvaluationResult(verdict, signedHttpReceipt, guard evidence) outside the middleware.extract_identity/IdentityExtractor— the default header-based extractor and the function type for plugging in your own.ChioTowerError— surfaced when evaluation itself fails; distinct from aDenyverdict, which is a normal 403 response.
A minimal axum example. The layer sits in front of the router so every route inherits the evaluation:
use chio_core_types::crypto::Keypair;
use chio_tower::ChioLayer;
use axum::{routing::get, Router, Json};
use serde_json::json;
#[tokio::main]
async fn main() {
// Stable kernel keypair. In production this comes from a sealed seed
// file or HSM; generate() is fine for local dev.
let keypair = Keypair::generate();
// policy_hash binds this process's receipts to the exact policy
// document that was loaded. Compute once at startup from chio.yaml or
// the OpenAPI spec and pass it here.
let chio = ChioLayer::new(keypair, "sha256:c7e9...02af".to_string());
let app = Router::new()
.route("/pets", get(|| async { Json(json!({ "pets": [] })) })
.post(|Json(b): Json<serde_json::Value>| async move { Json(json!({ "created": b })) }))
.route("/pets/:id", get(|| async { Json(json!({ "pet": {} })) })
.delete(|| async { Json(json!({ "deleted": true })) }))
.layer(chio);
let listener = tokio::net::TcpListener::bind("0.0.0.0:4000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}What the layer does for each request:
- Buffers the body so its SHA-256 hash can be computed and the bytes replayed to your handler. The body type must implement
http_body::BodyplusFrom<Bytes>;axum::body::BodyandFull<Bytes>qualify. - Extracts caller identity via
extract_identity, which checksAuthorization: Bearer,X-Api-Key,Cookie, then falls through to anonymous. Raw values never leave memory; only SHA-256 hashes reach the receipt. - Calls the
HttpAuthorityevaluator with method, path, query, caller, body hash, body length, and any capability token fromX-Chio-Capabilityor thechio_capabilityquery param. - On
Deny, returns the verdict's HTTP status (default 403), setsx-chio-receipt-id, stashes the receipt inresponse.extensions(), and never calls the inner handler. - On
Allow, forwards to the inner service and finalizes the receipt with the response status once the handler returns.
Custom identity and route resolution
The default extractor is header-only. If your service validates JWTs or looks up session cookies against a store, project that richer identity into the receipt by building an evaluator with your own extractor and passing it to ChioLayer::from_evaluator. The with_route_resolver hook on the same builder lets you collapse instance paths to OpenAPI-style templates, which is what route_pattern records in the receipt:
use chio_core_types::crypto::Keypair;
use chio_http_core::CallerIdentity;
use chio_tower::{ChioEvaluator, ChioLayer};
fn tenant_aware_identity(headers: &http::HeaderMap) -> CallerIdentity {
let mut caller = chio_tower::extract_identity(headers);
if let Some(tenant) = headers.get("x-tenant-id").and_then(|v| v.to_str().ok()) {
caller.tenant = Some(tenant.to_string());
}
caller
}
fn route_pattern(_method: &str, path: &str) -> String {
// Match against your router's compiled patterns and return the template.
if let Some(rest) = path.strip_prefix("/pets/") {
if !rest.is_empty() && !rest.contains('/') {
return "/pets/{id}".to_string();
}
}
path.to_string()
}
fn chio_layer() -> ChioLayer {
let evaluator = ChioEvaluator::new(Keypair::generate(), "sha256:c7e9...".into())
.with_identity_extractor(tenant_aware_identity)
.with_route_resolver(route_pattern)
.with_fail_open(false); // fail-closed is the default; make it explicit.
ChioLayer::from_evaluator(evaluator)
}A few constraints worth stating plainly, grounded in the current crate:
- The body is fully buffered before evaluation. That makes body-hash binding and body-aware guards work, but it also means streaming uploads are held in memory up to the collected size. Size caps belong upstream of the layer.
- The
chio-towercrate works with replayable tower body types includingaxum::body::Bodyand bytes-backed HTTP bodies used in generic Tower/HTTP2 tests. Realtonic::body::Bodyreplay is a follow-on concern the current middleware contract does not claim to cover — tonic/gRPC body replay is not yet supported, so there is no qualified path for wrapping a live tonic gRPC service inChioLayertoday. - Identity extractors and route resolvers are plain
fnpointers, not closures. State they need must be passed through headers or compiled into the extractor at build time.
Node, Python, Go: via the Sidecar
Outside Rust, the recommended shape today is a thin framework middleware that talks to a local Chio sidecar over HTTP. The sidecar is the same process you would run for chio api protect, but in evaluation-only mode: it exposes an internal endpoint the middleware posts normalized requests to and receives a verdict plus signed receipt. The kernel keypair stays in the sidecar; your application process never sees it.
The localhost round trip keeps the kernel signing key in a small, separately auditable binary. The sidecar loads the same OpenAPI spec and x-chio-* policy as the reverse proxy, and each evaluator writes to the same receipt store.
Sidecar client, not in-process kernel
chio-tower path above or host your service inside a tower-compatible runtime.Express (Node / TypeScript)
The @chio-protocol/express package exports a chio() middleware and a matching error handler. Both forward requests to a local chio sidecar; the config option points that sidecar at a chio.yaml runtime config. Mount chio() before any governed handlers; denied requests never reach them, allowed ones arrive with req.chioResult populated (verdict, caller identity, signed receipt).
import express from "express";
import { chio, chioErrorHandler } from "@chio-protocol/express";
const app = express();
app.use(
chio({
config: "./chio.yaml",
sidecarUrl: "http://127.0.0.1:9091",
})
);
app.get("/pets", (_req, res) => res.json({ pets: [] }));
app.post("/pets", (req, res) => res.status(201).json({ created: req.body }));
// Turns evaluator errors (not Deny verdicts, which are already 403s)
// into structured ChioErrorResponse bodies.
app.use(chioErrorHandler);
app.listen(4000);FastAPI (Python)
chio-fastapi leans into FastAPI-native patterns: the chio_requires decorator declares the capability a route needs, and dependency injection (get_chio_receipt, get_caller_identity) surfaces the receipt and caller on the handler. There is no ASGI middleware class; enforcement is per-route through the decorator.
from fastapi import FastAPI, Depends
from chio_fastapi import (
chio_requires, chio_approval, get_chio_receipt, get_caller_identity,
)
app = FastAPI()
@app.get("/pets/{pet_id}")
@chio_requires("pets-api", "get_pet", operations=["Invoke"])
async def get_pet(
pet_id: str,
caller = Depends(get_caller_identity),
receipt = Depends(get_chio_receipt),
):
# Handlers under @chio_requires must be async def: the decorator wraps
# them and awaits the inner handler. caller.subject is the SHA-256 hex
# digest of the bearer token, API key, or session cookie value, with no
# method prefix, or the literal "anonymous"; caller.auth_method records
# which signal produced it. receipt.id correlates this response with the
# signed audit record.
return {"pet_id": pet_id, "caller": caller.subject, "receipt_id": receipt.id}
# Stack @chio_approval on top of @chio_requires to gate a write above a
# monetary threshold on an operator-issued approval token.
@app.delete("/pets/{pet_id}")
@chio_approval(threshold_cents=0)
@chio_requires("pets-api", "delete_pet", operations=["Invoke"])
async def delete_pet(pet_id: str):
# threshold_cents=0 always requires approval: the evaluator emits a
# pending-approval receipt and waits for an operator decision.
return {"deleted": pet_id}Go (net/http, Gin, chi)
chio-go-http exposes a Protect function that wraps any http.Handler. Gin, Echo, and chi all expose an http.Handler adapter, so the same wrapper covers them:
package main
import (
"encoding/json"
"net/http"
chio "github.com/backbay-labs/chio/sdks/go/chio-go-http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/pets", func(w http.ResponseWriter, r *http.Request) {
// On allowed requests the middleware sets X-Chio-Receipt-Id on the
// response, so downstream logging can correlate the call to its receipt.
json.NewEncoder(w).Encode(map[string]any{"pets": []any{}})
})
protected := chio.Protect(mux,
chio.ConfigFile("./chio.yaml"),
chio.WithSidecarURL("http://127.0.0.1:9091"),
)
http.ListenAndServe(":4000", protected)
}Keep the kernel out-of-process
What Gets Signed
Every evaluated request produces an HttpReceipt (from chio-http-core). On allowed requests you get two: a decision receipt signed before the handler runs and a final receipt once the response status is known, linked by metadata.chio_decision_receipt_id. Denied requests produce a single final-scope receipt because there is no upstream call to wait on.
{
"id": "01HX7MZW8Q5J3K2ERT9P4A1B6C",
"request_id": "01HX7MZW7N8K2F5QT3P6A1B0VE",
"route_pattern": "/pets/{id}",
"method": "POST",
"caller_identity_hash": "9f4c...e2a1",
"session_id": null,
"verdict": { "verdict": "allow" },
"evidence": [
{
"guard_name": "CapabilityGuard",
"verdict": true,
"details": "valid capability token presented"
}
],
"response_status": 201,
"timestamp": 1745020800,
"content_hash": "8b1c...7d33",
"policy_hash": "c7e9...02af",
"capability_id": "cap-pets-writer",
"metadata": {
"chio_http_status_scope": "final",
"chio_decision_receipt_id": "01HX7MZW8Q5J3K2ERT9P4A1B6B"
},
"kernel_key": "ed25519:f03b...91c2",
"signature": "ed25519:a3b4...c5d6"
}route_patternis the template, not the instance URL. Inchio-tower, supply this viawith_route_resolver; in the sidecar path, the pattern comes from the loaded OpenAPI spec or explicitchio.yamlroute map.caller_identity_hashis a SHA-256 hash of the extracted subject. Bearer tokens and API keys are never stored raw.content_hashcovers the canonicalized method, path, query, and body. Two requests that differ only in body bytes produce different hashes, so the receipt is bound to the exact request that was evaluated.policy_hashfingerprints the policy document that was in effect. Rotating policy changes the hash, which downstream verifiers can detect.
The full schema, including the canonical JSON layout used for signature verification, lives in Receipt Format.
Policy Patterns
Middleware and the proxy derive route and method policy from the OpenAPI spec plus x-chio-* extensions, as described in the Protect an API guide — not from a chio.yaml route block. A few patterns recur:
Route and method allowlist
Only operations present in the spec become tools; anything not enumerated is unknown and denies side effects by default. Narrow a route with x-chio-* so a purely-read POST stays session-scoped and a sensitive route is guarded:
paths:
/pets:
get:
operationId: listPets
responses:
"200": { description: OK }
post:
operationId: createPet
x-chio-side-effects: true # requires a capability token
responses:
"201": { description: Created }
/pets/{id}:
delete:
operationId: deletePet
x-chio-approval-required: true # deny-by-default, operator approval
responses:
"204": { description: Deleted }Body-size caps
The evaluator receives the request body length, so oversized uploads can be rejected before the handler runs. This is a code-level bound, not a spec field: in Rust set it with ChioService::with_max_body_bytes; the non-Rust sidecar exposes an equivalent server-side max_body_bytes ceiling.
Egress per route
When a route itself makes outbound calls (webhook dispatcher, third-party integration), scope the capability's egress grant to a narrow URL pattern. Any unapproved destination denies at the egress guard and leaves a receipt.
Full authoring reference: Write a Policy.
Body-aware guards require buffering
ChioService::with_max_body_bytes in Rust, or the sidecar's max_body_bytes) so a single oversized upload cannot eat your process memory.When to Use Middleware vs the Sidecar Proxy
| Situation | Middleware | chio api protect |
|---|---|---|
| You own the service and ship its binary. | Yes. | Optional. |
| Service is a closed-source third party. | Not available. | Yes. |
| Polyglot fleet, one governance surface. | Per-language wiring. | Preferred. One binary per service. |
| Receipts should reflect post-auth identity. | Strong — sees framework auth state. | Header-only unless your auth is header-based. |
| Need governance without a deploy. | Requires a deploy. | Rollable in front of a running service. |
| Already behind Envoy or a service mesh. | Works, but overlaps the mesh. | Or use Envoy ext_authz. |
A common production shape is both: middleware in services you own,chio api protect in front of the ones you do not, and a single receipt store collecting evidence from both surfaces.
Next Steps
- Architecture · how the kernel, guard pipeline, and receipt store fit together regardless of the request entry point.
- Protect an API · the reverse-proxy counterpart to this guide, for services you cannot modify.
- Write a Policy · HushSpec authoring for capability scopes, approval rules, and guard configuration.
- Envoy ext_authz · run chio as an external authorization service at the mesh layer when middleware is too coupled and a sidecar is too coarse.
- Trust Control Plane · swap local policy, dev keys, and SQLite for hosted equivalents without touching application code.