Skip to content

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 branch version/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 rewrite req.auth.userId via X-Impersonate-User-Id (middleware/impersonation.ts).
  • Admin routes use bare requireAuth + requireAdmin (middleware/requireAdmin.ts, DB user_settings.is_admin).
  • Intentionally unauthenticated: /api/invites/:token, /api/character-invites/:token, /api/demo, /health, and module routers.
  • ⚠︎ Dev footgun: with CLERK_SECRET_KEY absent, auth degrades to a shared req.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) and library.ts:121 (default 50). tokens.ts honors ?limit (hard cap 5000) but no offset. No list route accepts client sort/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 parseListParams exists (utils/listQuery.ts:106, supports limit/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), sharing campaignSharing.ts, campaign↔characters campaignCharacters.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), sharing characterSharing.ts.
  • Tokens tokens.ts (CRUD + actions/auras/conditions/mount/artworks; GET / requires mapId), parties parties.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.

  • 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 into PurpleCatClient.
  • 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 / equipment with 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-8 applyEdit / EditIntent types 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).

7. Known gaps

  1. parseListParams is dead code — the centerpiece of the list-cleanup ships built + unit-tested but with 0 callers (§4).
  2. No client sort/order/fields anywhere; sparse fieldsets simulated by two bespoke flags.
  3. patchGuard referenced but nonexistent; reject-unknown-fields essentially unenforced (§3).
  4. PUT ≡ PATCH across 24 files (§3).
  5. Per-handler auth + error formatting (drift-prone) and dev_user silent fallback (§2).
  6. Two overlapping deletion subsystems (§3).