Skip to content

Master Overview: Progressive World-Building Platform Backend

1. System Overview

The Progressive World-Building Platform is a collaborative content roadmap and relationship mapping tool designed for tabletop RPG campaigns, serialized fiction, and creative writing projects. It enables Storytellers, Writers, and Players to organize content releases, visualize character and faction relationships, and collaborate in real time with role-based visibility controls.

The backend is built as a modular monolith in C# / .NET with PostgreSQL, following Domain-Driven Design patterns. Each feature is implemented as a separate solution project with its own entities, business logic, and API endpoints, while sharing common infrastructure (database, authentication, real-time messaging, permission evaluation).

Feature Map

#FeatureDescriptionDoc
1Authentication & User ManagementRegistration, login, JWT sessions, password reset, invitation bridge01
2Project ManagementProject CRUD, keys, visibility, member invitations, role hierarchy, sequence counter02
3Content Timeline SystemHierarchical content releases (updates > sections > items) with development status lifecycle03
4Character SystemPC/NPC characters, JSONB metadata, typed relationships, character graph endpoint04
5Faction SystemOrganizations with nested hierarchies, memberships, inter-faction relationships, faction graph05
6Permission & VisibilityIPermissionService interface, role-based access matrix, cascading visibility filtering06
7Real-time & CollaborationSignalR hub, activity feed, comments with @mentions, notifications, editing indicators07
8Version History & ChangelogAutomatic version snapshots, diff/compare, restore; curated creator-authored changelog08
9Search & NavigationPostgreSQL full-text search, bookmarks, recently viewed, export, bulk ops, templates09

Solution Structure

KnowledgeBase.sln
src/
  Shared/
    KnowledgeBase.Domain/              # Base classes, enums, interfaces
    KnowledgeBase.Infrastructure/      # DbContext, auth, SignalR, email
    KnowledgeBase.Common/              # Pagination, error handling, permissions
  Features/
    KnowledgeBase.Projects/            # Feature 2
    KnowledgeBase.Timelines/           # Feature 3
    KnowledgeBase.Characters/          # Feature 4
    KnowledgeBase.Factions/            # Feature 5
    KnowledgeBase.Collaboration/       # Features 7, 8 (comments, activity, versions)
  KnowledgeBase.API/                   # Thin host -- wires everything together
tests/
  KnowledgeBase.Projects.Tests/
  KnowledgeBase.Timelines.Tests/
  KnowledgeBase.Characters.Tests/
  KnowledgeBase.Factions.Tests/
  KnowledgeBase.Collaboration.Tests/

Features 1 (Auth), 6 (Permissions), and 9 (Search) are implemented within the Shared and Infrastructure projects because they provide cross-cutting services consumed by every feature module.


2. High-Level Architecture

Request flow summary: Client sends an HTTP request with a JWT -> Auth Middleware validates the token and populates ICurrentUserService -> Controller dispatches to the appropriate feature handler -> Handler checks permissions via IPermissionService -> Handler executes business logic and persists via EF Core -> Aggregate root raises domain events -> Events trigger activity feed entries, search index updates, and SignalR broadcasts -> Filtered response returned to client.


3. Feature Dependency Map

Reading the graph:

  • An arrow from A to B means "B depends on A" (A must exist for B to function).
  • Authentication is the root dependency -- every feature requires user identity.
  • Project Management is the next layer -- all content lives within projects.
  • Permissions is consumed by every content and collaboration feature.
  • Characters feeds into Factions (faction memberships reference characters).
  • Real-time and Versions are consumers of all content features via domain events.
  • Search indexes content from Timelines, Characters, and Factions.

4. Cross-Cutting Concerns

4a. Authentication Context

Every API request carries an authenticated user identity. The JWT validation middleware runs first in the pipeline, extracts claims from the access token, and populates ICurrentUserService -- a scoped (per-request) service providing UserId (UUIDv7), Email, DisplayName, and IsAuthenticated. Every feature handler injects this service to identify the caller. No feature module reads JWT claims directly.

SignalR connections authenticate with the same JWT, passed during the WebSocket handshake. The ProjectHub validates the token on connection and uses the identity to determine group membership.

4b. Permission Checks

IPermissionService is the single authorization interface for the entire platform. It lives in the Application layer and exposes two categories of methods:

  • Action checks (e.g., CanEditTimeline, CanCreateCharacter) -- called before mutations. Returns bool; handler returns 403 on failure.
  • Visibility filters (e.g., FilterByVisibility) -- called before returning data. Accepts a collection, returns only items the user may see.

