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 (
X-Principal-Id).get_principal_idtrusts the header. Absent:Settings.require_authenticated_principalcontrols fallback (False =SYSTEM_PRINCIPAL_ID; True = 401). Production MUST front with an auth proxy that verifies credentials, strips client-supplied headers, and sets the verified UUID. - 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.