Routes
Unlike middleware and lifecycle hooks,
routers in Aegis are explicitly registered. There is exactly one
place in the backend where every public route is wired up:
app/components/backend/api/routing.py.
That asymmetry is intentional. Middleware and hooks are infrastructure that services drop in; the public surface of the application should be visible in a single registry so you can audit it at a glance.
The Registry
include_routers(app) is called by create_backend_app() after
middleware discovery. The template version reads roughly:
# app/components/backend/api/routing.py
def include_routers(app: FastAPI) -> None:
app.include_router(health.router, prefix="/health", tags=["health"])
# Worker (if include_worker)
app.include_router(worker.router, prefix="/api/v1")
app.include_router(task_history.router, prefix="/api/v1")
app.include_router(worker_events.router, prefix="/events", tags=["events"])
# Scheduler (if scheduler_backend != "memory")
app.include_router(scheduler.router, prefix="/api/v1")
# Auth (if include_auth)
app.include_router(auth_router, prefix="/api/v1")
# OAuth (if include_oauth)
app.include_router(oauth_router, prefix="/api/v1")
# Orgs (if include_auth_org)
app.include_router(org_router, prefix="/api/v1")
# AI service routers (if include_ai)
app.include_router(ai_router)
app.include_router(voice_router, prefix="/api/v1") # ai_voice
app.include_router(llm_router, prefix="/api/v1") # ai_backend != memory + ollama
app.include_router(rag_router, prefix="/api/v1") # ai_rag
# Other service routers
app.include_router(comms_router, prefix="/api/v1") # include_comms
app.include_router(insights_router, prefix="/api/v1")
app.include_router(payment_router, prefix="/api/v1") # include_payment
app.include_router(payment_pages_router)
app.include_router(blog_router, prefix="/api/v1") # include_blog
# Plugin routers
for router in plugin_routers:
app.include_router(router, ...)
(The actual file is a Copier template; the if include_* gates are
Jinja conditionals, so a generated project only contains lines for the
services it enabled.)
A few conventions to notice:
/healthis always present. It carries no version prefix because the health surface is used by orchestrators (Docker, Kubernetes, Overseer) that should not have to know about API versions./health/is a constant-time liveness probe: it answers 200 without walking any component, which is all a load balancer or container healthcheck needs. Component status lives at/health/detailed, backed by a shared status cache so the dashboard, the scheduler and the probe never trigger more than one full walk every ten seconds between them.- Versioned APIs live under
/api/v1. Every service-owned router mounts there. When the API contract changes incompatibly, the next router goes under/api/v2and lives alongside the old one until consumers migrate. - Tags are set at registration time when the inner router does not
set them. The auth, AI, payment, and similar routers bring their
own
tags=declarations because their internal grouping is more nuanced than a single top-level tag. - Worker events mount at
/events. Server-Sent Events are not a versioned REST surface.
Adding A Router
The pattern is the one already used by every service:
1. Define the router.
# app/components/backend/api/reports.py
from fastapi import APIRouter
from app.services.reports import generate_weekly_summary
router = APIRouter()
@router.get("/reports/weekly")
async def weekly() -> dict[str, object]:
return await generate_weekly_summary()
2. Register it explicitly.
# app/components/backend/api/routing.py
from app.components.backend.api import reports
def include_routers(app: FastAPI) -> None:
# ... existing registrations ...
app.include_router(reports.router, prefix="/api/v1", tags=["reports"])
Restart the backend. The route appears in the OpenAPI schema at
/openapi.json, in Swagger at /docs, and in Overseer's Routes tab.
Inspecting Routes In Overseer
The Backend modal's Routes tab renders the full registered surface grouped by OpenAPI tag. Each route card shows:
- HTTP method badge (color-coded by method).
- Path with parameter slots highlighted.
- Response model name (if declared).
- Dependency chain (
Depends(...)callables that resolve before the endpoint runs). - A security badge when the endpoint requires authentication.
Calling a route
Expand a route and it shows a request form built from its OpenAPI
operation, the same schema /openapi.json and /docs serve: path and
query parameters as typed fields (required ones marked, enums as a list,
defaults filled in), the body as JSON pre-filled with an example from its
model, and who to sign in as (admin, user or anonymous). Execute sends
the request once, in the server itself, and shows the curl command, the
status, how long it took, the response body and its headers, as FastAPI's
/docs does. A route that writes (POST, PUT, PATCH, DELETE) is sent only
once you tick the box under it. Execute's requests are not load-test runs
and stay out of their history.
Load test this opens Server > Load Tests with the route and the values you filled in. Load Tests' route picker shows this same form, with requests and clients added, so a route is filled in the same way on both pages.
Dry path walks the route without sending it: every hop it can take,
read from the code. It starts at the handler and the dependencies FastAPI
injects, then follows each call through the service it is handed, its own
methods, the tables a query names (reads and writes), the components a
core client fronts, work handed to the queue (and on into the task, marked
as running on the worker) and the outside providers whose keys the code
holds. Each hop is set in under the one that makes it and opens at its line
in Overseer > Code; a hop under an if shows the condition, never
evaluated. Where the walk cannot see further it stops at that line and says
why: a parameter with no type, a function handed in as a value, a chain
past what it follows, a registry it cannot read, or more than 12 calls deep.
ORM lazy loads and cascades, raw text() SQL, middleware and events are
not seen by reading the code, and the page says so. Show on the map
opens the Server with the parts the route crosses lit. The button shows
only where Overseer > Code does (dev, or OVERSEER_CODE_ENABLED): it is
the code's map.
Because the Routes tab reads from cached metadata built by the
component_health startup hook, anything you add at runtime (rare, but
possible via the FastAPI app object) will not appear until the next
restart. Anything registered through include_routers shows up
automatically.
Standard Endpoints Provided By FastAPI
Aegis does not strip FastAPI's defaults:
/openapi.json: machine-readable schema./docs: Swagger UI./redoc: ReDoc UI.
These are excluded from Logfire's HTTP instrumentation along with
/health/* and /dashboard/*. See
middleware -> Logfire Tracing.
Reference
app/components/backend/api/routing.py: the registry above.app/components/backend/api/health.py: the health endpoints, always present.app/components/backend/main.py:create_backend_appcallsinclude_routers(app)right after middleware discovery.- Service-owned routers live under
app/components/backend/api/<service>/for services that ship multiple sub-routers (auth, AI, payment, comms, blog, insights, orgs).
