Skip to content

Content API 0.1 — De Facto Specification

Status: Descriptive — the contract as it ships on branch version/0.43.0 + System 8 module version/system8-0.5.0. Versioned by MODULE_API_VERSION 1.5.0. Master spec: 4-system-platform-separation/requirements.md (R3).

1. What it is

The Content API lets a module own where character-building content (feats/powers/spells/equipment/monsters) comes from, replacing per-system dispatch in the platform (feats.service.ts switch(systemId), dataTableApi.ts per-system fetchers). It is a set of Module-API extensions adjacent to the Character Sheet API (CS-5 consumes it).

⚠︎ Architectural constraint: the platform loads system modules client-side only — server-core never loads a SystemModule. So a module's contentProvider runs on the client, and the generic content-access layer is client-side; a provider typically fetches the module's own server router. Content-adjacent surfaces that must run server-side (AI parse schema, host allowlist, fixtures) are contributed via ModuleServerExport instead.

2. R3.1 — Provider + generic access

Provider (on SystemModule, systems/types.ts):

contentProvider?: ContentProvider   // keyed by content-type via the `type` arg
  listTraits?(type, filter?): ContentPage      // 'feat' | 'power' | …
  getTraitChoices?(character, slot): ContentPage
  listEquipment?(filter?): ContentPage
  listMonsters?(filter?): ContentPage          // M-4 bestiary
  getDetail?(type, slug): ContentRecord | undefined

Records are generic + mostly opaque (ContentRecord { slug, name, type?, source?, description?, metadata? }); pages are ContentPage { records, total?, cursor? }. Browsers/selects render off this metadata (the source tag, type).

Generic access (services/contentApi.ts): getContentProvider(systemId) resolves the module's provider via loadSystemModule; listSystemTraits / listSystemEquipment / listSystemMonsters / getSystemTraitChoices dispatch to it, returning empty pages for systems without a provider. This is the single system-agnostic access layer.

⚠︎ There are no generic server content routes (the server can't call a client provider). A module's own router (e.g. /api/modules/system8/{feats, powers,system-equipment}) is its content endpoint; the provider fetches it.

3. R3.2 — Module-hosted server contract

ModuleServerExport (types/module.ts) now has three legs, all mounted at boot in server-core/src/index.ts:

Leg Field Platform behavior
(a) Express router router auto-mounted at /api/modules/{moduleId}
(b) data-store hook initialize(ctx) called once at startup with a per-module dataDir
(c) external-host allowlist allowedHosts?: string[] merged into the proxy + image-fetch SSRF allowlists via moduleRegistry.registerAllowedHosts / getAllowedHosts — a module's hosts are allowed only while it is loaded

Leg (c) removed the hardcoded system8.purplecat.wiki from systemDataProxy.ts and tokens.ts.

4. R3.3 — AI parse-schema provider

ModuleServerExport.aiParseSchema?: AiParseSchema { attributes, guidelines? } (services/aiParseSchema.ts). Registered per system at boot (moduleRegistry.registerAiParseSchema); the AI import route (routes/ai.ts) builds its prompt via the pure buildCharacterParsePrompt(systemId, schema) with a generic six-attribute fallback — dropping the hardcoded D&D schema and the default('system8') (now the generic custom baseline).

5. R3.4 — Fixture registration (plural)

ModuleServerExport.systemFixtures?: SystemFixture[] (plural; singular kept for back-compat) + resolveSystemFixtures(export) — one package can seed multiple systems (the 5e edition split). Wired into the module fixture registration in index.ts. ⚠︎ Removing the enumerated default-action seeder (db.ts:846-853) is deferred (fresh-DB/prod seeding-order risk).

6. Proven against System 8

  • contentProvider.ts — system8ContentProvider over the module's own routes (source-tagged "System 8").
  • server/index.ts — contributes allowedHosts: ['system8.purplecat.wiki'] + an aiParseSchema.

7. Known gaps / deferred

  • ⚠︎ Consumer flip deferred. feats.service.ts dispatch, dataTableApi.ts per-system fetchers, and the Add*Modal / EquipmentList wiring still use the legacy path — they are the out-of-scope old sheet / ui-components, closing with their retirement. Dnd5eFeatRecord moves into the 5e module in Phase 5.
  • ⚠︎ Seeder de-dup (3.5) and shared de-leak (3.6) deferred (see the project todo for reasons).