# Infrastructure wiring
Active contributors: Saksham, Ravi
The infrastructure layer is the composition root: it builds the FastAPI app, wires lifespan startup and shutdown, registers middleware, exception handlers, and routes, constructs the two MCP HTTP sub-apps, and owns the shared APScheduler singleton. `app/factory.py` is a thin entry point that delegates to `app/infrastructure/`. Everything that needs to happen once at process start (or once at process end) lives here.
## Directory layout
```
app/
├── factory.py # Thin composition root (create_app)
├── infrastructure/
│ ├── lifespan.py # Startup/shutdown orchestration
│ ├── middleware.py # Middleware registration
│ ├── errors.py # Exception handlers
│ ├── mcp.py # MCP HTTP app construction (lazy)
│ ├── routing.py # Route and mount registration
│ ├── scheduler.py # Shared AsyncIOScheduler singleton
│ └── request_context.py # Re-export of request ID helpers from core/logging
└── middleware/
├── security.py # SecurityHeaders, RequestID, RequestLogging
├── rate_limit.py # RateLimitMiddleware + EndpointRateLimiter
├── cache_control.py # CacheControlMiddleware
└── trailing_slash.py # StripTrailingSlashMiddleware
```
## Key abstractions
| Abstraction | Location | Purpose |
|---|---|---|
| `create_app(testing)` | `app/factory.py` | Builds the FastAPI instance, OpenAPI tags, lifespan, middleware, MCP apps |
| `create_lifespan(testing, user_mcp_app, admin_mcp_app)` | `app/infrastructure/lifespan.py` | Returns the FastAPI lifespan context manager |
| `register_middleware(app, testing)` | `app/infrastructure/middleware.py` | Adds CORS, rate limit, security, cache control, trailing slash, request ID, request logging |
| `register_exception_handlers(app)` | `app/infrastructure/errors.py` | Registers handlers for 401, 403, `BaseAPIException`, `HTTPException`, `ValueError`, `Exception` |
| `register_routes(app, user_mcp_app, admin_mcp_app)` | `app/infrastructure/routing.py` | Mounts REST, WS, share, OAuth well-known, MCP apps |
| `build_mcp_http_apps()` | `app/infrastructure/mcp.py` | Returns lazy user/admin MCP ASGI proxies |
| `get_scheduler()` / `start_scheduler()` / `shutdown_scheduler()` | `app/infrastructure/scheduler.py` | Shared `AsyncIOScheduler` singleton |
## How it works
### Startup
```mermaid
graph TD
Start["create_app(testing)"] --> Build["build_mcp_http_apps
LazyMCPHTTPApp user + admin"]
Build --> App["FastAPI(lifespan=..., openapi_tags=OPENAPI_TAGS)"]
App --> RegMW["register_middleware
CORS → rate limit → security → cache control → trailing slash → request ID → logging"]
App --> RegErr["register_exception_handlers"]
App --> RegRoutes["register_routes
api_router, ws, share, oauth, /mcp, /mcp-admin"]
Lifespan["lifespan(app)"] --> Cache["initialize_cache"]
Lifespan --> Migrations["_apply_pending_migrations
ALTER TYPE / ADD COLUMN IF NOT EXISTS"]
Lifespan --> DNS["_prewarm_supabase_dns
getaddrinfo against SUPABASE_URL"]
Lifespan --> Sched["_register_scheduler_jobs
blog, notifications, vector sync, data hub"]
Sched --> StartSched["start_scheduler"]
```
The lifespan context manager wraps the inner MCP app lifespans. When not in testing mode, startup runs four steps in order: initialize the cache, apply lightweight one-off DDL that cannot go through Supabase CLI migrations (enum value adds, column adds), prewarm Supabase DNS by resolving `SUPABASE_URL` once so misconfigured DNS surfaces in startup logs, and register scheduler jobs on the shared `AsyncIOScheduler` before starting it. In serverless mode (`SERVERLESS_ENABLED=True`), scheduler registration is skipped to allow scale-to-zero.
The startup migrations are idempotent `ALTER TYPE ... ADD VALUE IF NOT EXISTS` and `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` statements for changes that the Supabase CLI migration workflow cannot apply (Postgres enum extension is one example). Each statement is wrapped in a try/except that logs a warning on failure without blocking startup.
### Shutdown
Shutdown runs in reverse: stop the scheduler, close cached AI provider HTTP clients, close the FCM and SMS shared clients, close all shared httpx clients (`app/core/http.close_all_clients`), shut down the notification thread pool, disconnect the cache, and dispose both DB engines (main and background). Each step is wrapped in a try/except so a failure in one shutdown step does not prevent the rest from running.
### Middleware
Middleware is registered in a specific order because Starlette executes middleware in reverse registration order (bottom-up). CORS is outermost (first inbound, last outbound). Inside CORS sits the rate limiter (500 req/min per IP global, with tighter per-route limits via `EndpointRateLimiter`). Innermost are the security headers, cache control, trailing slash, request ID, and request logging middleware. In development or testing mode CORS allows `*` without credentials; in production it uses `settings.CORS_ORIGINS` with credentials.
### Exception handlers
`register_exception_handlers` installs handlers for 401 and 403 that add `WWW-Authenticate: Bearer resource_metadata=...` headers on MCP routes (so MCP hosts trigger OAuth), a `BaseAPIException` handler that emits the standardized `{error: {code, message, details}}` shape, an `HTTPException` handler that preserves OAuth error payloads, a `ValueError` handler (422), and a catch-all `Exception` handler that logs to Sentry and returns a generic 500 (leaking the exception string only when `DEBUG` is on).
### MCP HTTP apps
`build_mcp_http_apps` returns two `LazyMCPHTTPApp` instances. These are ASGI proxies that build the concrete MCP app on first request, so the heavy `mcp` and widget registration imports stay off the app import path. The concrete builder registers ChatGPT widgets, wires `BearerAuthBackend` with the `SupabaseTokenVerifier` (requiring `mcp:read` and `mcp:write` scopes), and creates the FastMCP HTTP app with `stateless_http=True`. The inner MCP lifespans are entered when the parent app lifespan runs.
### Shared scheduler
`app/infrastructure/scheduler.py` holds a single module-level `_scheduler` (an `AsyncIOScheduler` with timezone `Asia/Kolkata`). `get_scheduler()` creates it lazily, `start_scheduler()` starts it once, and `shutdown_scheduler()` stops and clears it. All background jobs (blog auto-publish, notifications, vector sync, data hub scrapers) register on this one instance via their `start_*_scheduler` functions, which are called from `_register_scheduler_jobs` in lifespan. No module creates its own scheduler.
## Integration points
- **Core** supplies the cache, DB engines, HTTP clients, and logging that lifespan initializes and shuts down. See [core-cross-cutting](systems--core-cross-cutting.md).
- **Services** register schedulers via `start_*_scheduler` functions called from lifespan. See [services-layer](systems--services-layer.md).
- **MCP servers** are mounted by `register_routes` and lazily built by `build_mcp_http_apps`. See [features/mcp-servers](features--mcp-servers.md).
- **Middleware** in `app/middleware/` is registered by `register_middleware`.
## Entry points for modification
- New middleware: implement it in `app/middleware/`, add it in `register_middleware` at the right position (remember execution is reverse-order).
- New background job: write a `start_*_scheduler(app)` function that calls `get_scheduler().add_job(...)`, then call it from `_register_scheduler_jobs` in `app/infrastructure/lifespan.py`.
- New startup migration: add a `(label, sql)` tuple to `_apply_pending_migrations`.
- New exception shape: subclass `BaseAPIException` in `app/core/exceptions.py`; the existing handler will serialize it.
## Key source files
| File | Role |
|---|---|
| `app/factory.py` | App factory, OpenAPI tags |
| `app/infrastructure/lifespan.py` | Startup/shutdown orchestration, startup migrations, DNS prewarm |
| `app/infrastructure/middleware.py` | Middleware registration order |
| `app/infrastructure/errors.py` | Exception handlers, MCP `WWW-Authenticate` injection |
| `app/infrastructure/mcp.py` | Lazy MCP HTTP app construction |
| `app/infrastructure/routing.py` | REST, WS, share, OAuth, MCP mounts |
| `app/infrastructure/scheduler.py` | Shared `AsyncIOScheduler` singleton |
| `app/middleware/security.py` | Security headers, request ID, request logging |
| `app/middleware/rate_limit.py` | Global + per-route rate limiting |