Skip to content

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:

  • /health is 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/v2 and 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.

Overseer Backend modal — Routes tab grouping endpoints by OpenAPI tag

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_app calls include_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).
Was this page helpful?