Skip to main content

Health & metrics

The API exposes two health probes, a bearer-gated Prometheus endpoint, and a per-client rate limiter. Every response — healthy or not — carries a fixed set of security headers, and every error is returned as an RFC 7807 problem document.

Health probes

Two probes are registered on the FastAPI app, both with include_in_schema=False so they do not appear in the public OpenAPI document:
apps/api/src/neobank/main.py
Liveness confirms the process is up; readiness confirms it can serve a real request. Wire /health/live to a liveness probe and /health/ready to a readiness probe in Kubernetes or your load balancer — they are exempt from the rate limiter (see below), so probe traffic does not consume any client’s budget.

Prometheus metrics

The endpoint returns the default Prometheus exposition. Two series are emitted by the request-context middleware: The endpoint is gated by a static bearer token. If NEOBANK_METRICS_TOKEN is unset, or the Authorization header does not match Bearer <token>, the endpoint returns 404 rather than 401 — it does not advertise that it exists:
apps/api/src/neobank/main.py
Behind the single-server Caddy edge and the shipped Helm ingress, /internal/* is not routed publicly. On Kubernetes, only networkPolicy.metricsNamespace may reach API port 8000 in addition to the ingress controller namespace. Scrape from that namespace with the 32-character-or-longer bearer token. On the server, scrape inside the private network or through an authenticated sidecar.

Rate limiting

The rate limiter runs as ASGI middleware ahead of the router. /health/* and /internal/* are exempt; every other request is limited:
apps/api/src/neobank/middleware.py
The limit is keyed first by client address, then by credential (the Authorization header or session cookie). A request consumes budget on both buckets when a credential is present, so a single token cannot bypass the per-IP ceiling and a single IP cannot exhaust a per-token ceiling alone. Uvicorn accepts X-Forwarded-For only from NEOBANK_TRUSTED_PROXY_IPS. The server Compose deployment pins Caddy to 172.30.255.2 and trusts only that address. The Helm value config.trustedProxyIps must match the source address or CIDR of the ingress controller pods; the NetworkPolicy separately limits port 8000 ingress to networkPolicy.ingressNamespace. Never use *, 0.0.0.0/0, or ::/0. A mismatched value causes callers to share the proxy address bucket; an overbroad value lets another network peer spoof client addresses. When a bucket is exhausted the middleware returns a 429 as an RFC 7807 problem document with a Retry-After: 60 header:
apps/api/src/neobank/middleware.py
In secure runtimes the limiter is backed by Redis; in development it falls back to an in-memory counter. The Redis path is what makes the limit coherent across multiple API replicas.

Headers on every response

The request-context middleware attaches five headers to every HTTP response, including errors:
apps/api/src/neobank/middleware.py
  • X-Request-ID — generated per request (or echoed when the caller supplies one); the same value is written to the audit trail and the structured log line. Use it to correlate a user report with logs and an Audit row.
  • X-Content-Type-Options: nosniff — prevents MIME sniffing on responses.
  • Referrer-Policy: same-origin — the API origin is not leaked as a referrer.
  • Permissions-Policy — denies camera, microphone, and geolocation to the API origin. The WebAuthn entries for app.perflo.ai live on the web app’s edge headers, not here; see Perflo hosted origins.
  • Cache-Control: no-store — API responses are never cached.
The web app’s edge (apps/web/security-headers.conf) adds the CSP and the WebAuthn Permissions-Policy entries in addition to these.

Errors are problem documents

Every error — validation failures, auth rejections, provider errors, rate limits — is serialized as application/problem+json with the ProblemDetails shape (type, title, status, detail, instance, code, request_id, retryable, submission_uncertain). The retryable and submission_uncertain flags tell callers whether to retry and whether a retried financial submission could double-spend; see Confirmation & idempotency.