Starlette for the HTTP Adapter (not FastAPI)
Context and Problem Statement
The project needs a driving HTTP adapter — the component that receives HTTP requests, extracts credentials, runs authentication and authorization through the existing domain services, and proxies requests to an upstream service. This is the "Phase 3" that makes the proxy usable.
Which ASGI framework should implement this adapter? The two candidates are Starlette (lightweight ASGI toolkit) and FastAPI (built on Starlette, adds dependency injection, validation, and OpenAPI).
Decision Drivers
- The proxy is a transparent reverse proxy — it intercepts all HTTP methods on all paths and forwards to upstream
- No request body validation or response model serialization is needed
- The proxy does not expose its own API — it proxies upstream's API
- Streaming (request and response bodies) is important for large payloads
- Authentication and authorization are handled by existing domain services via Protocols (
Authenticator,Authorizer) - Dependency injection already uses constructor injection in an application factory (
create_app()), following the project's established pattern - The project prioritizes minimal dependencies and avoids unnecessary abstractions
Considered Options
- Starlette — lightweight ASGI toolkit with routing, requests, responses, middleware
- FastAPI — full web framework built on Starlette with dependency injection, OpenAPI, validation
Analysis: stac-auth-proxy as FastAPI reference
stac-auth-proxy is a production FastAPI-based auth proxy for STAC APIs. Analyzing its architecture reveals how FastAPI is used in a proxy context:
| Aspect | stac-auth-proxy approach | Relevance to hdc-auth-proxy |
|---|---|---|
| OpenAPI | Disabled (openapi_url=None), proxies upstream's schema instead |
We don't need OpenAPI — no API to document |
FastAPI Depends() |
Not used for auth — auth is raw ASGI middleware | We use domain services via constructor injection |
| Response streaming | Disabled — full response buffered for body transformation (link rewriting, CQL2 filtering) | We need streaming — no body transformation |
| Middleware | Raw ASGI __call__ (not BaseHTTPMiddleware) |
Same pattern needed regardless of framework |
| Swagger UI | Separate route proxying upstream's OpenAPI | Not needed |
| Health endpoints | include_router() for /healthz |
Can be simple Starlette routes |
Key insight: stac-auth-proxy uses FastAPI but doesn't use its main features. It disables OpenAPI, skips Depends(), and implements middleware at the raw ASGI level. FastAPI's primary value in that project is the TestClient and the ability to compose with external FastAPI apps via configure_app(). The response buffering (no streaming) is required by their middleware stack that transforms JSON bodies — a requirement we don't have.
Analysis: fastapi-opa PR #81 as PKCE reference
fastapi-opa PR #81 adds PKCE (RFC 7636) and cookie-based sessions to a FastAPI/OPA middleware library. Relevant patterns for Phase 4:
| Aspect | fastapi-opa approach | Relevance to hdc-auth-proxy |
|---|---|---|
| Auth interface | ABC (AuthInterface) with inheritance |
We use Protocols — no forced inheritance |
| PKCE store | PKCEStoreProtocol with atomic retrieve-and-delete |
Good pattern — will adopt as async output port |
| Cookie session | CookieAuthMiddleware wraps OPAMiddleware (tight coupling) |
Independent composable middleware layers |
| Domain purity | authenticate() reads request.query_params, builds RedirectResponse |
Strict separation: HTTP adapter → domain service → HTTP adapter |
| HTTP client | Synchronous requests.get/post in async context |
httpx.AsyncClient for all HTTP I/O |
| Config | OIDCConfig with 18+ fields (God object) |
Focused frozen dataclasses: OIDCProviderConfig, PKCEConfig, ClientConfig |
| Result branching | isinstance(result, dict) for backward compat |
Single typed AuthenticationResult return |
Key insight: The PKCE flow and PKCEStoreProtocol patterns are sound and will be adopted for Phase 4 (OIDC browser login), but with async interfaces, hexagonal separation (domain service doesn't import Starlette), and focused configuration objects.
Decision Outcome
Chosen option: Starlette, because the proxy use case does not benefit from FastAPI's features, while Starlette provides everything needed with less overhead.
Rationale
What the proxy needs:
- Catch-all route matching all HTTP methods and paths → Route("/{path:path}", handler)
- Request/response objects → starlette.requests.Request, starlette.responses.StreamingResponse
- Lifespan management (httpx client setup/teardown) → @asynccontextmanager lifespan
- Simple header parsing for credential extraction → pure functions on Request
What FastAPI adds that we don't need:
- OpenAPI auto-generation → no API routes to document (the proxy is transparent)
- Pydantic request/response validation → no body to validate (forwarded as-is)
- Dependency injection via Depends() → constructor injection in create_app() is sufficient and already the project pattern
- Typed path/query parameters → single catch-all route with no parameters
Streaming advantage: Starlette with StreamingResponse + httpx.AsyncClient enables true bidirectional streaming. Responses flow directly from upstream to client without buffering. stac-auth-proxy sacrifices this for response body transformation — a tradeoff we don't need to make.
Implementation
create_app(ProxySettings) -> Starlette
└── Route("/{path:path}", ProxyHandler.handle)
├── extract_credential(request) → Credential | None
├── authenticator.authenticate(credential) → AuthenticationResult
├── authorizer.authorize(access_request) → AccessResult
└── upstream_client.forward(request) → StreamingResponse
The ProxyHandler depends on Authenticator and Authorizer Protocols (input ports), not on specific implementations. All wiring happens in the create_app() factory.
Consequences
- Good, because no unnecessary framework overhead for a transparent proxy
- Good, because true bidirectional streaming is possible (no response buffering)
- Good, because Starlette is already an indirect dependency (via httpx)
- Good, because the architecture stays clean — the HTTP adapter is a thin translation layer
- Neutral, because Starlette's test utilities (
httpx.ASGITransport) are equivalent to FastAPI'sTestClient - Bad, because adding future admin endpoints (health, metrics) requires either Starlette routes or mounting a FastAPI sub-app
When to reconsider
This decision should be revisited if: - The proxy needs to expose its own API endpoints with typed request/response models - Response body transformation is required (link rewriting, content filtering) - The proxy evolves into an API gateway with per-route configuration and OpenAPI documentation - Integration as a library into external FastAPI apps becomes a requirement
In these cases, migrating to FastAPI would be additive (wrap the existing Starlette app) rather than a rewrite.
Confirmation
- The HTTP adapter uses
starlette.applications.Starletteas the ASGI app - No FastAPI import exists anywhere in the codebase
ProxyHandlerdepends only onAuthenticatorandAuthorizerProtocols- Response streaming works end-to-end (upstream → client without buffering)
More Information
- Starlette documentation
- stac-auth-proxy — FastAPI-based reference implementation analyzed for comparison
- fastapi-opa PR #81 — PKCE + cookie session patterns analyzed for Phase 4 planning
- Related: ADR-0006 Sync-First Protocols (partially superseded by ADR-0014), ADR-0010 Cognito JWT Adapter
- Affected files:
src/hdc_auth_proxy/adapters/http/