Skip to main content

Troubleshooting

API won't start: "dev auth bypass must not be enabled". API_DEV_AUTH_BYPASS=true with API_ENV != development. Set the bypass to false in staging/production.

/readyz returns 503. Postgres is unreachable — check DATABASE_URL, network, and that migrations ran.

401 on /v1/... in development. Provide the X-Debug-Actor JSON header (dev only) or a valid bearer token in production.

A portal's login goes nowhere and its logs show nothing. Almost always a missing Keycloak redirect URI: Keycloak rejects an unregistered redirect_uri on its own error page, so the container passes its healthcheck and the deployment looks entirely healthy while that one portal has no working login. All eight portals' callbacks must be registered on the realm.

invalid_request: Missing parameter: code_challenge_method on sign-in. Expected since 2026-08-28: PKCE S256 is now enforced at the realm rather than merely sent. A client that does not send a challenge is refused. Similarly, a password-grant (ROPC) call now answers unauthorized_client: Client not allowed for direct access grants — the grant was disabled deliberately. scripts/synthetic-journey.sh is a known casualty.

429 responses. Rate limit exceeded; back off and retry (honor Retry-After).

If you are getting 429s you did not earn, check API_TRUSTED_PROXY_CIDRS on the api service before tuning the limits. Without it the API sees only the reverse proxy's address, so every "per client IP" limiter is one global bucket shared by the whole internet, and one noisy client throttles everybody (ADR-030). The API logs a warning naming the variable at every startup while it is unset; when it is set, the startup line reads trusted proxies: X-Forwarded-For honoured from these blocks only. Note that setting it in Coolify is not sufficient on its own — Compose forwards only what it names, so the variable must also appear under the api service in the compose file.

Buckets are still per process; a Redis-backed shared limiter for multi-instance does not exist yet.

Docs build fails on a broken link. onBrokenLinks: throw is intentional — fix the link so documentation never silently diverges. This is a real check and it runs in apps/docs/Dockerfile, so a broken internal link stops that image from building.