The Phase 1 implementation (RoleBasedPermissionService) evaluates a hardcoded permission matrix based on the user's role in the project. It is registered as a scoped service, caching the role lookup for the duration of the request. The implementation is designed to be swappable via DI registration for future configurable per-project permissions.

The pattern every handler follows:

  1. Call an action check before executing the command.
  2. If reading data, apply visibility filters before assembling the response.
  3. For graph endpoints, apply cascading filters (characters filtered before relationships, so orphaned edges are removed).

4c. Audit Fields

All entities inherit from a base Entity class that includes:

  • CreatedAt (DateTime) -- set on insert
  • CreatedBy (Guid) -- set from ICurrentUserService.UserId on insert
  • UpdatedAt (DateTime) -- set on every update
  • UpdatedBy (Guid) -- set from ICurrentUserService.UserId on every update

These fields are populated automatically by the DbContext SaveChanges override, ensuring consistent audit data without requiring each feature to manage them explicitly.

4d. Public ID System

Internal database identifiers are UUIDv7, never exposed to users. Every aggregate root receives a project-scoped sequence number on creation, forming a public ID in the format {ProjectKey}-{SequenceNumber} (e.g., VNO-42).

The sequence counter lives on the Project entity (NextSequenceNumber) and is incremented atomically at the database level using UPDATE ... RETURNING within the same transaction as the entity creation. This guarantees no duplicates under concurrency. The counter is shared across all entity types within a project -- a timeline and a character in the same project receive sequential numbers from the same pool.

API routes and responses use the project key and sequence number for all public-facing references. Internal UUIDs are used only within the database and domain layer.

4e. Domain Events

Aggregate roots (Project, TimelineUpdate, Character, Faction) raise domain events when significant state changes occur (e.g., CharacterCreatedEvent, TimelineStatusChangedEvent). Events are dispatched after successful persistence (post-commit) to ensure referenced data exists in the database.

The event pipeline:

Event handlers are responsible for creating activity feed entries, updating the search index, generating notifications, and broadcasting real-time updates. Each handler independently processes the event, and the SignalR broadcaster applies per-user permission filtering before delivery.

4f. Error Handling

The platform uses a standardized error response format across all features:

  • 400 Bad Request -- validation failures (malformed input, missing required fields)
  • 401 Unauthorized -- missing or invalid authentication token
  • 403 Forbidden -- authenticated user lacks the required role/permission
  • 404 Not Found -- entity does not exist, or user is not authorized to know it exists (private projects return 404 to non-members, not 403)
  • 409 Conflict -- business rule violation (duplicate project key, duplicate membership, one-PC-per-player)
  • 422 Unprocessable Entity -- structurally valid but semantically invalid (invalid status transition, nesting depth exceeded)

Validation is performed in two layers: model-level validation (format, required fields) in the controller, and business-rule validation (uniqueness, state transitions, role checks) in the handler. Errors include a machine-readable error code and a human-readable message.

4g. Pagination

The platform uses two pagination strategies:

StrategyUsed ForParametersResponse Fields
Cursor-basedActivity feed, search results, notifications, recently viewedcursor (opaque), limit (max 100)nextCursor, hasMore
Offset-basedTimeline updates, characters, factions, bookmarkspage, pageSize (max 100)totalCount, page, pageSize, totalPages

Cursor-based pagination is preferred for feeds where items may be added between requests. Offset-based pagination is used for stable, ordered collections. Both strategies enforce a maximum of 100 items per page.

4h. Real-time Broadcasting

When a domain event triggers a SignalR broadcast, the PermissionAwareBroadcaster service:

  1. Identifies the project group from the event's project ID.
  2. Determines whether the event involves restricted content (private characters, secret faction relationships, unpublished timelines).
  3. For public content events, broadcasts to the entire project group.
  4. For restricted content events, iterates through connected users in the group, checks each user's visibility via IPermissionService, and sends only to authorized connections.

This reuses the same permission logic as the REST API, ensuring consistency: if a user cannot see content in a GET response, they also will not receive a real-time event about it.


5. Request Lifecycle


6. Data Model Overview


7. Feature Interaction Matrix

