Skip to main content

ADR 0017: Pluggable ASGI Middleware Stack

Status

Accepted

Context

The Framework M web layer is built on Litestar and uses ASGI middleware for cross-cutting concerns: authentication, rate limiting, CSRF protection, locale resolution, metrics, and now recent-document tracking for the Desk UI.

Previously, all middleware was hardcoded in framework_m_standard.adapters.web.app:create_app(). Adding a new middleware required editing the central application factory, which:

  • Violates the "replaceable batteries" design goal.
  • Creates merge conflicts when multiple apps or plugins contribute middleware.
  • Makes it impossible for a deployment to disable or replace a default middleware without forking code.

Decision

We will manage the middleware stack through a singleton MiddlewareRegistry with the following properties:

  1. Priority-based ordering: Each middleware declares a numeric priority; lower values run first (outer layer).
  2. Entry-point discovery: Any package can contribute middleware by declaring it under framework_m.middleware in pyproject.toml.
  3. Name-based overrides: Entry points with the same name override earlier registrations (last-wins).
  4. Config-driven overrides: framework_config.toml can enable/disable, re-prioritize, or pass extra kwargs to any registered middleware.
  5. Built-in registration in create_app(): Middlewares that need runtime-constructed dependencies are still registered programmatically, but they participate in the same priority-ordered stack.

Consequences

Positive

  • New middleware can be added without touching app.py.
  • Apps and MX packages can replace default middlewares cleanly.
  • Middleware order is explicit and configurable.
  • Deployments can disable features like recent-document tracking via config.

Negative

  • The registry is a singleton, so tests must clear it to remain deterministic.
  • Built-in middlewares with runtime dependencies still require code changes in create_app(); they cannot yet be fully declarative.
  • Entry-point discovery depends on package installation, which can be surprising in editable-development setups if the package metadata is stale.

Alternatives considered

Keep hardcoding middleware in app.py

Rejected because it centralizes change and conflicts with the plugin architecture.

Register each middleware in a bootstrap step

Rejected because it is still a one-off registration per middleware. Entry points are more declarative and align with the existing adapter-override pattern.

Use Litestar's built-in middleware config only

Rejected because Litestar's configuration does not support package discovery or deployment overrides through framework_config.toml.

Implementation notes

  • MiddlewareRegistry lives in framework_m_standard.adapters.web.middleware_registry.
  • create_middleware_stack() is the single source of truth for building the Litestar middleware list.
  • RecentDocumentMiddleware is the first middleware contributed through the new entry-point group.