API Conventions
One page of rules shared by every module, so the per-module docs stay short.
Base URLs
- Backend origin:
https://shanebackend-production.up.railway.app https://shanejli.com/api/*rewrites to the same backend (afterFiles rewrite), so relative/api/...URLs work from the site's own pages.
Errors
Every error is JSON {"error": "message"}. Validation failures (bad params, query, or body) return 400 with the zod error object. Unhandled throws return 500 {"error":"Internal server error"}.
Auth posture
- Missing/invalid token on a protected route: 401.
- PAT lacking the required scope: 403
Token missing required scope: <scope>. - A row that exists but is not yours: usually 404, not 403 (deliberately indistinct; loans is the exception and 403s).
Wire format
- JSON bodies. Request field names are camelCase in most modules; the journal and comment surfaces use snake_case for multi-word request fields (
parent_comment_id,base_version_num,target_version_num). Responses are camelCase everywhere. - Dates that key resources are
YYYY-MM-DDstrings, validated as real calendar dates (2026-02-30 is a 400, not a 500). - Free-text fields are trimmed before validation; whitespace-only input is a 400.
Pagination
Keyset, newest-first, via limit (1 to 100) and cursor. The cursor value differs by module: the journal cursor is the last entry's DATE (YYYY-MM-DD); trips, loans, tea, scoreboard matches, and rng history use a createdAt ISO timestamp. Read nextCursor from each response; null means done.
Optimistic concurrency (If-Match)
Racy journal mutations (revert, suggestion approve) require an If-Match header carrying the entry's current version number as a plain integer (from GET /api/journal/entries/:date, field currentVersionNum).
- Missing header: 428
- Non-numeric: 400
- Stale: 409 with
{"error":"Version conflict","currentVersionNum":<latest>}so you can rebase and retry.
Rate limits
Per-PAT rolling 60 second buckets (JWT browser sessions bypass); see the bucket table in Auth and Tokens. 429 responses carry Retry-After.
CORS
Allowed request headers are Content-Type, Authorization, If-Match, X-Tea-Pin. A new custom header needs a backend change; the symptom of forgetting is a browser-only "Failed to fetch".