Feature PairInteraction
Auth <-> ProjectsProjects reference ownerUserId. Invitation acceptance requires authenticated identity matching.
Auth <-> PermissionsIPermissionService depends on ICurrentUserService for the caller's user ID.
Auth <-> Real-timeSignalR connections authenticate via the same JWT. Hub validates token on connection.
Auth <-> All ContentAll content creation records CreatedBy from ICurrentUserService. Version history tracks CreatedBy per change.
Projects <-> TimelinesTimelines belong to a project. Timeline creation consumes a sequence number from the project's counter. Project deletion cascades to all timelines.
Projects <-> CharactersCharacters belong to a project. Character creation consumes a sequence number. Project membership and roles drive character permission checks.
Projects <-> FactionsFactions belong to a project. Faction creation consumes a sequence number.
Projects <-> Real-timeSignalR groups are organized by project. Member changes affect real-time connection authorization.
Projects <-> SearchSearch is scoped by project. Cross-project search requires membership lookup.
Characters <-> FactionsCharacters join factions via FactionMembership. Deleting a character cascades to remove faction memberships. The faction graph includes character member data.
Characters <-> RelationshipsCharacter relationships link two characters. Deleting a character cascades to remove all relationships where it is source or target.
Permissions <-> TimelinesCanEditTimeline and CanPublishTimeline govern write access. Timeline queries filter by DevelopmentStatus for Players/Viewers.
Permissions <-> CharactersCanCreateCharacter and CanEditCharacter govern mutations. Character queries filter by Visibility. Relationship queries cascade character filtering.
Permissions <-> FactionsCanCreateFaction governs mutations. Faction relationship queries filter by IsSecret. Membership lists filter out private characters.
Permissions <-> Real-timeSignalR broadcasts are permission-filtered. Private content events are not sent to unauthorized connections.
Permissions <-> SearchSearch results pass through the same visibility pipeline. Permission filtering is applied as part of the database query.
Timelines <-> VersionsTimeline updates are a primary target for version history. Status changes trigger snapshots. Changelog entries are scoped to individual timeline updates.
Characters <-> VersionsCharacter edits create version snapshots. Characters can be browsed, diffed, and restored.
Factions <-> VersionsFaction edits and membership changes trigger version snapshots on the faction aggregate.
Timelines <-> Real-timeStatus changes (especially publish) trigger SignalR notifications. Reorder operations broadcast UI updates.
Characters <-> Real-timeCharacter creation and relationship changes generate activity feed events and graph update events.
Factions <-> Real-timeFaction changes and membership changes generate activity feed events.
Timelines <-> SearchTimeline updates, sections, and items are indexed for full-text search.
Characters <-> SearchCharacters and relationship descriptions are indexed. Character templates create new characters.
Factions <-> SearchFactions and faction relationships are indexed for search. Faction templates create new factions.
Real-time <-> VersionsRestore operations trigger real-time notifications. Changelog creation may generate activity feed entries.
Comments <-> All ContentComments use a polymorphic target model (TargetType + TargetId) to attach to timelines, characters, or factions. Comment visibility follows target content visibility.

8. Development Phase Map

PhaseFeaturesDescription
Phase 1Auth, Projects, Timelines, Permissions, Basic Search, Basic Real-time, Navigation AidsFoundation: users can register, create projects, manage timeline content, search, and receive basic real-time updates.
Phase 2Characters, Factions, Relationships, Graph Endpoints, Graph EventsRelationship mapping: characters and factions with typed relationships and interactive graph visualization.
Phase 3Comments, @Mentions, Notifications, Activity Feed (full), Version History, Editing IndicatorsCollaboration: threaded comments, notifications, full audit trail with version snapshots and restore.
Phase 4Changelog, Advanced Search, Cross-Project Search, Export, Bulk Operations, TemplatesPolish: creator-facing changelog, advanced search with filters, data export, bulk status changes, content templates.

9. Pending Decisions

The following decisions are referenced across feature documents as pending team resolution:

DecisionOptions Under ConsiderationImpacts
Authentication ProviderASP.NET Identity, Auth0/Clerk, Firebase AuthFeature 1 abstracts behind ICurrentUserService; implementation details change, contract does not
Hosting PlatformAzure, AWS, self-hostedAffects deployment, scaling strategy, Redis availability, file storage options
File StorageS3/R2/Azure Blob, local filesystemCharacter images, faction symbols, export files. Affects Features 4, 5, 9
Search Engine (long-term)PostgreSQL FTS (Phase 1), Meilisearch (future)Feature 9 abstracts behind ISearchEngine; Phase 1 uses PostgreSQL, migration path designed
Email ProviderSendGrid, SES, otherTransactional emails for verification, password reset, invitations
Faction Deletion CascadeOrphan children to top-level vs. recursive deleteFeature 5 currently orphans children (sets parentFactionId to null)
Comment Deletion on Content DeleteCascade delete vs. soft-delete/preserveFeature 7 notes this as a design decision