HTTP API
The site’s backend API is OpenAPI 3.0, and the specification is generated from the routes themselves rather than maintained beside them.
The tiers
Section titled “The tiers”Every route lives under /api/v1/<tier>/, and the tier decides the gate.
| Tier | Routes | Sits behind |
|---|---|---|
admin |
~93 | noindex, isLoggedIn, requireRole('admin','editor','moderator') |
auth |
~38 | Public by necessity; heavily rate-limited and bot-scored |
player |
~24 | noindex, requireAuth — role-agnostic self-service |
public |
~19 | Nothing, by design |
settings |
2 | requireAuth + noindex, no role gate |
Plus two outside the versioned surface: /api/health and /api/csp-report.
Those two are deliberately not under /api/v1. A browser learns the CSP report path from the
policy header rather than from a client build, so it is not part of the versioned client
contract.
Two things the tier table implies
Section titled “Two things the tier table implies”player is role-agnostic. It is self-service for whoever is signed in, gated on
requireAuth alone and never on “is not staff”. Staff are a superset of players — an
administrator has characters too, and a player route that excluded them would 403 an admin
off their own account.
A module’s routes inherit their tier’s gate and add their own on top. A module never re-implements the tier gate; see The module API.
Authentication
Section titled “Authentication”Three ways in, one session model:
- Cookie —
httpOnlyJWT, for the browser. - Bearer — short access tokens plus rotated, hashed, revocable refresh tokens, for the native app.
- SSO — OAuth2/OIDC with PKCE, and link-only: an external identity must already be attached to an existing account.
Admin roles are re-validated against the database on every request, so a demoted user loses access immediately rather than at token expiry.
The sidecar’s API is a different thing
Section titled “The sidecar’s API is a different thing”The uo-link sidecar exposes its own small REST and WebSocket surface, reached only by the
website’s backend. It carries X-UOLink-Version and answers 409 on a protocol mismatch.
It is not part of this API and is not served from your site. See The bridge.
Keeping the spec current
Section titled “Keeping the spec current”For contributors: the specification is generated from #swagger.* annotations next to each
route, and the output is committed.
cd website/server && npm run swaggerA route that is not in the specification is not finished. Modules publish their own fragment, at a fixed filename in the bundle root, so a module’s routes appear in the same documentation as core’s.
Canonical document
Section titled “Canonical document”BACKEND_DESIGN.md
§4 is the API contract, including §4.0’s authoritative route list.