Content API 0.1 — De Facto Specification
Status: Descriptive — the contract as it ships on branch
version/0.43.0+ System 8 moduleversion/system8-0.5.0. Versioned byMODULE_API_VERSION1.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-corenever loads aSystemModule. So a module'scontentProviderruns 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 viaModuleServerExportinstead.
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—system8ContentProviderover the module's own routes (source-tagged "System 8").server/index.ts— contributesallowedHosts: ['system8.purplecat.wiki']+ anaiParseSchema.
7. Known gaps / deferred
- ⚠︎ Consumer flip deferred.
feats.service.tsdispatch,dataTableApi.tsper-system fetchers, and theAdd*Modal/EquipmentListwiring still use the legacy path — they are the out-of-scope old sheet / ui-components, closing with their retirement.Dnd5eFeatRecordmoves 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).