Production hardening, logging, HTTP errors.
Default-secure or refuse to boot. The app declines to start in production unless authentication is required and the DB role is the read-write app role, not the migration owner. Everything else (limits, tracing, log shape) is wired so a missing piece is visible, not silent.
Wired in cora/api/main.py:create_app().
- Body size limit.
BodySizeLimitMiddlewarereturns 413 overSettings.max_request_body_size_bytes(default 1 MiB). Production also enforces at the reverse proxy. - Prometheus
/metrics. Per-appCollectorRegistry(global crashes on a secondTestClient(create_app())). Hidden from its own counters and from OpenAPI. - OpenTelemetry tracing.
Settings.otel_exporter:none/console/otlp. OTLP honoursOTEL_EXPORTER_OTLP_*env vars. Trace context is the source of truth for correlation:current_correlation_id()returnsUUID(int=trace_id). Handler spans viawith_tracinginwire.py; name<bc>.<command|query>.<command_name>. - Auth. Three modes, picked in order: (1) bearer-verified principal on
request.state.principal(set byBearerAuthMiddlewarewhenSettings.identity_providersis configured); (2) bearer mode 401 with RFC 6750WWW-Authenticatewhen bearer is required but missing or invalid; (3) legacyX-Principal-Idheader when no IdPs are configured (production MUST front with a verifying proxy that strips client-supplied headers and sets the verified UUID). Absent on the legacy path:Settings.require_authenticated_principalcontrols fallback (False →SYSTEM_PRINCIPAL_ID; True → 401). Token introspection unavailability surfaces as 503 +Retry-After: 5. - Production startup gate. Refuses to boot if
app_env in {"prod","production"}ANDrequire_authenticated_principal=False. Opt in:APP_ENV=prod,REQUIRE_AUTHENTICATED_PRINCIPAL=true,DATABASE_URL=postgresql://cora_app:.../cora. - DB role separation.
cora_apphas SELECT + INSERT onevents+entries_*; UPDATE/DELETE/TRUNCATE revoked. Migrations run as the database owner.proj_*tables get full DML.
Two patterns:
- Handlers:
<verb>.<event>(register_actor.start,register_actor.denied,register_actor.success). Every handler emitsstartplusdeniedorsuccess. Decider failures propagate as exceptions. - Cross-cutting:
<concern>.<event>(idempotency.cache_hit,body_size_limit.rejected).
Field names:
correlation_id: request correlation (str-cast UUID)causation_id: command handlers only; upstream event id (nullfor HTTP/MCP root). Always emitted.principal_id: calling principal (str-cast UUID)command_name/query_name: dataclass nameactor_id(or<aggregate>_id): aggregate id when in scope. One key per concept.
- In routes:
raise HTTPException(...). FastAPI idiom. - In exception handlers:
return JSONResponse(...). RaisingHTTPExceptioninside a handler creates nested-exception pitfalls (FastAPI guidance).
Routes raise; handlers return. Same JSON shape over the wire.