Files
docs/website/API_V2_SKELETON.md
wtclaude 5de5e19445 docs(website): finalize the API plan — in-place router split, no /api/v2
The API v2 plan is revised down to the work that is actually justified: a CSP
hardening pass and an in-place domain split of the monolithic route wiring.

- Auth merge (httpOnly cookies -> bearer + rotating refresh for every client) is
  removed and re-filed as deferred behind trigger conditions. httpOnly+SameSite
  is the stronger model, session.service.js already unifies cookie and bearer,
  the SSO/PKCE transaction cookies survive any merge, and it dragged the admin
  SSE fetch/ReadableStream rewrite along as a dependency for no user-visible
  payoff. A revival must first spec refresh-token reuse detection and a rollback
  procedure.
- No parallel /api/v2. The URL surface is already grouped by capability, so each
  new router file mounts at the prefix it already owns and every URL stays
  byte-identical. No dual mount, no per-route migration, no v1 retirement; the
  SPA, Discord bot, and Android app are all untouched. API_V2_SKELETON.md is
  marked superseded (kept as the recipe if a versioned API is ever forced).
- The /api/mobile facade and app-version floor are deferred with the revival note
  that it starts as a one-line alias mount, not ~70 hand-written delegates. The
  M11 milestone is dropped from android/PLAN.md.
- Adds PR 0: a generated route manifest, so "every URL is unchanged" is proved by
  a zero-line diff rather than asserted in review. The baseline
  api-route-inventory.json (199 API routes + 2 internal) is committed here and is
  what PR 0's generator must reproduce byte-for-byte.
- Split sequenced as five grouped PRs; CSP fixed to report-only first, then
  enforce (the old plan contradicted itself), with the verified delta being just
  form-action 'self' and frame-ancestors 'none'.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 14:31:26 -05:00

5.0 KiB

Website API v2 — /api/v2 Skeleton (PR 1)

⚠ Superseded — not scheduled

The router domain split is being done in place, with every URL byte-identical, so there is no parallel version to stand up and this scaffold will not be built. See API_V2_PLAN.md § Why there is no /api/v2.

The file is kept, unedited below, as the concrete recipe if a real contract break ever forces a versioned API. Nothing here describes current or planned work.

Companion to API_V2_PLAN.md — this is the concrete scaffold for PR 1 in that plan's sequencing. It stands up /api/v2 empty but wired, next to a frozen /api/v1, with no behavior change. Endpoints are filled in by the later PRs (auth merge, then the domain split).

Scope

  • Create the router/v2/ tree of empty, domain-named routers.
  • Mount /v2 alongside /v1 in api.router.js.
  • Add a single trivial GET /api/v2/version so the mount is testable end-to-end.
  • Out of scope: any real endpoint, any auth change, any controller edit. Those are PR 2+.

File tree to create

website/server/src/router/v2/
  v2.router.js              # mounts the domain sub-routers; adds GET /version
  admin/
    index.js                # mounts the admin capability routers under /admin
    dashboard.router.js     users.router.js       moderation.router.js
    content.router.js       wiki.router.js         shard.router.js
    settings.router.js      invites.router.js      bot-activity.router.js
  auth/
    index.js                login.router.js  sso.router.js  totp.router.js  session.router.js
  public/
    index.js                news.router.js   wiki.router.js  page.router.js  shard.router.js
  player/
    index.js                profile.router.js  appeals.router.js  shard.router.js

internal/ is not part of v2's public tree — the internal routes stay on the separate, unpublished port (internalApp.js), exactly as in v1. See API_V2_PLAN.md § Phase 2.

Wiring

api.router.js gains the v2 mount next to v1:

const v1Router = require('./v1/v1.router')
const v2Router = require('./v2/v2.router')

apiRouter.use('/v1', v1Router)
apiRouter.use('/v2', v2Router)   // NEW — parallel version, migrate off v1 route-by-route

v2.router.js mounts each domain group and exposes the version ping:

const express = require('express')
const v2Router = express.Router()

const adminRouter = require('./admin')
const authRouter = require('./auth')
const publicRouter = require('./public')
const playerRouter = require('./player')

// Cheap liveness/mount check so the parallel version is testable before any
// real endpoint exists. Returns the API major version, nothing sensitive.
v2Router.get('/version', (req, res) => res.json({ version: 2 }))

v2Router.use('/auth', authRouter)
v2Router.use('/public', publicRouter)
v2Router.use('/admin', adminRouter)
v2Router.use('/player', playerRouter)
// NOTE: /internal is intentionally NOT mounted here — same reason as v1.

module.exports = v2Router

Each capability router is an empty stub at this stage — a router that mounts cleanly and adds no routes yet, so PR 2+ only has to add handlers, never re-wire:

// router/v2/admin/dashboard.router.js
const express = require('express')
const router = express.Router()

// Routes added in the admin domain-split PR (see API_V2_PLAN.md § Phase 2).

module.exports = router

Each index.js mounts its group's capability routers under the URL that names them, e.g.:

// router/v2/admin/index.js
const express = require('express')
const admin = express.Router()

admin.use('/dashboard', require('./dashboard.router'))
admin.use('/users', require('./users.router'))
admin.use('/moderation', require('./moderation.router'))
admin.use('/content', require('./content.router'))
admin.use('/wiki', require('./wiki.router'))
admin.use('/shard', require('./shard.router'))
admin.use('/settings', require('./settings.router'))
admin.use('/invites', require('./invites.router'))
admin.use('/bot-activity', require('./bot-activity.router'))

module.exports = admin

Acceptance criteria

  • Server boots with no error; every sub-router mounts.
  • GET /api/v2/version200 { "version": 2 }.
  • GET /api/v1/** behavior is byte-for-byte unchanged — v1 is untouched.
  • Existing server tests stay green (cd website/server && npm test).
  • A new test asserts the /api/v2/version mount (smallest possible coverage of the wiring).

Docs / spec

  • Swagger regeneration is deferred until v2 has real routes (PR 2) — a lone /version ping doesn't need an annotation. When PR 2 lands, add #swagger.* to the new routes and run npm run swagger.
  • No BACKEND_DESIGN.md change here beyond noting the parallel /api/v2 mount exists; the route-map/security-contract edits land with the PRs that add real endpoints.

Next

PR 2 fills the auth/ routers with the bearer access + refresh flow and drops the session cookie — see API_V2_PLAN.md § Phase 1.