Worldbuilding, Narrative Design, and World Simulation Platform
This document specifies what is implemented in libs/hathor/* and
apps/hathor/*. Every model, field, enum, endpoint, event, and library entry
below is taken directly from the source: primarily
database/prisma/schema.prisma, the world-api, narrative-api, and
simulation-worker route files, event-publisher/src/*,
event-handlers/src/index.ts, contracts/src/events/hathor.ts, the
svc-veilborn-* app files, lore-compiler/src/*, validation/src/index.ts,
and the package.json/project.json of each project.
The Hathor domain spans 17 libraries and 7 applications. Section 10 is
the only forward-looking section; everything in it is labelled (planned) and
sourced from the V2 contract docs under V2/.
1. Data Models (@hathor/database)#
@hathor/database is the Prisma schema and client for Hathor. The schema is in
libs/hathor/database/prisma/schema.prisma. The Prisma generator emits the
client into src/generated/client;
previewFeatures = ["fullTextSearchPostgres"] is enabled. The datasource is
PostgreSQL via the HATHOR_DATABASE_URL environment variable. The schema does
not declare a multi-schema namespace — all tables live in the connection's
default schema. Every model id is a cuid() stored as VarChar(25).
The schema defines 20 models in total: World, WorldVersion, Branch, MergeRequest, Entity, EntityRelation, StoryGraph, StoryNode, StoryArc, DialogueTree, DialogueNode, DialogueSession, Quest, QuestObjective, QuestReward, SimulationRun, SimulationEvent, SimulationSnapshot, ValidationResult, and AuditLog.
1.1 World#
The World model is the top-level container for a simulation universe. Every
other model belongs to a world, directly or indirectly.
Table: world | Unique: [userId, name]
| Field | Type | Default | Description |
|---|---|---|---|
id |
String (cuid) | auto | Primary key |
name |
String (≤200) | — | World name |
description |
String? (Text) | — | Description |
status |
WorldStatus | DRAFT |
Lifecycle status |
genre |
WorldGenre | FANTASY |
Genre classification |
scope |
WorldScope | MEDIUM |
World size/scope |
technologyLevel |
TechnologyLevel | MEDIEVAL |
Technology era |
magicLevel |
MagicLevel | MEDIUM |
Magic prevalence |
userId |
String (≤50) | — | Owner user ID |
organizationId |
String? (≤50) | — | Organization |
settings |
Json | {} |
World settings |
stats |
Json | {} |
Aggregate statistics |
tags |
String[] | [] |
Tags |
createdAt |
DateTime | now() |
Creation timestamp |
updatedAt |
DateTime | auto | Update timestamp |
publishedAt |
DateTime? | — | Publish timestamp |
deletedAt |
DateTime? | — | Soft-delete marker |
Relations: versions, branches, entities, quests, storyGraphs,
dialogueTrees, simulations. Indexes: status, genre, userId,
organizationId, createdAt, deletedAt.
1.2 WorldVersion#
WorldVersion represents an immutable snapshot of world state. Versions form a
parent-child chain analogous to git commits: each version has a
parentVersionId pointing to the previous one, and isCurrent marks the HEAD
of each branch.
Table: world_version | Unique: [worldId, version],
[worldId, branchId, version]
| Field | Type | Default | Description |
|---|---|---|---|
id |
String | auto | Primary key |
version |
Int | 1 |
Version number |
name |
String? (≤200) | — | Version name |
description |
String? (Text) | — | Version description |
worldId |
String | — | FK to World (cascade delete) |
branchId |
String? | — | FK to Branch |
parentVersionId |
String? | — | Self-FK to parent WorldVersion |
changes |
Json | [] |
List of changes (delta) |
snapshot |
Json | {} |
Full state snapshot |
createdBy |
String? (≤50) | — | Creator user ID |
createdAt |
DateTime | now() |
Creation timestamp |
isCurrent |
Boolean | false |
Whether this is the branch's HEAD |
Self-relation VersionParent provides childVersions. Also has merge-request
back-relations mergeRequestsSource and mergeRequestsTarget.
1.3 Branch#
A Branch represents an alternate timeline or development track within a world.
Each branch diverges from a specific branchPointVersionId, accumulates its own
WorldVersion records, and can be merged back via a MergeRequest.
Table: branch | Unique: [worldId, name]
| Field | Type | Default | Description |
|---|---|---|---|
id |
String | auto | Primary key |
name |
String (≤200) | — | Branch name |
description |
String? (Text) | — | Branch description |
worldId |
String | — | FK to World (cascade delete) |
branchPointVersionId |
String? | — | Version where branch split |
isMain |
Boolean | false |
Whether main branch |
createdAt |
DateTime | now() |
Creation timestamp |
updatedAt |
DateTime | auto | Update timestamp |
Relations: versions.
1.4 MergeRequest#
A MergeRequest tracks a proposal to merge changes from one version into
another. The conflicts JSON field records any entities changed on both sides;
each conflict must be resolved (source, target, or custom) before the merge can
execute.
Table: merge_request
| Field | Type | Default | Description |
|---|---|---|---|
id |
String | auto | Primary key |
title |
String (≤200) | — | Merge request title |
description |
String? (Text) | — | Description |
sourceVersionId |
String | — | FK to source WorldVersion |
targetVersionId |
String | — | FK to target WorldVersion |
status |
MergeRequestStatus | OPEN |
Status |
conflicts |
Json | [] |
Conflict details |
resolution |
Json? | — | Resolution details |
createdBy |
String? (≤50) | — | Creator user ID |
reviewedBy |
String? (≤50) | — | Reviewer user ID |
createdAt |
DateTime | now() |
Creation timestamp |
updatedAt |
DateTime | auto | Update timestamp |
mergedAt |
DateTime? | — | Merge timestamp |
1.5 Entity#
Entity is the universal container for all objects in a world. Rather than
having separate tables for characters, factions, locations, etc., all 10 entity
types share this one table and store type-specific properties in the
properties JSON field. This design allows new entity types to be added without
schema migrations.
The 10 supported entity types are: CHARACTER, FACTION, LOCATION, EVENT, RELATIONSHIP, ERA, REGION, ITEM, CULTURE, RELIGION.
Table: entity | Unique: [worldId, type, name]
| Field | Type | Default | Description |
|---|---|---|---|
id |
String | auto | Primary key |
name |
String (≤200) | — | Entity name |
description |
String? (Text) | — | Description |
type |
EntityType | — | Entity type (10 values) |
worldId |
String | — | FK to World (cascade delete) |
versionId |
String? | — | Version reference |
properties |
Json | {} |
Type-specific properties |
tags |
String[] | [] |
Tags |
aliases |
String[] | [] |
Alternate names for search |
createdAt |
DateTime | now() |
Creation timestamp |
updatedAt |
DateTime | auto | Update timestamp |
deletedAt |
DateTime? | — | Soft-delete marker |
searchVector |
Unsupported(tsvector) |
— | Full-text search vector |
Relations: outgoingRelations, incomingRelations (both
EntityRelation[]).
1.6 EntityRelation#
EntityRelation models a directed relationship between two entities — for
example, "character A is allied with faction B" or "location C is inside region
D". The type field is free-text so any relationship semantics can be expressed
without schema changes. The bidirectional flag indicates whether the
relationship applies equally in both directions.
Table: entity_relation | Unique: [sourceId, targetId, type]
| Field | Type | Default | Description |
|---|---|---|---|
id |
String | auto | Primary key |
sourceId |
String | — | FK to source Entity (cascade) |
targetId |
String | — | FK to target Entity (cascade) |
type |
String (≤100) | — | Relationship type (free-text) |
properties |
Json | {} |
Additional properties |
strength |
Float | 1.0 |
Relationship strength / weight |
bidirectional |
Boolean | false |
Whether bidirectional |
createdAt |
DateTime | now() |
Creation timestamp |
updatedAt |
DateTime | auto | Update timestamp |
1.7 StoryGraph, StoryNode, StoryArc#
The three story models together represent a non-linear branching narrative. A
StoryGraph is the top-level container; StoryNode records represent
individual scenes, choices, or conditions; StoryArc records are the directed
edges connecting nodes.
StoryGraph — Branching narrative graph. Table: story_graph |
Unique: [worldId, name, version]. Fields: id, name, description?,
worldId (FK, cascade), version (Int, default 1), isActive (Boolean,
default true), tags, createdAt, updatedAt, publishedAt?. Relations:
nodes (StoryNode[]), arcs (StoryArc[]).
StoryNode — A node in a story graph. Table: story_node. Fields: id,
storyGraphId (FK, cascade), type (StoryNodeType, default SCENE), name,
content?, positionX/positionY (Float, default 0, for the visual editor),
conditions (Json []), actions (Json []), metadata (Json {}),
createdAt, updatedAt. Relations: outgoingArcs, incomingArcs.
StoryArc — Directed edge between two StoryNodes. Table: story_arc |
Unique: [storyGraphId, sourceId, targetId]. Fields: id, storyGraphId
(FK, cascade), sourceId/targetId (FK to StoryNode, cascade), label? (≤200,
choice text), conditions (Json [], traversal conditions), priority (Int,
default 0, for auto-selection), createdAt.
1.8 DialogueTree, DialogueNode, DialogueSession#
The three dialogue models support both authoring (the tree structure) and
runtime playback (active sessions). A DialogueTree holds the authored
structure; DialogueNode records hold individual lines and choices;
DialogueSession tracks a live playback instance, recording which node the
participant is currently at and what choices have been made.
DialogueTree — Tree structure for character conversations. Table:
dialogue_tree | Unique: [worldId, name]. Fields: id, name,
description?, worldId (FK, cascade), speakers (Json [], array of entity
IDs), locationId? (location context), tags, createdAt, updatedAt.
Relations: nodes, sessions.
DialogueNode — A single dialogue step. Table: dialogue_node. Fields:
id, dialogueTreeId (FK, cascade), type (DialogueNodeType, default
STATEMENT), speakerId? (entity ID), content?, emotion? (≤50,
emotion/tone tag), conditions (Json []), actions (Json []), isStart
(Boolean, default false), isEnd (Boolean, default false), choices (Json
[]), nextNodes (Json [], branching node IDs), createdAt, updatedAt.
DialogueSession — A runtime instance of a dialogue tree being played.
Table: dialogue_session. Fields: id, dialogueTreeId (FK, cascade),
currentNodeId?, history (Json [], visited nodes), context (Json {},
session state), participantId? (≤50), isComplete (Boolean, default false),
startedAt, completedAt?.
1.9 Quest, QuestObjective, QuestReward#
Quests are stored across three related tables. Quest holds the top-level
definition; QuestObjective holds the individual tasks a player must complete;
QuestReward holds the items, currency, or reputation changes granted on
completion.
Quest — A quest definition. Table: quest | Unique:
[worldId, name]. Fields: id, name, description?, worldId (FK,
cascade), category (QuestCategory, default SIDE), priority (QuestPriority,
default NORMAL), status (QuestStatus, default DRAFT), giverId?
(quest-giver entity), locationId?, minLevel?/maxLevel? (Int),
prerequisites (Json [], quest IDs), tags, createdAt, updatedAt,
publishedAt?. Relations: objectives, rewards.
QuestObjective — An objective within a quest. Table: quest_objective.
Fields: id, questId (FK, cascade), type (ObjectiveType, default CUSTOM),
description (Text, required), targetCount (Int, default 1),
currentProgress (Int, default 0), isCompleted (Boolean, default false),
isOptional (Boolean, default false), orderIndex (Int, default 0),
targetEntityId?, metadata (Json {}), createdAt, updatedAt.
QuestReward — A reward granted on quest completion. Table:
quest_reward. Fields: id, questId (FK, cascade), type (String, ≤50),
amount? (Int, for currency/XP), itemId? (entity ID for item rewards),
description? (≤500), isChoice (Boolean, default false), createdAt.
1.10 SimulationRun, SimulationEvent, SimulationSnapshot#
Simulation state is tracked across three models. SimulationRun is the job
record, tracking status and progress from submission through completion.
SimulationEvent records individual events emitted during the run (e.g., a
price spike, an alliance formed). SimulationSnapshot captures the full
simulation state at a specific tick so it can be compared or restored later.
SimulationRun — A simulation job. Table: simulation_run.
| Field | Type | Default | Description |
|---|---|---|---|
id |
String | auto | Primary key |
worldId |
String | — | FK to World (cascade) |
type |
SimulationType | FULL |
Simulation type |
status |
SimulationStatus | PENDING |
Job status |
priority |
SimulationPriority | NORMAL |
Priority |
input |
Json | {} |
Input parameters |
output |
Json? | — | Output results |
progress |
Float | 0 |
Progress (0–1) |
currentTick |
Int | 0 |
Current simulation tick |
totalTicks |
Int? | — | Total ticks to run |
errorMessage |
String? (Text) | — | Error message |
errorDetails |
Json? | — | Structured error detail |
workerId |
String? (≤50) | — | Worker assignment |
startVersionId |
String? | — | Version at sim start |
endVersionId |
String? | — | Version after sim |
createdAt |
DateTime | now() |
Creation timestamp |
updatedAt |
DateTime | auto | Update timestamp |
startedAt |
DateTime? | — | Start timestamp |
completedAt |
DateTime? | — | Completion timestamp |
Relations: events, snapshots.
SimulationEvent — An event generated during simulation at a specific tick.
Table: simulation_event. Fields: id, simulationId (FK, cascade),
type (String, ≤100), tick (Int), data (Json {}), affectedEntities
(Json [], entity IDs), createdAt.
SimulationSnapshot — Full state snapshot at a tick. Table:
simulation_snapshot | Unique: [simulationId, tick]. Fields: id,
simulationId (FK, cascade), tick (Int), state (Json {}), createdAt.
1.11 ValidationResult#
ValidationResult stores the output of lore consistency validation checks so
results can be reviewed later and compared across runs.
Table: validation_result
| Field | Type | Description |
|---|---|---|
id |
String | Primary key |
worldId |
String | World reference |
entityId |
String? | Entity reference |
validationType |
String (≤100) | Validation type |
isValid |
Boolean | Whether valid |
issues |
Json ([]) |
Issues found |
suggestions |
Json ([]) |
Improvement suggestions |
createdAt |
DateTime | Creation timestamp |
1.12 AuditLog#
AuditLog records a tamper-evident trail of tracked changes, capturing the
previous state alongside the new changes so that the history of any entity can
be reconstructed.
Table: audit_log. Fields: id, entityType (String, ≤50), entityId
(String, ≤25), action (String, ≤50), userId? (≤50), changes (Json {}),
previousState (Json?), createdAt. Indexes: [entityType, entityId],
userId, action, createdAt.
2. Database Enumerations#
All enums below are defined in schema.prisma and re-exported from
@hathor/database. They form the canonical persistence contract — see the note
at the end of this section for differences between these Prisma enums and the
per-service string-union types used at the API wire layer.
WorldStatus (4)#
DRAFT, ACTIVE, ARCHIVED, DELETED
WorldGenre (8)#
FANTASY, SCIENCE_FICTION, HISTORICAL, CONTEMPORARY, HORROR, MYSTERY,
MYTHOLOGY, CUSTOM
WorldScope (5)#
SMALL (village/city), MEDIUM (region/country), LARGE (continent), EPIC
(world), COSMIC (multi-world/universe)
TechnologyLevel (9)#
PRIMITIVE, ANCIENT, MEDIEVAL, RENAISSANCE, INDUSTRIAL, MODERN,
FUTURISTIC, ADVANCED, MIXED
MagicLevel (5)#
NONE, LOW, MEDIUM, HIGH, EPIC
EntityType (10)#
CHARACTER, FACTION, LOCATION, EVENT, RELATIONSHIP, ERA, REGION,
ITEM, CULTURE, RELIGION
CharacterStatus (5)#
ALIVE, DECEASED, UNKNOWN, MISSING, LEGENDARY
FactionType (10)#
GOVERNMENT, MILITARY, RELIGIOUS, CRIMINAL, MERCHANT, GUILD,
SECRET_SOCIETY, FAMILY, TRIBAL, OTHER
LocationType (11)#
CITY, TOWN, VILLAGE, FORTRESS, TEMPLE, DUNGEON, WILDERNESS,
LANDMARK, REGION, REALM, PLANE
QuestStatus (6)#
DRAFT, AVAILABLE, ACTIVE, COMPLETED, FAILED, CANCELLED
QuestCategory (6)#
MAIN, SIDE, EXPLORATION, FACTION, DAILY, WORLD_EVENT
QuestPriority (4)#
LOW, NORMAL, HIGH, CRITICAL
ObjectiveType (8)#
COLLECT, KILL, TALK, EXPLORE, ESCORT, DELIVER, CRAFT, CUSTOM
StoryNodeType (6)#
START, SCENE, CHOICE, CONDITION, ACTION, END
DialogueNodeType (6)#
GREETING, STATEMENT, QUESTION, RESPONSE, CHOICE, END
SimulationType (5)#
ECONOMY, POLITICS, CULTURE, SCENARIO, FULL
SimulationStatus (6)#
PENDING, QUEUED, RUNNING, COMPLETED, FAILED, CANCELLED
SimulationPriority (4)#
LOW, NORMAL, HIGH, CRITICAL
MergeRequestStatus (5)#
OPEN, APPROVED, MERGED, REJECTED, CANCELLED
ValidationSeverity (4)#
INFO, WARNING, ERROR, CRITICAL
Note on string-union types in services. The API apps and the SDK declare their own lowercase string-union types that are not always identical to the Prisma enums above. For example
apps/hathor/world-api/src/types.tsdeclaresWorldStatus = 'draft' | 'in_progress' | 'review' | 'published' | 'archived'andMergeRequestStatus = 'open' | 'merged' | 'closed' | 'conflict', and@hathor/validation(src/types.ts) declaresValidationSeverity = 'error' | 'warning' | 'info'(three values, noCRITICAL). Treat the Prisma enums above as the persistence contract and the per-service unions as the wire/runtime contract.
3. Database Schema Highlights#
This section summarizes the structural design decisions in the schema that are most important to understand before working with Hathor's database layer.
- ORM: Prisma 6 | DB: PostgreSQL.
- Generated client: emitted into
database/src/generated/client. - 20 models: World, WorldVersion, Branch, MergeRequest, Entity, EntityRelation, StoryGraph, StoryNode, StoryArc, DialogueTree, DialogueNode, DialogueSession, Quest, QuestObjective, QuestReward, SimulationRun, SimulationEvent, SimulationSnapshot, ValidationResult, AuditLog.
- Version-control model: Git-like versioning with branches, parent-child
version chains (
WorldVersion.parentVersionId), and merge requests.isCurrentonWorldVersionmarks the HEAD. - Entity design: a single
Entitytable withproperties: Jsonfor type-specific fields supports all 10 entity types without per-type tables. - Full-text search: an
Unsupported("tsvector")column onentity(fullTextSearchPostgrespreview feature enabled). - Client lifecycle:
@hathor/databaseexposesgetHathorClient,createHathorClient, anddisconnectHathorClient(database/src/client.ts), plus seed scripts (prisma/seed.ts,prisma/seed-test.ts).
4. API Surface#
The Hathor domain ships three HTTP worldbuilding services (World API, Narrative API, and Simulation Worker) plus two Veilborn game services. The worldbuilding services store data through in-memory service stores backed by repositories; the simulation worker uses an in-memory job queue.
4.1 World API (@hathor/world-api, Port 3001)#
The World API is a Hono server (apps/hathor/world-api/src/app.ts). Global
middleware includes secureHeaders, cors, request logger, prettyJSON, and
a rateLimit middleware applied to /api/*. Routes are mounted under
/api/v1. Validation uses @hono/zod-validator.
Health: GET /health, GET /ready (readiness probes the world and version
services).
World CRUD (routes/worlds.ts)#
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/worlds |
List worlds (filterable) |
| POST | /api/v1/worlds |
Create a world |
| GET | /api/v1/worlds/:id |
Get world by ID |
| PUT | /api/v1/worlds/:id |
Update world properties |
| DELETE | /api/v1/worlds/:id |
Delete a world |
| POST | /api/v1/worlds/:id/clone |
Clone a world |
| GET | /api/v1/worlds/:id/export |
Export world as JSON file |
| PUT | /api/v1/worlds/:id/status |
Update world status |
| PATCH | /api/v1/worlds/:id/settings |
Update world settings |
| PATCH | /api/v1/worlds/:id/stats |
Update world stats |
Collaborators (routes/worlds.ts)#
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/worlds/:id/collaborators |
Add a collaborator |
| DELETE | /api/v1/worlds/:id/collaborators/:userId |
Remove a collaborator |
Collaborators are tracked by the world service; there is no
CollaboratorPrisma table — collaborator state is part of the in-memory world record.
Branches (routes/worlds.ts)#
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/worlds/:id/branches |
List branches |
| POST | /api/v1/worlds/:id/branches |
Create a branch |
| GET | /api/v1/worlds/:id/branches/:branchId |
Get branch details |
| DELETE | /api/v1/worlds/:id/branches/:branchId |
Delete a branch |
Versioning (routes/versions.ts)#
The versioning routes implement the full git-like version lifecycle: committing changes, viewing history, reconstructing state at any point, and marking official releases.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/worlds/:worldId/versions |
List versions |
| POST | /api/v1/worlds/:worldId/versions |
Create a version (commit) |
| GET | /api/v1/worlds/:worldId/versions/:versionId |
Get version by ID |
| GET | …/versions/:versionId/history |
Get ancestor chain |
| GET | …/versions/:versionId/state |
Reconstruct state at a version |
| GET | …/versions/:versionId/changes |
List changes in a version |
| GET | /api/v1/worlds/:worldId/releases |
List releases |
| POST | …/versions/:versionId/release |
Mark version as a release |
| POST | …/versions/:versionId/snapshot |
Create a snapshot for a version |
| GET | …/versions/:versionId/snapshot |
Get the snapshot for a version |
| POST | …/versions/:versionId/revert |
Revert world to a version |
| GET | …/versions/:fromId/compare/:toId |
Diff between two versions |
| GET | /api/v1/worlds/:worldId/current |
Get the current (HEAD) version |
| GET | /api/v1/worlds/:worldId/state |
Get current reconstructed state |
Merge System (routes/merge.ts)#
The merge routes cover the full merge workflow: diffing branches, creating a merge request, resolving individual conflicts, and executing the merge. Cherry-pick and rebase are also exposed here.
| Method | Path | Description |
|---|---|---|
| GET | …/diff/:fromId/:toId |
Diff two versions |
| GET | …/branches/:sourceId/diff/:targetId |
Diff two branches |
| GET | …/merge-requests |
List merge requests |
| POST | …/merge-requests |
Create a merge request |
| GET | …/merge-requests/:mrId |
Get merge request |
| POST | …/merge-requests/:mrId/close |
Close a merge request |
| GET | …/merge-requests/:mrId/conflicts |
List conflicts |
| POST | …/merge-requests/:mrId/conflicts/resolve |
Resolve a single conflict |
| POST | …/merge-requests/:mrId/conflicts/resolve-all |
Resolve all (source/target) |
| POST | …/merge-requests/:mrId/merge |
Execute the merge |
| POST | …/cherry-pick |
Cherry-pick a version's changes |
| POST | …/rebase |
Rebase a branch onto another |
Merge-request creation in
routes/merge.tstakes asourceBranchId/targetBranchIdbody and the merge service resolves their HEAD versions.
Query Engine (routes/query.ts)#
The query engine exposes general-purpose search and graph-traversal operations
over world entities. POST …/relationships performs an actual BFS over an
entity's ID-reference fields up to a requested depth; inferTargetType() maps
field names (e.g. factionId, allies, prerequisites) to entity types.
| Method | Path | Description |
|---|---|---|
| POST | …/query |
General entity query |
| GET | …/search?q=… |
Full-text search |
| GET | …/factions |
Query factions |
| GET | …/characters |
Query characters |
| GET | …/locations |
Query locations |
| GET | …/events |
Query timeline events |
| GET | …/entities/:type/:id |
Get one entity |
| POST | …/entities/:type/batch |
Get many entities by ID |
| GET | …/entities/:type/:id/related/:field |
Get entities related by field |
| POST | …/relationships |
BFS graph traversal |
| POST | …/paths |
Find paths between entities |
| POST | …/aggregate |
Aggregation (count/sum/avg/min/max/group_by) |
Auth and Rate Limiting#
- Auth:
X-User-IDrequest header (forwarded by the gateway), read by mutating routes. - Rate limit:
rateLimit({ limit: 100, windowMs: 60_000 })on/api/*. ExposesX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset.
4.2 Narrative API (@hathor/narrative-api, Port 3002)#
The Narrative API is a Hono server (apps/hathor/narrative-api/src/app.ts) with
middleware: cors, logger, timing, requestId, errorHandler. Validation
uses @hono/zod-validator. Routes are mounted under /api.
Health: GET /health; service info at GET /.
Story Graphs (routes/story-graphs.ts)#
CRUD at /api/story-graphs plus: arc add/update/delete (/:id/arcs,
/:id/arcs/:arcId); node add/update/delete (/:id/nodes,
/:id/nodes/:nodeId); connect nodes (POST /:id/nodes/:nodeId/connect); state
update (PUT /:id/state); variable and flag setters
(PUT /:id/variables/:name, PUT /:id/flags/:name); clone (POST /:id/clone);
export (GET /:id/export).
Quests (routes/quests.ts)#
CRUD at /api/quests plus lifecycle transitions
(POST /:id/start|complete|fail|abandon); objective progress
(PUT /:id/objectives/:objId/progress, POST /:id/objectives/:objId/complete);
clone (POST /:id/clone); export (GET /:id/export).
Dialogues (routes/dialogues.ts)#
CRUD at /api/dialogues plus: node CRUD (/:id/nodes, /:id/nodes/:nodeId);
choice CRUD (/:id/nodes/:nodeId/choices/...); speaker CRUD
(/:id/speakers/...); runtime sessions (POST /:id/sessions,
GET /sessions/:sessionId, GET /sessions/:sessionId/current,
POST /sessions/:sessionId/advance, DELETE /sessions/:sessionId); clone
(POST /:id/clone); export (GET /:id/export).
Validation (routes/validation.ts)#
The narrative validation routes allow validating individual resources (a story graph, quest, or dialogue) or batching multiple resources in a single request.
| Method | Path | Description |
|---|---|---|
| POST | /api/validation/story-graphs |
Validate a story graph |
| POST | /api/validation/quests |
Validate a quest |
| POST | /api/validation/dialogues |
Validate a dialogue tree |
| POST | /api/validation/batch |
Batch-validate multiple entities |
| POST | /api/validation/constraints/check |
Check a single constraint |
4.3 Simulation Worker (@hathor/simulation-worker, Port 3004)#
The Simulation Worker is a Hono server
(apps/hathor/simulation-worker/src/app.ts) with middleware: cors, logger.
Routes are mounted under /api. The default worker-pool configuration has one
worker each for economy, politics, culture, and scenario job types.
| Method | Path | Description |
|---|---|---|
| POST | /api/jobs |
Submit a simulation job |
| GET | /api/jobs |
List jobs (filter by status/type) |
| GET | /api/jobs/:jobId |
Get job status, position, ETA |
| POST | /api/jobs/:jobId/cancel |
Cancel a running or queued job |
| POST | /api/jobs/:jobId/retry |
Retry a failed job |
| GET | /api/workers |
Get all worker health statuses |
| GET | /api/workers/:workerId |
Get one worker's health |
| GET | /api/stats |
Queue and worker statistics |
| POST | /api/cleanup |
Trigger cleanup of completed jobs |
Job submission validates the simulation type against
economy | politics | culture | scenario | integrated, validates the
startDate/endDate, and rejects when the queue is full (HTTP 503).
Note that the simulation worker uses its own string-union types that differ
slightly from the Prisma enums in § 2. SimulationType in the worker is
'economy' | 'politics' | 'culture' | 'scenario' | 'integrated'.
SimulationStatus adds paused to the Prisma set; SimulationPriority is
'urgent' | 'high' | 'normal' | 'low' | 'background'
(apps/hathor/simulation-worker/src/types/index.ts). The five simulation input
shapes are EconomySimulationInput, PoliticsSimulationInput,
CultureSimulationInput, ScenarioSimulationInput, IntegratedSimulationInput
(all extend BaseSimulationInput), with matching *SimulationOutput types.
4.4 Veilborn Game Services#
Two Fastify services under apps/hathor/ implement Veilborn Chronicles, a
tabletop-RPG / wargame game built on the Hathor domain. Both use
@lilith/fastify-core and @lilith/service-lib and run on port 8080 by
default (PORT env var).
@hathor/veilborn-core (apps/hathor/svc-veilborn-core)#
The core game engine for the tabletop RPG and wargame. The HTTP surface
(src/app.ts) exposes /, /health, /ready, /live, and a /metrics
endpoint (JSON or Prometheus). The game systems are implemented as ~30
domain-logic modules under src/ (~42k lines total), each fully unit-tested
under src/__tests__/integration/:
combat.ts— hex-grid tactical combat ("Confluence"): axial/cube hex coordinates, Tide Initiative turn order, an Impulse/Flow/Surge/Torrent action economy, line-of-sight and cover.character-creation.ts,character-sheet.ts,abilities.ts,awakening.ts,paths.ts,resonance.ts— character build, progression, and abilities.cards.ts— card system (the largest module).dice.ts— dice resolution.world-gen.ts— seeded procedural generation of realms, regions, locations, items, creatures, and factions (SeededRandom).wargame.ts,territory-control.ts,combat-ai.ts,veylmaster.ts— wargame and AI systems.weave.ts,threads.ts,social.ts,multiplayer.ts,game-state.ts,tutorial.ts,launch-content.ts,mobile-ui.ts,ui-system.ts,beta-program.ts,asset-gen.ts,data-access.ts,database-schema.ts— social, persistence, content, and UI systems.
@hathor/veilborn-strategy (apps/hathor/svc-veilborn-strategy)#
Strategy game engine for the "The Veil War" mode. Fastify service with optional
@fastify/websocket real-time sync and @lilith/service-lib JWT
authentication. REST surface (src/app.ts) under /api/v1:
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/games |
List games |
| POST | /api/v1/games |
Create a game |
| GET | /api/v1/games/:gameId |
Get full game state |
| POST | /api/v1/games/:gameId/join |
Join a game |
| GET | /api/v1/games/:gameId/state |
Player view (fog-of-war) |
| POST | /api/v1/games/:gameId/actions |
Submit an action |
| GET | /api/v1/games/:gameId/actions |
Action history |
| GET | /api/v1/games/:gameId/players |
List players |
| POST | /api/v1/games/:gameId/snapshots |
Save game state |
| GET | /api/v1/games/:gameId/snapshots |
List snapshots |
| POST | /api/v1/games/:gameId/snapshots/:snapshotId/load |
Restore a snapshot |
| DELETE | /api/v1/games/:gameId/snapshots/:snapshotId |
Delete a snapshot |
| GET | /api/v1/ws/game |
WebSocket sync (when enabled) |
| GET | /api/v1/realtime/stats |
Real-time stats |
Five game modes are wired at game-creation time, each with its own engine and
route module: Echoes of Fate (echoes-of-fate/timeline-engine.ts,
routes/temporal-routes.ts), Resonance Wars
(resonance-wars/combat-engine.ts, routes/resonance-wars-routes.ts), Weave
Conspiracy social deduction (weave-conspiracy/conspiracy-engine.ts,
routes/weave-conspiracy-routes.ts), Eternal Game area control
(eternal-game/influence-engine.ts, routes/eternal-game-routes.ts), and
Grand Campaign multi-session meta-game
(grand-campaign/campaign-manager.ts, routes/grand-campaign-routes.ts). The
service also includes per-mode AI opponents (src/ai/*) and a balance pipeline
(src/balance/* — skill rating, pattern tracking, tournament runner).
4.5 Studio Web (@hathor/studio-web)#
apps/hathor/studio-web is a small package that defines the V2 fighting-game
narrative content map (src/v2-narrative-content.ts). It declares five
narrative surfaces (story, side_story, krypt, chronicles, dj_story),
the content packs and beats per surface, and the compiled-artifact plans
(dialogue banks, sequencer outlines, quest graphs, timeline data, rivalry
matrices, codex entries, localization keys) emitted to
V2/ue/Content/Generated/.... Its only dependency is @hathor/lore-compiler.
It is not an HTTP server.
5. Events#
All events are delivered via @oshun/event-bus (Redis-backed in
@hathor/event-publisher). The event contracts are defined in
@oshun/contracts (libs/contracts/src/events/hathor.ts).
5.1 Events Published (@hathor/event-publisher)#
HathorEventPublisher
(libs/hathor/event-publisher/src/hathor-event-publisher.ts) publishes seven
event types. It uses a singleton accessor pattern (getHathorEventPublisher /
factory createHathorEventPublisher) and a default Redis key prefix of
oshun:events. Publish failures are swallowed (logged, not thrown).
| Event | Publisher method | Default targets |
|---|---|---|
hathor.world.created |
publishWorldCreated |
isis, bellona |
hathor.world.published |
publishWorldPublished |
bellona, yemaya |
hathor.element.added |
publishElementAdded |
(none) |
hathor.narrative.generated |
publishNarrativeGenerated |
sophia |
hathor.simulation.started |
publishSimulationStarted |
(none) |
hathor.simulation.completed |
publishSimulationCompleted |
bellona |
hathor.world.validated |
publishWorldValidated |
(none) |
The Zod payload schemas (from @oshun/contracts) carry richer information than
a simple ID list. Key fields per event type:
HathorWorldCreatedPayload—worldId,projectId,userId,name,type(WorldType:narrative | game | simulation | virtual_production | educational | experimental),description?, andsettings(scale,timeScale,physics).HathorWorldPublishedPayload—worldId,projectId,userId,version,previousVersion?,statistics(location/character/item/rule counts, total assets and bytes),exportFormats,validationPassed,publishedAt.HathorElementAddedPayload—elementId,worldId,projectId,elementType(location | character | item | event | rule | relationship),name,parentElementId?,attributes?.HathorNarrativeGeneratedPayload—narrativeId,worldId,projectId,userId,type(backstory | plot | dialogue | description | lore),title,characterCount,wordCount,referencedElements,coherenceScore?,generationTimeMs.HathorSimulationStartedPayload/HathorSimulationCompletedPayload—simulationId,worldId,projectId,type(physics | social | economic | ecological | narrative), simulation parameters/results, artifacts, duration, success.HathorWorldValidatedPayload—worldId,projectId,validationId,passed,checks[](each withname,category,passed,severity,message,affectedElements?),score(0–100),durationMs.
5.2 Events Consumed (@hathor/event-handlers)#
setupHathorEventHandlers (libs/hathor/event-handlers/src/index.ts)
subscribes to four upstream events via @oshun/event-bus. Subscriptions use
consumer-group support, graceful shutdown, and a getHandlerRegistrations()
helper that assigns per-event concurrency so a burst from one source does not
block the others.
| Event | Handler file | Concurrency |
|---|---|---|
sophia.document.ingested |
handlers/sophia-document-ingested.ts |
5 |
isis.asset.generated |
handlers/isis-asset-generated.ts |
10 |
yemaya.project.created |
handlers/yemaya-project-created.ts |
3 |
yemaya.character.created |
handlers/yemaya-character-created.ts |
10 |
Handlers receive a HathorHandlerContext with repositories for worlds,
suggestions, document references, asset links, character visual attributes,
character summaries, relationship suggestions, character arcs, and a job-queue
client (event-handlers/src/types.ts).
6. Configuration and Environment Variables#
The following environment variables must be set for a full Hathor deployment.
Port defaults are hard-coded in each service and overridden by PORT (Veilborn
services) or DEFAULT_APP_CONFIG (simulation worker).
# Database (Prisma)
HATHOR_DATABASE_URL=postgresql://...
# Event bus (Redis)
REDIS_URL=redis://localhost:6379
HATHOR_EVENTS_ENABLED=true # set to "false" to disable publishing
# Service ports (defaults)
# world-api 3001
# narrative-api 3002
# simulation-worker 3004
# veilborn-core 8080 (PORT)
# veilborn-strategy 8080 (PORT)
@hathor/event-publisher reads REDIS_URL and HATHOR_EVENTS_ENABLED. The
simulation worker's default port (3004) is hard-coded in DEFAULT_APP_CONFIG.
7. Integration Points#
7.1 Sophia Integration (@hathor/sophia-integration)#
Sophia is the only domain that Hathor calls synchronously (via direct API call rather than events). This is because citation lookup and lore fact-checking are interactive operations that require an immediate response. The three services in this library each address a different aspect of the Sophia integration:
| Service | File | Description |
|---|---|---|
CitationService |
citation-service.ts |
Link world lore to Sophia research sources |
ResearchGroundingService |
research-grounding.ts |
Generate world content grounded in researched knowledge |
LoreValidationService |
lore-validation.ts |
Fact-check world lore against Sophia's research corpus |
7.2 Bellona Consumption of Hathor Artifacts#
The Bellona-facing compiler and consumer are implemented in the Bellona
domain, not in Hathor. This boundary exists because Bellona owns knowledge of
engine project formats — Hathor produces engine-neutral compiled artifacts and
Bellona converts them to engine-native form. The relevant Bellona files are
libs/bellona/integration/src/hathor/lore-compiler.ts and
libs/bellona/integration/src/hathor/artifact-consumer.ts. When
hathor.world.published is emitted, Bellona consumes it to produce
engine-native artifacts. Hathor's own @hathor/lore-compiler produces the
upstream compiled artifacts (see § 8).
7.3 Shared Oshun Libraries Used#
| Library | Usage |
|---|---|
@oshun/event-bus |
Event publishing and subscription |
@oshun/contracts |
Hathor event schemas and HathorEventTypes |
@oshun/logging |
Structured logging |
8. Library Catalog#
This section documents the implementation details of each Hathor library — the files that exist, the entry points, and the key types and functions exported.
8.1 @hathor/lore-compiler#
Compiles narrative content into engine-ready and film-ready artifacts
(libs/hathor/lore-compiler/src). Three compilers are exposed via factory
functions:
- Quest Compiler (
engine/quest-compiler.ts,createQuestCompiler) — compiles quests for target engines.TargetEngineisunreal | unity | godot | blender | custom;EngineArtifactFormatisjson | yaml | xml | binary | sqlite. - Screenplay Compiler (
film/screenplay-compiler.ts,createScreenplayCompiler) — compiles a story into aCompiledScreenplayand exports it.FilmArtifactFormatisfountain | fdx | pdf | html | json | avid_log. Beats use the 15-beatFilmBeatTypeset (Save-the-Cat structure). - Bellona Package Builder (
bellona/package-builder.ts,BellonaPackageBuilder) — assembles aBellonaPackage(manifest, world, quests, characters, locations, items, dialogues, optional screenplay, assets, localization).
Key compiled types: CompiledQuest, CompiledObjective, QuestPrerequisite,
QuestReward, DialogueTree/DialogueNode, CompiledScreenplay,
CompiledBeat, CompiledScene, CompilationResult<T>.
8.2 @hathor/validation#
Four lore validators plus a combined runner (libs/hathor/validation/src):
createTimelineValidator, createCausalityValidator,
createTaxonomyValidator, createContradictionDetector, and
createLoreValidator() which runs all four. LoreValidator.validate() accepts
events, eras, timeline, characters, and locations, respects failFast and
maxIssuesPerCategory, and returns a LoreValidationResult with per-validator
results, allIssues, and a summary (errorCount, warningCount,
infoCount, validatorsRun, duration). The validation lib's
ValidationSeverity has three values: error | warning | info.
8.3 @hathor/narrative#
Engine-agnostic narrative systems (libs/hathor/narrative/src): quest/ (quest
engine), dialogue/ (dialogue engine), story-graph/ (typed-node graph with
traversal), journal/ (journal/codex system), branching/ (branching
utilities), and export/ — exporters for Ink (ink-exporter.ts), Yarn Spinner
(yarn-exporter.ts), and JSON (json-exporter.ts) selected by
exporter-factory.ts.
8.4 @hathor/simulation#
The largest domain library (libs/hathor/simulation/src): four simulation
managers — economy/manager.ts, politics/manager.ts, culture/manager.ts,
scenario/manager.ts — each with a providers.ts and typed inputs; an NPC
behavior-tree system (npc/behavior-tree.ts); state persistence
(state/state-store.ts, state/postgres-storage.ts, state/redis-cache.ts);
and a physics engine (physics/engine.ts, physics/math.ts). There is no
ecology module.
8.5 @hathor/llm-npc#
LLM-powered NPC system (libs/hathor/llm-npc/src): brain/ — personality
(personality-manager.ts), emotional state, memory, world awareness;
dialogue/ — dialogue-generator.ts, safety-filter.ts; behavior/ —
behavior-controller.ts, npc-conversation.ts; platforms/ — llm-client.ts,
nvidia-ace-client.ts, inworld-client.ts, convai-client.ts; a
provider-chain.ts with circuit-breaker failover; and an advanced/npc-sota.ts
module.
8.6 @hathor/theory#
Three theory subsystems (libs/hathor/theory/src): mda/ (MDA framework),
narrative/ (narrative theory — fabula/syuzhet, beat sheets, structure
templates), cinematography/ (shot/composition/lighting/color planning). Each
has a manager.ts, types.ts, and constants.ts.
8.7 @hathor/pre-production#
Pre-production tooling (libs/hathor/pre-production/src):
scriptwriting/— chronicle document parser (parser.ts), templates, and an exporter.storyboard/—storyboard-manager.tswith frames, sequences, layers, camera specs, transitions, character placement, and an exporter.planning/— aproject-manager.tsfor worldbuilding project management (phases, milestones, tasks, dependencies,checklists.ts), plus a very large library of virtual-production / on-set systems (~240*-system.tsanddrone-*modules covering drone choreography and safety, virtual sets, LED-wall ICVFX, relighting, motion capture, switching, color, and continuity). Each module has a matching.test.ts.
8.8 @hathor/domain-models#
Engine-agnostic worldbuilding domain models (libs/hathor/domain-models/src).
Nine subdomains, each with types.ts, constants.ts, factories.ts,
utils.ts, validators.ts: common/, faction/, economy/, law/,
culture/, geography/, timeline/, character/, location/. This library
holds the pure world type system. The Git-like versioning code (versions,
branches, merge requests) lives in @hathor/world-api's services, not here.
8.9 @hathor/client#
TypeScript SDK for the Hathor HTTP APIs (libs/hathor/client/src).
HathorClient / createClient / createClientFromEnv expose three resources:
WorldResource (api/world.ts), NarrativeResource (api/narrative.ts),
SimulationResource (api/simulation.ts). Branded ID types: WorldId,
VersionId, BranchId, EntityId, StoryGraphId, QuestId, DialogueId,
JobId. Helpers (helpers/): world-state (getWorldStateSnapshot,
buildRelationshipMap, findConnectedEntities, getTimelineRange,
getEntityTimeline, analyzeFaction, compareVersions, searchWorld,
findEntities); quest generation (generateQuestFromTemplate,
generateQuestChain, analyzeQuest, getQuestRecommendations,
QUEST_TEMPLATES); lore validation (validateWorldLore, validateEntity,
checkLoreImpact, getValidationRules).
8.10 Façade Libraries#
Four thin façade libraries adapt Hathor domain types for cross-domain (CGI / production) consumption. The façade pattern keeps the CGI pipeline's interface stable — consumers see a minimal, stable packet type rather than the full internal domain model. Each façade exports metadata, a packet builder, a validator, and a serializer:
@hathor/characters— CGI character-definition import facade. MapsCharacterrecords into aHathorCharactersCgiPacket(appearance, costume designs, visual references);createHathorCharactersCgiPacket,validateHathorCharactersCgiPacket. Depends on@hathor/domain-modelsand@hathor/pre-production.@hathor/world— CGI scene-definition import facade. Depends on@hathor/domain-models.@hathor/timeline— narrative scene-order facade (story-order vs. shoot-order reconciliation, causal-dependency checking, world-state continuity). Depends on@hathor/domain-models.@hathor/quests— scene-dependency / story-arc capture facade. Depends on@hathor/narrative.
9. Common Error Format#
All three Hono-based Hathor APIs return errors in a consistent JSON envelope. Understanding this shape is important when building integrations that need to distinguish error types programmatically.
{
"error": {
"code": "NOT_FOUND",
"message": "World not found: wld_xyz"
}
}
Common codes seen in the route handlers: NOT_FOUND, VALIDATION_ERROR,
CREATE_FAILED, CLONE_FAILED, QUERY_FAILED, MERGE_FAILED,
CONFLICTS_EXIST, INVALID_TYPE, QUEUE_FULL, INTERNAL_ERROR. Zod
validation failures surface via the global onError handler with a details
array. The Veilborn Fastify services use a different envelope: a top-level
{ success, data?, error?, timestamp } shape with error.code and
error.message.
SDK Error Hierarchy (@hathor/client)#
The SDK maps HTTP error responses to a typed exception hierarchy so callers can
catch specific error conditions without inspecting response codes manually.
HathorError is the base, with subclasses NetworkError, TimeoutError,
AbortError, NotFoundError, ValidationError, ConfigError,
UnauthorizedError, ForbiddenError, ConflictError, RateLimitError.
createErrorFromResponse maps HTTP responses to the right subclass.
10. V2 Fighting-Game Narrative Contract (planned)#
The V2 fighting-game project (V2/) is a named Hathor consumer for Story Mode,
per-fighter Side Stories, the Krypt, Chronicles of the Sword, Def Jam Story, and
the rivalry matrix. The integration contract is documented in
V2/V2_ARCHITECTURE.md, V2/V2_DEPENDENCIES.md, and
V2/docs/integration/hathor-narrative-contract.md.
Implemented today: apps/hathor/studio-web/src/v2-narrative-content.ts
defines the V2 narrative content map (five surfaces, content packs, beats, and
compiled-artifact plans) and a coverage assertion. It targets
@hathor/lore-compiler and the unreal engine, JSON output,
compiled-artifact-only consumption.
Planned (not yet in code). The following are described in the V2 contract
docs as reciprocal Hathor backlog and are not implemented in libs/hathor/*
today. They are listed here because they are an explicit design contract, not
invented detail:
- Extend
@hathor/domain-models/@hathor/narrativewith fighting-game entity shapes:FighterTimeline,RivalryMatrix,LadderEndingGraph,SignatureStageContext(and relatedFactionMatrix,SideStorySequencerOutline). TheLadderEndingGraphis the per-fighter arcade/ladder ending branch tree that the lore compiler emits toV2/ue/Content/Generated/Quests; per the contract, every fighter has at least one ladder ending graph and eacharcadeEndingIdsentry must resolve to one. - Extend
@hathor/llm-npcwith combat-bark and fighting-game personality presets, plus open-world NPC hooks (npc.encounter.start,npc.dialogue.request,npc.behavior.tick,npc.combat.barks); NPC outputs cached off-rollback atV2/ue/Content/Generated/NPCDialogue/<hash>. - Extend
@hathor/lore-compilerwith the V2 emission contract (dialogue banks, Sequencer outlines, quest graphs, fighter-timeline and rivalry-matrix UE data assets, codex entries, localization keys). - Extend
@hathor/validationwith rivalry-consistency and Themis NIL / venue rights gates. - Add a fighting-game scenario-generation preset to
@hathor/simulation.
Planned V2-related events on the bus (per the contract doc):
hathor.dialogue.ready, hathor.rivalry.updated, hathor.timeline.branched.
These are not in @oshun/contracts today — only the seven events in § 5.1
are implemented.
11. Acceptance Criteria#
A Hathor deployment is correct when all of the following hold:
@hathor/databasemigrates cleanly and exposes the 20 models / 19 enums in § 1–2;getHathorClientconnects and disconnects.world-api(3001) serves the world, branch, version, merge, and query routes in § 4.1 behind the 100-req/min rate limit and theX-User-IDmutation check;/readyreports both the world and version services.narrative-api(3002) serves the story-graph, quest, dialogue (including runtime sessions), and validation routes in § 4.2.simulation-worker(3004) accepts, runs, lists, cancels, and retries simulation jobs of all five types and reports worker health and queue stats.@hathor/event-publisherpublishes the sevenhathor.*events with the contract payloads in § 5.1;@hathor/event-handlerssubscribes to the four upstream events in § 5.2.@hathor/lore-compilerproduces quest, screenplay, and Bellona-package artifacts;@hathor/validation'screateLoreValidator()runs all four validators.- The Veilborn services build and serve their game and strategy APIs (§ 4.4) with passing integration tests.
@hathor/clientround-trips against all three HTTP APIs with its branded ID types and error hierarchy.