Appearance
Character System
Overview
The Character System is the foundation of the relationship-mapping side of the Progressive World-Building Platform. It allows project members to create, manage, and connect characters that populate their collaborative worlds -- whether those worlds are tabletop RPG campaigns, serialized fiction universes, or other creative projects.
Characters come in two flavours: Player Characters (PCs), owned and managed by individual players, and Non-Player Characters (NPCs), created by Storytellers, Co-Creators, or the project Owner. Each character carries structured identity data (name, type, visibility) alongside a flexible JSONB metadata payload that adapts to any genre or system without requiring schema migrations.
The system's centrepiece is the character relationship graph. Relationships between characters are typed, weighted, optionally directional, and permission-filtered -- enabling rich social webs that range from family trees to supernatural bloodlines. A dedicated graph endpoint assembles this data into a pre-built node-and-edge structure ready for frontend visualization.
Goals
- Allow players to create and manage their own PC within a project
- Allow Storytellers, Co-Creators, and Owners to populate the world with NPCs
- Support flexible, schema-free character metadata for any genre or game system
- Model rich, typed relationships between characters with configurable directionality
- Expose a permission-filtered relationship graph endpoint for frontend visualization
- Enforce visibility rules so private characters and relationships are fully omitted from unauthorized responses
- Support character images for visual identification on profiles and graph nodes
User Stories
Game Master / Storyteller
- As a Storyteller, I want to create NPCs with custom metadata fields (clan, generation, haven) so I can track system-specific details alongside narrative ones.
- As a Storyteller, I want to create private NPCs that only I and the Owner can see, so I can prepare plot-relevant characters before revealing them.
- As a Storyteller, I want to define relationships between any characters (including between two PCs) so I can establish the social fabric of the world.
- As a Storyteller, I want to mark certain relationships as private so players discover connections organically during play.
- As a Storyteller, I want to view the full relationship graph -- including private characters and relationships -- so I have complete situational awareness.
Writer / Co-Creator
- As a Co-Creator, I want to create NPCs and link them into the relationship web so I can contribute to world-building without full Storyteller access.
- As a Co-Creator, I want to attach images to characters so the team has visual references.
Player
- As a Player, I want to create my own PC with a name, description, image, and custom metadata so my character is represented in the project.
- As a Player, I want to define relationships from my character to other characters (PCs or public NPCs) so I can express my character's social connections.
- As a Player, I want to view the public relationship graph so I can understand the known social landscape of the world.
Functional Description
Character CRUD
Characters are aggregate roots within the KnowledgeBase.Characters feature module. Each character belongs to a single project and receives a project-scoped sequence number on creation (e.g., VNO-42).
Creation rules by role:
| Role | Can Create PC? | Can Create NPC? | Notes |
|---|---|---|---|
| Owner | Yes | Yes | Full access |
| Storyteller | Yes | Yes | Full access |
| Co-Creator | Yes | Yes | Full access |
| Player | Own PC only | No | Limited to one PC per player per project |
| Viewer | No | No | Read-only access |
- A Player may only have one PC per project. Attempting to create a second PC returns a validation error.
- Owners, Storytellers, and Co-Creators have no such limit and may create both PCs and NPCs freely.
Update and delete rules:
- Players may update or delete only their own PC.
- Storytellers and Owners may update or delete any character.
- Co-Creators may update or delete characters they created.
- Deleting a character cascades: all relationships where the character is source or target are also removed, along with any faction memberships (see Faction System).
Character Types: PC vs NPC
| Property | PC | NPC |
|---|---|---|
| Created by | Any role (Players limited to their own) | Owner, Storyteller, Co-Creator |
| Limit per player | 1 per project | N/A |
| Typical use | Player avatars | World population |
| Ownership | Bound to creating player | Bound to creating user |
The type field is an enum (pc / npc) set at creation time and immutable -- a PC cannot be converted to an NPC or vice versa.
Character Metadata (JSONB)
The metadata column is a PostgreSQL JSONB field that stores arbitrary key-value data. This design allows every project to define its own character schema without database migrations.
Example metadata for a Vampire: The Masquerade character:
{
"clan": "Ventrue",
"generation": 10,
"haven": "Penthouse at Elysium Towers",
"sire": "Marcus Vitel"
}Example metadata for a D&D character:
{
"class": "Paladin",
"race": "Dragonborn",
"level": 7,
"alignment": "Lawful Good"
}The backend treats metadata as opaque JSON -- it stores, retrieves, and returns it without validation. Schema enforcement, if desired, is a frontend concern or a future enhancement.
Character Images
Characters may optionally have an associated image (portrait, token, etc.).
- Images are uploaded through a dedicated upload mechanism (file storage strategy is a pending team decision).
- The
imageUrlfield on the Character entity stores the URL to the uploaded image. - Image updates replace the previous URL. The old image file should be cleaned up by a background process or storage lifecycle policy.
- Image deletion sets
imageUrlto null.
Character Visibility
Each character has a visibility field: public or private.
- Public characters are visible to all project members (including Players and Viewers).
- Private characters are visible only to the character's creator, plus any user with the Storyteller or Owner role.
Private characters are fully omitted from API responses for unauthorized users -- not redacted, not hinted at, simply absent. This applies to list endpoints, detail endpoints, and the graph endpoint.
Character Relationships
Relationships are the connective tissue of the character system. Each relationship is an entity (not an aggregate root) belonging to a project, linking two characters with typed, weighted, and directional metadata.
Relationship Types and Subtypes
| Type | Example Subtypes |
|---|---|
| family | parent, child, sibling, spouse, cousin, ancestor |
| social | friend, enemy, rival, mentor, ally, acquaintance |
| professional | employer, employee, partner, client, colleague |
| supernatural | sire, childe, blood bond, pack mate, thrall |
| custom | User-defined freeform subtype string |
The type field is an enum; the subtype field is a freeform string allowing project-specific terminology.
Relationship Properties
| Property | Type | Description |
|---|---|---|
| strength | int (1-5) | Intensity of the relationship. 1 = tenuous, 5 = defining |
| status | enum | active (current), ended (historical), secret (in-world secret) |
| visibility | enum | public (visible to all members) or private (creator + Storyteller + Owner only) |
| isBidirectional | bool | Whether the relationship reads the same from both sides |
| description | string? | Optional narrative description of the relationship |
Bidirectionality (per ADR-009)
Each relationship stores a single row with sourceCharacterId and targetCharacterId. The creator chooses whether the relationship is bidirectional or directional.
- Bidirectional relationships (e.g., "Friends", "Siblings", "Rivals") are returned when querying from either character's perspective. The query checks both the source and target columns.
- Directional relationships (e.g., "Mentor -> Student", "Sire -> Childe") are returned only from the source character's perspective by default. The inverse perspective uses a corresponding inverse label.
Inverse labels for directional relationships:
| Source Label | Inverse (Target) Label |
|---|---|
| mentor | student |
| sire | childe |
| parent | child |
| employer | employee |
| master | apprentice |
| creator | creation |
For directional relationships, the API returns the appropriate label based on which character's perspective is being queried. When viewing Character A's relationships and they are the target of a directional relationship, the inverse subtype label is displayed.
Multiple Relationships Between Characters
Two characters may have multiple relationships simultaneously. For example, Marcus and Elena could be both social/friend and professional/colleague. Each relationship is a separate entity with its own type, subtype, strength, status, and visibility.
A uniqueness constraint prevents exact duplicates (same source, target, type, and subtype combination) but allows different types or subtypes between the same pair.
Relationship Permission Rules
| Role | Can Create? | Can View? |
|---|---|---|
| Owner | Any characters | All relationships (including private) |
| Storyteller | Any characters | All relationships (including private) |
| Co-Creator | Any characters they have access to | Public relationships + own private |
| Player | From own PC only | Public relationships + own private |
| Viewer | No | Public relationships only |
- Players may only create relationships where their own PC is the source character.
- Private relationships are fully omitted from responses for unauthorized users.
Character Relationship Graph Endpoint
GET /api/projects/{key}/graphs/characters
This endpoint returns a pre-assembled graph data structure optimized for frontend visualization libraries. It performs all permission filtering server-side so the frontend receives only the data the current user is authorized to see.
Graph Response Structure
The response contains two arrays: nodes (characters) and edges (relationships).
Node properties:
| Field | Description |
|---|---|
| id | Character sequence number |
| name | Character name |
| type | pc or npc |
| imageUrl | Character portrait URL (nullable) |
| metadata | Subset of metadata for display (configurable) |
Edge properties:
| Field | Description |
|---|---|
| id | Relationship internal ID |
| source | Source character sequence number |
| target | Target character sequence number |
| type | Relationship type (family, social, etc.) |
| subtype | Relationship subtype (friend, sire, etc.) |
| strength | 1-5 weight for edge thickness |
| isBidirectional | Whether the edge has arrows on both ends |
| status | active, ended, or secret |
Graph Assembly Pipeline
Step G is important: even if a relationship is public, if one of its endpoint characters has been filtered out (because the character is private and the user lacks access), the edge must also be removed. This prevents information leakage about the existence of hidden characters.
Player-Driven Relationship Building
Players participate in world-building by defining their own character's social connections. The workflow is:
- Player creates their PC (one per project).
- Player browses the list of public characters (PCs of other players, public NPCs).
- Player creates a relationship from their PC to another character, choosing type, subtype, directionality, and strength.
- The relationship appears in the graph for all users who have visibility.
Players cannot create relationships between two characters that are not their own. This prevents Players from defining connections they do not control. Storytellers and Owners can create relationships between any characters.
Data Flow
Character Creation Flow
Relationship Creation Flow
Key Components
CharactersController
Handles all character CRUD endpoints and delegates to the appropriate command/query handlers. Routes follow the pattern /api/projects/{key}/characters/{seq}.
RelationshipsController
Handles character relationship CRUD endpoints. Routes follow the pattern /api/projects/{key}/characters/relationships/{id}.
CharacterGraphController
Handles the GET /api/projects/{key}/graphs/characters endpoint. Orchestrates the graph assembly pipeline: loading data, filtering by permissions, and structuring the node-and-edge response.
CharacterService
Core business logic for character lifecycle: creation (with sequence number allocation), updates, deletion (with cascade cleanup), and validation of business rules (one PC per player, type immutability).
RelationshipService
Business logic for relationship management: creation (with duplicate detection), updates, deletion, and bidirectional query logic per ADR-009.
IPermissionService (shared)
Consulted by all handlers to determine what the current user can create, edit, view, and delete. Implements role-based filtering rules described in this document. Shared across feature modules (see Permission & Visibility).
Diagrams
Relationship Data Model
Character Visibility Filtering
Feature Interactions
| Feature | Interaction |
|---|---|
| Project Management | Characters belong to a project. Project membership and roles drive permission checks. Character creation increments the project's shared sequence counter. |
| Faction System | Characters can be members of factions via FactionMembership. Deleting a character must cascade to remove faction memberships. The faction graph may reference character counts. See Faction System. |
| Permission & Visibility | All character and relationship operations go through IPermissionService. Visibility filtering is applied on every read path. See Permission & Visibility. |
| Real-time & Collaboration | Character creation, updates, and relationship changes raise domain events that feed into the activity stream and trigger SignalR notifications. See Real-time & Collaboration. |
| Version History | Character edits and relationship changes are tracked in the version history system for audit and rollback. See Version History & Changelog. |
| Search & Navigation | Characters and their metadata are indexed for project-wide search. See Search & Navigation. |
Edge Cases & Error Handling
| Scenario | Handling |
|---|---|
| Player tries to create a second PC | Return 409 Conflict with a clear message. Query existing PCs for the player before insert. |
| Player tries to create an NPC | Return 403 Forbidden. Role check rejects the request. |
| Player creates relationship from another player's PC | Return 403 Forbidden. Source character must be the requesting player's own PC. |
| Relationship references a private character the user cannot see | Return 404 Not Found. Do not reveal the character exists. |
| Duplicate relationship (same source, target, type, subtype) | Return 409 Conflict. Prompt user to edit the existing relationship or choose a different subtype. |
| Deleting a character with existing relationships | Cascade delete all relationships where the character is source or target. Also remove faction memberships. |
| Deleting a character with existing faction memberships | Cascade delete all faction memberships for the character. |
| Graph request with no visible characters | Return an empty graph structure: { nodes: [], edges: [] }. |
| Metadata field exceeds reasonable size | Enforce a maximum JSONB payload size (e.g., 32 KB) at the API validation layer. |
| Concurrent sequence number allocation | Handled by PostgreSQL atomic UPDATE ... RETURNING on the project's counter. No application-level locking needed. |
| Relationship between two private characters, viewer is unauthorized for both | Relationship and both characters are omitted entirely. No information leakage. |
Phase & Priority
| Aspect | Value |
|---|---|
| Phase | Phase 2: Relationship Mapping |
| Priority | High -- characters and relationships are the core of Phase 2 |
| Dependencies | Requires Phase 1 (Project Management, Auth) to be complete |
| Dependents | Faction System (memberships reference characters), Collaboration features (comments on characters) |