The public reference at /docs/api describes a selected subset of the existing
/api/v1 catalog. backend/services/public_api_schema.py owns that selection and
small example requests. /api/v1/openapi.json generates the response and parameter
schemas from the actual routes. /openapi.json remains the full application
schema. Discovery links advertise the public schema.
- Confirm the read has no user-specific data or side effects and uses a bounded response. Add a typed response model before documenting an opaque dictionary.
- Add the route and a valid example to
PUBLIC_ENDPOINTS. The public browser middleware matches the actual registered route, not just its URL shape. - Check request parameters and examples against the route. The public search
contract requires explicit
mode=keyword; existing search modes and defaults are unchanged for website and other clients. - Run
backend/tests/test_public_api.py,backend/tests/test_public_api_nginx.py, andfrontend/src/views/ApiDocsView.test.tsx. Verify actual responses and identifiers too: contract tests cannot establish that an example record still exists. - Record planned breaking changes in the
/docs/api#changelogsection (frontend/src/views/ApiDocsView.tsx) with announcement and effective dates at least 14 days apart. Urgent security or restoration fixes are exempt. Do not silently change documented behavior.
Both nginx templates (deploy/nginx/) contain shared per-IP zones: catalog GETs
under /api/v1/(papers|tasks|datasets|evaluations) sustain 120/minute with 60
excess burst slots; list/search GETs additionally sustain 60/minute with 20
excess burst slots. The identity is nginx's $binary_remote_addr, never an
untrusted forwarding header. Production sits behind Cloudflare and the templates
do not set real_ip, so that address is currently a Cloudflare edge IP, not the
visitor. OPTIONS and mutations do not enter these zones.
These are initial operating limits sized from a pre-launch access-log sample, not a capacity guarantee. Follow nginx's limit-exceeded log entries and API latency; adjust rates and bursts together with the docs and avoid production load tests. A rejected request receives JSON, no-store, a two-second Retry-After, and public CORS headers. The fixed, credential-independent error reflects an Origin header and permits credentials so existing browser clients can read the retry instruction; successful responses still use the application CORS policy.
The application CORS middleware grants anonymous cross-origin GET access only to the explicitly selected operations and the public schema. It does not grant credentialed access or change administrative/write CORS rules. Staging's nginx access gate remains in place.
Rate limits and the exact /docs/api location (served by the frontend; the
broad /docs proxy remains for backend docs) live in nginx. Changing them needs
both the application release through pwc-deploy and the checked-in nginx
template applied separately through pwc-nginx for the target environment; an
application deploy alone changes neither. Verify deep-link and footer
navigation, the public schema, an anonymous read with an Origin header, and
normal site browsing.
test_public_api_nginx.py uses disposable directories, ephemeral ports, a
synthetic upstream, and an unprivileged nginx process, including its
configuration check. It does not include host config, contact live services, or
signal a live nginx process.