System API 0.1 — De Facto Specification
Status: Descriptive. Documents the System contract as it ships today (retroactively numbered
0.1; the surface is unversioned in code). Written from production source on branchversion/0.40.0. Quirks/drift flagged ⚠︎.Supersedes as the contract reference the design narrative in
temp_to_be_analyzed/architecture/system.md(kept for the 3-layer mental model; note the drift listed in §7).
1. What the System API is — and the "two APIs, one word" caveat ⚠︎
A System (D&D 5e 2014/2024, System 8, custom) is described by three
layers (per system.md), but in code those layers are two very different
APIs that happen to share the word "System":
- Data plane (Layers 1–2): the trait/fixture REST API. Fully built, DB-backed, dual-ownership (system-default vs. campaign-override), seeded from TypeScript fixtures. This is the bulk of the surface and is consumed broadly (campaigns, tokens, maps, lighting, actions).
- Logic plane (Layer 3): the
SystemModuleinterface. A lazy-loaded TypeScript character-logic module. It has no REST surface and is consumed only by the Character Sheet UI — so it is really the foundation of the Character Sheet API, not a general system concern (see §8).
2. Version
Unversioned — no SYSTEM_API_VERSION constant exists. Three version-ish
fields, only one enforced:
| Field | Type | Location | Enforced? |
|---|---|---|---|
SystemModule.version |
free-text string ('1.0.0') |
systems/types.ts:269 |
❌ no consumer reads it |
SystemFixture.version |
number (5) |
types/systemFixture.ts:26 |
✅ seeder compares vs. fixture_versions table to decide re-seed (seedSystems.ts:388-396) |
MODULE_API_VERSION |
'1.4.0' |
module.ts:436 |
✅ but versions the module SDK, not this API |
So: fixture data is versioned (integer, enforced); the code module carries an unenforced string; the API contract itself has no version gate.
3. Logic plane — SystemModule (packages/shared/src/systems/types.ts)
| Export | Line | Purpose |
|---|---|---|
SystemModule |
:261 | Top-level: id, name, version, description, abilityScores, classes?, validateHp?, suggestions?, classAttributePriorities?, applyTemplateStats? |
AbilityScoreConfig |
:194 | Ability rules + validateAbilityScores / validateSingleAbility / getMaxBaseScoreAtLevel |
SystemClass |
:246 | Class (name, description?, hitDie?, savingThrowProficiencies?, spellcastingAbility?) |
SuggestionEngine |
:150 | suggestAttributes / suggestFeats / suggestPowers |
ClassAttributePriorities |
:176 | Per-class primary/secondary/tertiary abilities |
ApplyTemplateStatsResult |
:257 | Discriminated union from applyTemplateStats |
ValidationIssue / AbilityScoreValidation / HpValidation / AbilityScoreChoice |
:21 / :32 / :45 / :57 | Validation result types |
AttributeSuggestion / FeatSuggestion / PowerSuggestion / SuggestionContext |
:86 / :102 / :120 / :138 | Suggestion result/input types |
SystemModuleRegistry |
:296 | { [systemId]: () => Promise<SystemModule> } |
Character Sheet / Monster / Content API members (MODULE_API_VERSION 1.5.0, all
optional, additive). SystemModule gained the projection/generation/content
surfaces; see the dedicated specs for detail:
| Member(s) | API | Spec |
|---|---|---|
sheetSchema |
CS-1 declarative sheet vocabulary | character-sheet-api-0.1.md §3 |
construct, getAttributes, getTraits, getActions, getDerivedStats |
CS-2 read projection | ” |
projectToken |
CS-6 Character→Token projection | ” |
validate, validateEdit, applyEdit (+ EditIntent) |
CS-3 / CS-8 (⚠︎ Edit-Mode types-only) | ” |
autoFill |
CS-4 automation | ” |
contentProvider |
CS-5 → Content API | content-api-0.1.md |
listTemplates, generateCharacter |
M-1 / M-2 monster generation | monster-api-0.1.md |
campaignConfigurationSchema, getCampaignCatalogue (1.38.0, CC.1) |
Campaign configuration — the tabletop settings a system lets a campaign decide (seven kinds in groups, defaults at read, help text), and the catalogues a filter ranges over |
packages/shared/src/systems/campaignConfiguration.ts; roadmap campaign-configuration-settings |
The Character type it operates on is imported from
types/character.ts:36 — it is
system-agnostic; SystemModule is the interpretation layer.
Registry / loading (systems/index.ts):
systemRegistry (:24) statically registers only dnd5e-2014 and
dnd5e-2024; loadSystemModule(id) (:109) is cache-first and falls back to
moduleRegistry.getSystemModule(id).loadModule() (:129) for module-provided
systems (this is how System 8 loads — it is an external module, not in this
repo ⚠︎). getLoadedSystemModule (:148), isSystemAvailable (:155),
getAvailableSystems (:39), systemInfoList/getSystemInfoList (:57/:79, the
UI list; splices module systems in before custom).
4. Data plane — Systems core REST (routes/systems.ts, mounted /api/systems)
| Method | Path | Purpose | Line |
|---|---|---|---|
| GET | /api/systems |
List systems (DB rows merged with un-seeded DEFAULT_SYSTEMS) |
:39 |
| GET | /api/systems/:id |
One system (fallback to DEFAULT_SYSTEMS) |
:68 |
| POST | /api/systems |
Create user system | :107 |
| PUT/PATCH | /api/systems/:id |
Update metadata | :141/:174 |
| DELETE | /api/systems/:id |
Delete + cascade trait rows | :181 |
| POST | /api/systems/:id/copy |
Deep-copy system incl. traits | :223 |
| POST | /api/systems/:id/restore-defaults |
Re-seed bundled system traits from fixture | :401 |
| GET | /api/systems/:id/campaign-count |
Count campaigns using system | :497 |
| POST | /api/systems/:id/sync-campaigns |
Remove campaign trait copies shadowing defaults (dryRun) | :527 |
5. Data plane — Trait CRUD (system-level & campaign-level)
System-level trait CRUD — generic createTraitRouter factory
(routes/systemTraits.ts:41),
mounted /api/systems/:systemId/<trait> (all behind
requireAuthWithImpersonation). Each exposes GET /, GET /:id, POST /,
PUT /:id, PATCH /:id, DELETE /:id. Ten traits: prop-types,
property-definitions, conditions, bars, light-source-types,
vision-types, functions, actions, damage-types, currencies. ⚠︎
Entirely undocumented in system.md.
Campaign-level (inherited) trait endpoints — one router per trait, mounted
under /api/campaigns/:campaignId/<trait> (conditions is the exception:
/api/conditions?campaignId=). Files: propTypes.ts, propertyDefinitions.ts,
bars.ts, conditions.ts, lightSourceTypes.ts, visionTypes.ts,
campaignFunctions.ts, campaignActions.ts, damageTypes.ts, currencies.ts.
- Dual-ownership merge (verified
propTypes.ts:49-79): with asystemId, queryOR(campaignId=X, systemId=…), annotate each rowisSystemDefault = (systemId != null && campaignId == null)andisEditable = (campaignId === X). - 403-on-system-row (verified
propTypes.ts:168-170): writes reject whenexisting.systemId && !existing.campaignId.
change-system (campaigns.ts:447):
body { newSystemId, mergeNewDefaults, deleteOldDefaults, overwriteExisting };
optionally deletes old-default copies, copies new defaults to campaign level,
then updates campaign.systemId. ⚠︎ Copies only 7 of 10 trait types (omits
campaign_actions, damage_types, currencies) — likely a bug (§7).
6. Data plane — Trait tables & seeding
Tables (all in dbSchema.ts,
sqlite + pg mirror), each with dual-ownership system_id/campaign_id columns:
systems, prop_types, property_definitions, conditions, bars,
light_source_types, vision_types, campaign_functions, campaign_actions,
damage_types, currencies, plus fixture_versions (seed tracking).
Seeding (seedSystems.ts):
seedAllSystems(force?) (:373, also the startup call at
index.ts:843) pulls
getAllSystemFixtures() (built-in + module-provided), compares each
fixture.version to the fixture_versions table, and seeds/re-seeds
accordingly. Prop-types/property-definitions/bars get namespaced ids
(${systemId}_${id}) so multiple systems can define e.g. hp without PK
collision (buildNamespacedTraitRows, :57).
7. Drift vs. system.md
- ⚠︎ System 8 is no longer bundled.
system.mdlistssystem8/system8.tsin the repo; it has been extracted to@purple-cat-games/system8and loads viamoduleRegistry. ThesystemRegistrysnippet (system.md:616) is wrong for current code (2 entries, not 3). - ⚠︎ System-level trait CRUD (
/api/systems/:systemId/*, 10 routers) is absent from the doc. - ⚠︎ 3 extra trait types in code not in the doc's Layer-2 list:
actions(campaign_actions),damage_types,currencies(+fixture_versions,baseCurrency). - ⚠︎ Fixture packaging claim outdated: doc says traits seed via SQL
migrations 0014–0018; actual mechanism is TS fixtures +
seedSystems.ts+ version-gatedfixture_versions. - Doc omits newer fields (
aliases,showOnHover, barcolorScale*,SystemClass.savingThrowProficiencies/spellcastingAbility,applyTemplateStats) and thecustomno-validation system behavior.
8. Overlap with the Character Sheet API — the key structural finding
SystemModule (Layer 3) is almost entirely a Character-Sheet concern. Its
sole consumer is
CharacterSheet.tsx
(loadSystemModule at :463; abilityScores/suggestions/classes/
applyTemplateStats at :474–:1021). No server code and no other UI consumes its
logic members. Classification:
- Foundational (this System API): Layer 1 metadata (
ISystem) + Layer 2 trait tables/REST — consumed app-wide. - Character-sheet-specific (belongs to the Character Sheet API):
abilityScores,classes,validateHp,suggestions,classAttributePriorities,applyTemplateStats, and all validation/suggestion types. These drive one screen and should arguably be documented/versioned under a Character-Sheet system-adapter contract, not "System API."
9. Known gaps
change-systemmigrates only 7/10 trait types (§5) — actions/damage-types/ currencies silently dropped on system switch.- No API version / negotiation;
SystemModule.versionis read by nothing. - ⚠︎
system-leveltrait writes are under-guarded: the generic factory only enforces same-system ownership, not "bundled system is read-only" — the server will happilyPATCH /api/systems/dnd5e-2014/bars/:id. Bundled protection relies on the client honoringisBundledSystem(which excludessystem8). - Two parallel trait-copy implementations (
systems.tscopy/restore vs.campaigns.tschange-system) with divergent field sets — divergence already realized in gap #1. - Legacy
DND5E_*constants intypes/system.tsduplicate fixture data (hardcodesystemId:'dnd5e-2014'even for 2024) — pre-fixture leftovers.