REST / HTTP API 0.1 — De Facto Specification
Status: Descriptive. Documents the HTTP API as it ships today (retroactively numbered
0.1; the surface is unversioned). Written from production source on branchversion/0.40.0. Quirks flagged ⚠︎. The committed changes to this surface are tracked in../../committed/architecture/rest-api-cleanup.md.
1. Shape & mounting
Express + Drizzle ORM. ~90 route modules, ~350+ endpoints, all mounted under
/api in index.ts:281-372. There
is no apps/server — the only app is apps/web (client). Resource-per-file CRUD,
heavily supplemented by RPC-style verb endpoints
(accept/deny/grant/mount/roll/generate/render/reorder).
Unversioned: no /api/v1, no version header, no version constant. (Every
/v1/ in the tree is an external vendor URL — OpenAI/Anthropic/xAI.) The only
version constant, MODULE_API_VERSION, governs the module contract, not REST.
2. Authentication (consistent — Clerk)
ClerkExpressRequireAuth(index.ts:246-254).- Almost every router mounts behind
requireAuthWithImpersonation=[requireAuth, handleImpersonation](index.ts:278); admins may rewritereq.auth.userIdviaX-Impersonate-User-Id(middleware/impersonation.ts). - Admin routes use bare
requireAuth+requireAdmin(middleware/requireAdmin.ts, DBuser_settings.is_admin). - Intentionally unauthenticated:
/api/invites/:token,/api/character-invites/:token,/api/demo,/health, and module routers. - ⚠︎ Dev footgun: with
CLERK_SECRET_KEYabsent, auth degrades to a sharedreq.auth = { userId: "dev_user" }(index.ts:250-253).
3. Conventions
| Convention | State |
|---|---|
| Error shape | Consistent { error: string } (+ optional details/stack). No shared formatter — each handler hand-writes it ⚠︎. Status codes idiomatic (400/401/403/404/409/410/500). |
| Soft-delete | Consistent for top-level entities (deletedAt/deletedBy; list/get filter isNull(deletedAt)). Two recovery subsystems: /api/soft-deleted/* (durable) and /api/deleted-items/* (1-hour undo) ⚠︎ overlapping. Child/value-table rows are hard-deleted. |
| Ownership/authz | Enforced everywhere, but re-implemented per handler (no shared guard) ⚠︎. |
| PUT vs PATCH | ⚠︎ Identical — in 24 route files PUT and PATCH share one handler; both are partial merges (PUT is not full-replace). |
| Sub-entity routes | Consistent where present: /:id/<collection>/:childId (token actions/auras/conditions/artworks; character actions + slug-keyed skills/feats/powers/attacks/attributes + artwork + versions). |
| Reject-server-fields | ⚠︎ Weak — per-route zod, but only drawings.ts uses .strict(); most writes silently drop unknown fields. A patchGuard is referenced in a docstring but does not exist. |
4. List / query conventions ⚠︎ (mostly absent — the core of the cleanup)
- Pagination: only two routes implement real
limit/offset, both hand-rolled —ledger.ts:102(1–100, default 20) andlibrary.ts:121(default 50).tokens.tshonors?limit(hard cap 5000) but no offset. No list route accepts clientsort/order(ordering is hardcoded per route). - Filtering: ad hoc per route (
?campaignId,?mapId,?layerId,?type,?tags,?category, …) with no shared parser. - Sparse fieldsets: a full helper
parseListParamsexists (utils/listQuery.ts:106, supportslimit/offset/sort/order/fields) but has zero callers — dead code. The only live payload-slimming is bespoke: characters' stub-column projection (characters.ts:~303) and tokens'?omitActions=1(tokens.ts:403).
5. Resource catalog (grouped; file:line = route file)
Core play entities:
- Campaigns
campaigns.ts(CRUD +/:id/change-system), sharingcampaignSharing.ts, campaign↔characterscampaignCharacters.ts. - Characters
characters.ts(CRUD + export/import + sub-entities: actions :938/:986/:1037, slug-keyed skills/feats/powers/attacks/attributes factory :1109-1189, artwork, versions), sharingcharacterSharing.ts. - Tokens
tokens.ts(CRUD + actions/auras/conditions/mount/artworks;GET /requiresmapId), partiesparties.ts.
Maps & layers: maps maps.ts + export mapExport.ts, map-objects mapObjects.ts, layers mapLayers.ts, drawings drawings.ts, fog fogOfWarShapes.ts, VBL vblShapes.ts.
Systems & value-tables (dual-scoped): see system-api-0.1.md. System-level via systemTraits.ts factory; campaign-level via per-trait files (propTypes, propertyDefinitions, bars, conditions, lightSourceTypes, visionTypes, campaignFunctions, campaignActions, damageTypes, currencies) + roll-tables rollTables.ts. Factions: 5 routers in factions.ts.
Media / upload (storage sub-surface): images.ts (upload :60, upload-file :211, remove-background :302/:422, presigned-url :620, register :689), imageProxy.ts (SSRF-guarded fetch→base64), textures.ts, library.ts.
AI / generation: see ai-api-0.1.md — ai.ts, mapGenerations.ts.
Ancillary: equipment equipment.ts, ledger ledger.ts, chat chatMessages.ts, entity-access entityAccess.ts, user-settings userSettings.ts, external system data systemDataProxy.ts, 5e data proxies (dnd5e2014Api.ts, dnd5e2024Api.ts).
History (v0.100.0): history.ts — GET /api/history/:type/:id, /diff?a&b, /:commitId, POST …/restore, GET /api/history/deleted; every write to a campaign, map or character is a commit — see the commit path.
Recovery / admin: softDeleted.ts, deletedItems.ts (a dead table since soft delete; see the commit path page), admin routers (adminUsers, adminSystemHealth, adminHostedModules, adminSystems), public invites, inline /health + /api/sessions/active + /api/modules.
6. Related contracts (documented elsewhere)
- Auth → §2 (Clerk boundary; types in
types/auth.ts). - Realtime/Socket →
realtime-api-0.1.md(not REST). - Campaign Functions/Actions (server-persisted Action extensions) →
action-api-0.1.md. - Client SDK (
packages/client-sdk) wraps a subset of REST + WebSocket intoPurpleCatClient. - Character Sheet API (module↔sheet contract) →
character-sheet-api-0.1.md. REST implications:- Explicit write path (CS-8 / CC-3): record mutations use the existing per-collection sub-entity routes
POST/PATCH/DELETE /characters/:id/<collection>/:slug(§5;<collection>∈ skills/feats/powers/attacks/attributes). The Character Sheet API's target is generic platform-parlance channels —attributes/traits/equipmentwith globally-unique slugs (CC-3 resolved) — but the current routes still speak the specific collections; the generic-channel envelope is the direction, not yet the route surface. - Interpreted writes (
applyEdit): ⚠︎ defined but not wired. The CS-8applyEdit/EditIntenttypes ship, but no generic interpreted-write route or Edit-Mode UI exists this project (next project — Character Sheet Overhaul). - Manifest read-cache: ⚠︎ no server endpoint. The platform loads system modules client-side only, so
construct()runs on the client and the manifest cache (manifestCache.ts, keyed by(characterId, updatedAt)) is client-side. The spec's server-side / Redis materialization needs server-side module loading (a later architectural change).
- Explicit write path (CS-8 / CC-3): record mutations use the existing per-collection sub-entity routes
7. Known gaps
parseListParamsis dead code — the centerpiece of the list-cleanup ships built + unit-tested but with 0 callers (§4).- No client
sort/order/fieldsanywhere; sparse fieldsets simulated by two bespoke flags. patchGuardreferenced but nonexistent; reject-unknown-fields essentially unenforced (§3).- PUT ≡ PATCH across 24 files (§3).
- Per-handler auth + error formatting (drift-prone) and dev_user silent fallback (§2).
- Two overlapping deletion subsystems (§3).