Troubleshooting

Target reader: developers debugging local Alpha setup, SDK payloads, and API calls.

Start from the failing boundary: Godot payload generation, local queue, public API, attachment upload, admin API auth, or Docker Compose dependencies.

Minimal example

cd ../echo-nebula-infra
./scripts/smoke-public-api.sh
./scripts/smoke-admin-api.sh

curl -i "http://localhost:4100/healthz"
curl -i "http://localhost:4100/readyz"
curl -i "http://localhost:4000/healthz"
curl -i "http://localhost:4000/readyz"

curl -fsS "http://localhost:4000/v1/sdk/config?locale=en" \
  -H "X-Project-Key: enb_pk_xxx"

Use feedback_payload_ready only to observe preparation status and a field count as in First feedback. This signal does not confirm delivery. Check queue item counts locally and HTTP status, stable error codes and the response request ID; confirm received reports and attachments in Developer Console. Never print or attach full payloads, queued JSON, descriptions, contacts, logs, keys, tokens or signed URLs. Submission consent does not authorize copying player data into diagnostic logs.

Treat /healthz and /readyz differently. A successful health response means the process is alive, not that PostgreSQL or object storage works. Admin 200 degraded means PostgreSQL is ready but object storage is degraded; text management can continue while attachment operations may fail. Public requires both dependencies and returns 503 unavailable when either fails. Use ./scripts/e2e-readiness-isolated.sh when you need repeatable outage-and-recovery evidence without touching the default Compose project.

Common mistakes

Next step

Return to the Alpha overview and verify the end-to-end order.

Resource and configuration boundaries

Public config reads and both write endpoints share a process-local, pre-authentication peer-IP budget (default 600 requests per minute). Invalid or missing Project Keys also consume it. Authenticated writes retain a separate project budget (default 60 per minute, shared across its Keys). A 429 response uses common.rate_limited and Retry-After; wait before retrying. Forwarded/X-Forwarded-For headers are ignored. Behind a proxy, all traffic from that peer shares the budget: enforce per-client limits at the ingress and configure the service budget for that deployment. Counters are not shared across replicas. Each limiter retains at most 4096 active identities and rejects new identities while full; expiration restores capacity.

Attachment filenames must contain 1–255 Unicode code points, valid UTF-8 and no control characters; dot/dot-dot basenames are rejected with 400 attachment.invalid. Path components are removed and unsupported characters become underscores in the stored name. File content remains capped at 10 MiB by the contract; smaller deployment limits still apply. Rejected filenames do not create objects. Existing consent, project isolation and idempotency rules apply to every retry.

The Admin API caps JSON at 64 KiB, with the existing 16 KiB authentication/organization caps retained. Oversized, incomplete or malformed bodies return the existing route-specific 400 error. Feedback search accepts at most 200 Unicode code points before trimming; invalid UTF-8 or longer input returns 400 admin.request.invalid. The UI checks the same code-point limit.

Both API processes reject malformed, zero, negative or excessive resource settings at startup. Defaults: Public read-header/read/write/idle/shutdown timeouts are 5s/2m/2m/2m/10s; Admin uses 5s/15s/30s/1m/10s. Timeouts are positive, capped at 1m for headers/shutdown, 5m for read/write/idle and 30s for readiness. Rate windows are positive and at most 1h; counts are 1–1,000,000. Download TTL is positive and at most 1h (default 15m). Public feedback body bytes are 1–1,048,576, attachment bytes 1–10,485,760, recent-log counts 1–200, breadcrumbs 1–100 and contact characters 1–200. Use project feature flags to disable optional data.

Outside APP_ENV=local, explicitly configure DATABASE_URL, S3_ENDPOINT, S3_REGION, S3_BUCKET and non-default S3 credentials; database passwords and S3 secrets require at least 16 bytes. PostgreSQL uses a URI with one explicit sslmode. Public database hosts require verify-full; private IPs, single-label service names and .internal/.localhost hosts may use disable, require or verify-ca on an operator-controlled network. Query parameters cannot override database identity, host or credentials. S3 may use HTTP only on those private hosts, otherwise HTTPS. Admin additionally requires an HTTPS S3_PUBLIC_ENDPOINT, strong non-default JWT/bootstrap credentials, and HTTPS origins for any explicit browser CORS allowlist. Empty CORS lists enable no cross-origin browser access. Local defaults remain available; do not reuse them outside local. Configuration errors identify fields without printing values. These checks do not constitute a production deployment or multi-replica protection.