API Guidelines
REST + OpenAPI 3.1 for all public/frontend APIs. Versioned under /v1. JSON, camelCase,
UTC RFC 3339 timestamps, YYYY-MM-DD dates, UUID ids.
Conventions
- Strict body decoding (unknown fields rejected), 1 MiB cap.
PATCHdistinguishes an absent field (unchanged) from explicitnull(clear).X-Correlation-IDhonored if a valid UUID, else minted; always echoed.- Idempotency keys on non-idempotent mutations (payments/bookings) as those land.
- Per-IP rate limiting on
/v1(token bucket, 20 rps / burst 40) with429+Retry-After, plus narrower limiters on specific surfaces (contact relay, report creation, tag scan, certificate verify, WebAuthn login, telemetry ingest, kiosk attestation, places search).
Errors — RFC 7807
{
"type": "https://buddora.dev/problems/validation",
"title": "Validation failed",
"status": 400,
"detail": "name: must not be empty",
"instance": "/v1/animals",
"correlationId": "8f1c..."
}
Authorization is decided server-side (OpenFGA), and enforced again in the database by
row-level security under a NOBYPASSRLS runtime role. The frontend never decides access.
A caveat on rate limiting
Until 2026-08-28 every "per client IP" limiter was in practice one global bucket: the API
sits behind Traefik and read RemoteAddr, which is the proxy's address for every caller.
API_TRUSTED_PROXY_CIDRS fixes this, and the fix is live and confirmed by measurement — but
two limits remain, and a client should not be written as though they do not:
- The buckets are still per process. There is no Redis-backed shared limiter, so limits do not hold across replicas.
identitytagsbuilds a 1 rps scan limiter and never calls it, soGET /v1/tags/{token}is constrained only by the 20 rps surface limiter. This is recorded in ADR-030 as an open fault, not a design choice.
See ADR-030 and Security Baseline.