Add API Middleware
Add Chio middleware to a FastAPI, Express, Go, Spring Boot, or ASP.NET Core service so the kernel decides each request first.
On this page
Chio's framework middleware asks the kernel about each request before your route handler runs. A refused request returns 403 from inside your service; an admitted one continues with its receipt id attached.
The middleware lives in your application and sends each request to a local Chio evaluator over HTTP. Larkfield's pricing service in FastAPI comes first: refuse a quote, authorize it, and read the three decisions. Then wire the same middleware into your own framework.
StartThe middleware holds each request until the kernel answers
The middleware sends the method, route, headers, and a hash of the body to the evaluator. The evaluator's kernel checks the capability and policy, signs the decision, and the middleware verifies that receipt before it lets the request continue.
κcapabilityσscopegguardsBbudget
Admission runs before your handler
The middleware sends the method, route, headers, and body hash to the kernel before the handler runs. In Python and Node it calls a separate evaluator and verifies the decision receipt; the Rust tower layer embeds the evaluator. An allow continues to the handler, which validates and saves the note. HTTP Framework Middleware.
Python, TypeScript, Go, JVM, and .NET middleware all call a separate evaluator. The Rust chio-tower layer embeds the evaluator in your process instead; HTTP Framework Middleware runs that path.
1. Start the governed FastAPI service
Starts from chio new pricing-python --example pricing-python, the project Python HTTP Frameworks walks through. Every terminal below starts in its directory.
chio new pricing-python --example pricing-python && cd pricing-python
uv sync
uv run uvicorn fastapi_app:app --host 127.0.0.1 --port 8091The service listens on port 8091. Its ASGI middleware sends requests to the evaluator on port 9098 and excludes /healthz.
Before you begin
You need the chio CLI (curl -fsSL https://chio.computer/install.sh | sh), Python 3.11 or newer, uv, and jq.
2. Start the evaluator beside it
In a second terminal, start chio api protect on port 9098. The middleware calls it for each decision.
chio --authority-seed-file .pricing/authority.hex api protect \
--upstream http://127.0.0.1:8091 \
--spec openapi.yaml --listen 127.0.0.1:9098 \
--receipt-store .pricing/receipts.dbChio creates the signing seed and receipt store and reuses them on restart. Send application requests to port 8091, where the middleware intercepts them; port 9098 hosts the evaluator and the local operator endpoints.
3. Read, then watch a quote stop
In a third terminal, check the service's health and read its quotes.
curl -sS -i http://127.0.0.1:8091/healthzHTTP/1.1 200 OK
content-type: application/json
{"status":"ready","framework":"fastapi"}curl -sS -i http://127.0.0.1:8091/v1/quotesHTTP/1.1 200 OK
content-type: application/json
x-chio-receipt: 01a12ef0-d1a4-7c3e-9b7d-5f0c2a61e8b4
{"quotes":[]}You should see the health check answer directly and the read carry an x-chio-receipt header. Now ask for a quote. POST /v1/quote has side effects, so the middleware refuses it before the quote handler runs.
curl -sS -i http://127.0.0.1:8091/v1/quote \
-H 'Content-Type: application/json' -d '{"sku":"sku-301","percent":80}'HTTP/1.1 403 Forbidden
content-type: application/json
{"error":"CapabilityGuard","message":"side-effect route requires a capability token","status":403}4. Grant a capability and repeat the write
As the local operator, issue the five-minute grant http-grant.json describes for Basalt's buying agent, and keep the capability in this directory.
curl -sS http://127.0.0.1:9098/v1/capabilities/mint \
-H 'Content-Type: application/json' --data-binary @http-grant.json \
| jq .capability > capability.jsonThe grant covers the evaluator's HTTP operation on any route, with no count of quotes. Keep this operator endpoint on loopback; a shared deployment puts it behind its own authenticated control boundary. Send the quote again with the capability.
curl -sS -i http://127.0.0.1:8091/v1/quote \
-H 'Content-Type: application/json' \
-H "X-Chio-Capability: $(jq -c . capability.json)" \
-d '{"sku":"sku-301","percent":80}'HTTP/1.1 201 Created
content-type: application/json
x-chio-receipt: 01a12ef0-d2f7-7a10-8c44-0e93b5d17a62
{"id":1,"sku":"sku-301","percent":80.0,"price":"28.00","currency":"USD"}You should see 201 with USD 28.00 and a new receipt id in x-chio-receipt.
5. Read the three decisions
Read the evaluator's store. On a fresh store these requests produce three receipts: the allowed read, the refused quote, and the authorized quote. The health check produces none.
python3 receipts.py01a12ef0-d1a4-7c3e-9b7d-5f0c2a61e8b4 ALLOW GET /v1/quotes HTTP 200 01a12ef0-d22b-7f06-a1c5-7e48d93b20aa DENY POST /v1/quote HTTP 403 01a12ef0-d2f7-7a10-8c44-0e93b5d17a62 ALLOW POST /v1/quote HTTP 200
You should see each decision with its route. The authorized quote's row shows HTTP 200 although FastAPI answered 201: the evaluator signs before the handler runs, so the receipt records the admission decision. Pass a receipt id to receipts.py to print the complete signed record; HTTP receipt lookup explains how the HTTP receipt links to its kernel record.
Your state is .pricing/receipts.db, .pricing/authority.hex, and capability.json. Stop both services with Ctrl-C when you finish, and keep the signing seed private. python3 run.py check runs an isolated copy of this flow and shuts down its services.
6. Wire the middleware into your own service
The pricing service runs in five more frameworks against one contract: GET /healthz outside Chio, GET /v1/quotes allowed with a receipt, and POST /v1/quote refused without a capability. Each delegates evaluation to a local evaluator. Add the interception point for your framework.
const app = express();
app.use(chio({ sidecarUrl: process.env.CHIO_SIDECAR_URL ?? 'http://127.0.0.1:9099', skip: ['/healthz'] }));
app.use(express.json());
// ...routes...
app.use(chioErrorHandler);app.add_middleware(
ChioASGIMiddleware,
config=ChioASGIConfig(
sidecar_url=os.environ.get("CHIO_SIDECAR_URL", "http://127.0.0.1:9098"),
max_body_bytes=8192,
exclude_paths=frozenset({"/healthz"}),
exclude_methods=frozenset(),
),
) protected := chio.Protect(router, chio.WithSidecarURL(sidecar)) @Bean
fun chioFilter() = FilterRegistrationBean<ChioFilter>().apply {
filter = ChioFilter(ChioFilterConfig(
sidecarUrl = System.getenv("CHIO_SIDECAR_URL") ?: "http://127.0.0.1:9102"
))
addUrlPatterns("/v1/*")
order = Ordered.HIGHEST_PRECEDENCE + 1
}builder.Services.AddChioProtection(options => {
options.MaxRequestBodyBytes = 8192;
options.SidecarUrl = Environment.GetEnvironmentVariable("CHIO_SIDECAR_URL")
?? "http://127.0.0.1:9102";
});
var app = builder.Build();
var store = new QuoteStore(".pricing/dotnet-quotes.json");
app.UseWhen(context => context.Request.Path.StartsWithSegments("/v1"), branch => {
branch.UseChioProtection();
});You should see your framework's health route answer directly and a quote without a capability return 403. Set the sidecar URL explicitly or supply CHIO_SIDECAR_URL, and give the evaluator your OpenAPI document when you start it. The complete services, with their dependencies and launchers, are on the Node, Python, Go and C++, and JVM and .NET pages.
Middleware and the gateway refuse the same requests in different places
In both modes a refused request never reaches your business logic: the middleware returns 403 with the same structured ChioErrorResponse body the gateway emits. The difference is where the interception point lives and what you deploy.
| Aspect | chio api protect gateway | Framework middleware |
|---|---|---|
| Deploy shape | Self-contained process in front of your service | One package in your service |
| Extra hop | The proxy forwards to the upstream | An evaluation call to the evaluator; none for embedded Rust |
| Language | Any service that speaks HTTP | An SDK for your framework |
| Policy source | OpenAPI plus x-chio-* extensions | The same policy through the evaluator, plus route decorators |
| Best for | Services you cannot modify; edge gateways | Services that can take a Chio dependency |
Importing middleware keeps the evaluator and its network hop, except on the Rust path. Protect an API runs the gateway mode end to end.
Middleware packages and receipts
Try chio-django, X-Chio-Receipt-Id, chio_requires, or drogon.
Where the receipt lands
Each adapter exposes the receipt through a response header or the framework's request object.
| Package | Entry point | Receipt access |
|---|---|---|
chio-asgi | Chio | Response header named by receipt_header, default X-Chio-Receipt |
chio-django | Chio | request., a dict whose id is the receipt id |
@chio- | chio(), chio | req.; narrow the tagged verdict with isAllowed() |
@chio- | fastify. | request., the Evaluate; caller identity is the receipt's caller_ |
chio-go-http | chio.Protect | Response header X- |
chio- | ChioFilter | Response header X-; configure under the chio prefix in application. |
Backbay. | Add, Use | Response header X-; namespace Backbay.Chio |
chio.Protect wraps any http.Handler, so chi, gorilla/mux, Echo, and Gin wire through their adapters. Spring Boot uses the world.chio:chio-spring-boot filter artifact; the JVM client lives in world.chio:chio-sdk-jvm.
Per-route enforcement in FastAPI
The ASGI middleware governs the whole app. chio-fastapi adds decorators and dependency injection for a route that names the capability it needs: chio_requires declares the grant, and get_caller_identity and get_chio_receipt hand the handler what the evaluator computed. The excerpt assumes your application defines run_search and installs the middleware that supplies the evaluated request context.
from fastapi import FastAPI, Request
from chio_fastapi import chio_requires
app = FastAPI()
@app.post("/search")
@chio_requires(server_id="search-srv", tool_name="search_documents")
async def search(request: Request, query: str) -> dict:
return {"results": run_search(query)}A request with no capability gets HTTP 401 and the body error code CHIO_CAPABILITY_REQUIRED, asserted in sdks/python/chio-fastapi/tests/test_decorators.py. Stack chio_approval above chio_requires to hold a write above a monetary threshold for an approval token that a person issues.
Other frameworks
- TypeScript.
@chio-protocol/fastify,@chio-protocol/elysia, and@chio-protocol/node-http, the framework-agnostic base the other three build on. - Python.
chio-django;chio-asgifor Starlette, Litestar, and other ASGI frameworks;chio-fastapifor per-route decorators. - C++.
chio-drogon, linked as a CMake subdirectory and attached per route aschio::drogon::ChioMiddleware. - Rust.
chio-towerships atower::Layer, so axum and anything built ontower::Servicewires the same way, with the evaluator in your process.
Each SDK reference covers its full API: TypeScript, Python, Go, JVM, .NET, and Rust.
Install the middleware
Each middleware installs from its language registry: go get github.com/backbay-labs/chio/sdks/go/chio-go-http, implementation("world.chio:chio-spring-boot:0.1.0") in build.gradle.kts, and dotnet add package Backbay.Chio.Middleware. Install an SDK lists every package.