Appearance
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
| # | Feature | Description | Doc |
|---|---|---|---|
| 1 | Authentication & User Management | Registration, login, JWT sessions, password reset, invitation bridge | 01 |
| 2 | Project Management | Project CRUD, keys, visibility, member invitations, role hierarchy, sequence counter | 02 |
| 3 | Content Timeline System | Hierarchical content releases (updates > sections > items) with development status lifecycle | 03 |
| 4 | Character System | PC/NPC characters, JSONB metadata, typed relationships, character graph endpoint | 04 |
| 5 | Faction System | Organizations with nested hierarchies, memberships, inter-faction relationships, faction graph | 05 |
| 6 | Permission & Visibility | IPermissionService interface, role-based access matrix, cascading visibility filtering | 06 |
| 7 | Real-time & Collaboration | SignalR hub, activity feed, comments with @mentions, notifications, editing indicators | 07 |
| 8 | Version History & Changelog | Automatic version snapshots, diff/compare, restore; curated creator-authored changelog | 08 |
| 9 | Search & Navigation | PostgreSQL full-text search, bookmarks, recently viewed, export, bulk ops, templates | 09 |
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:
- Call an action check before executing the command.
- If reading data, apply visibility filters before assembling the response.
- 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 insertCreatedBy(Guid) -- set fromICurrentUserService.UserIdon insertUpdatedAt(DateTime) -- set on every updateUpdatedBy(Guid) -- set fromICurrentUserService.UserIdon 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:
| Strategy | Used For | Parameters | Response Fields |
|---|---|---|---|
| Cursor-based | Activity feed, search results, notifications, recently viewed | cursor (opaque), limit (max 100) | nextCursor, hasMore |
| Offset-based | Timeline updates, characters, factions, bookmarks | page, 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:
- Identifies the project group from the event's project ID.
- Determines whether the event involves restricted content (private characters, secret faction relationships, unpublished timelines).
- For public content events, broadcasts to the entire project group.
- 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 Pair | Interaction |
|---|---|
| Auth <-> Projects | Projects reference ownerUserId. Invitation acceptance requires authenticated identity matching. |
| Auth <-> Permissions | IPermissionService depends on ICurrentUserService for the caller's user ID. |
| Auth <-> Real-time | SignalR connections authenticate via the same JWT. Hub validates token on connection. |
| Auth <-> All Content | All content creation records CreatedBy from ICurrentUserService. Version history tracks CreatedBy per change. |
| Projects <-> Timelines | Timelines belong to a project. Timeline creation consumes a sequence number from the project's counter. Project deletion cascades to all timelines. |
| Projects <-> Characters | Characters belong to a project. Character creation consumes a sequence number. Project membership and roles drive character permission checks. |
| Projects <-> Factions | Factions belong to a project. Faction creation consumes a sequence number. |
| Projects <-> Real-time | SignalR groups are organized by project. Member changes affect real-time connection authorization. |
| Projects <-> Search | Search is scoped by project. Cross-project search requires membership lookup. |
| Characters <-> Factions | Characters join factions via FactionMembership. Deleting a character cascades to remove faction memberships. The faction graph includes character member data. |
| Characters <-> Relationships | Character relationships link two characters. Deleting a character cascades to remove all relationships where it is source or target. |
| Permissions <-> Timelines | CanEditTimeline and CanPublishTimeline govern write access. Timeline queries filter by DevelopmentStatus for Players/Viewers. |
| Permissions <-> Characters | CanCreateCharacter and CanEditCharacter govern mutations. Character queries filter by Visibility. Relationship queries cascade character filtering. |
| Permissions <-> Factions | CanCreateFaction governs mutations. Faction relationship queries filter by IsSecret. Membership lists filter out private characters. |
| Permissions <-> Real-time | SignalR broadcasts are permission-filtered. Private content events are not sent to unauthorized connections. |
| Permissions <-> Search | Search results pass through the same visibility pipeline. Permission filtering is applied as part of the database query. |
| Timelines <-> Versions | Timeline updates are a primary target for version history. Status changes trigger snapshots. Changelog entries are scoped to individual timeline updates. |
| Characters <-> Versions | Character edits create version snapshots. Characters can be browsed, diffed, and restored. |
| Factions <-> Versions | Faction edits and membership changes trigger version snapshots on the faction aggregate. |
| Timelines <-> Real-time | Status changes (especially publish) trigger SignalR notifications. Reorder operations broadcast UI updates. |
| Characters <-> Real-time | Character creation and relationship changes generate activity feed events and graph update events. |
| Factions <-> Real-time | Faction changes and membership changes generate activity feed events. |
| Timelines <-> Search | Timeline updates, sections, and items are indexed for full-text search. |
| Characters <-> Search | Characters and relationship descriptions are indexed. Character templates create new characters. |
| Factions <-> Search | Factions and faction relationships are indexed for search. Faction templates create new factions. |
| Real-time <-> Versions | Restore operations trigger real-time notifications. Changelog creation may generate activity feed entries. |
| Comments <-> All Content | Comments use a polymorphic target model (TargetType + TargetId) to attach to timelines, characters, or factions. Comment visibility follows target content visibility. |
8. Development Phase Map
| Phase | Features | Description |
|---|---|---|
| Phase 1 | Auth, Projects, Timelines, Permissions, Basic Search, Basic Real-time, Navigation Aids | Foundation: users can register, create projects, manage timeline content, search, and receive basic real-time updates. |
| Phase 2 | Characters, Factions, Relationships, Graph Endpoints, Graph Events | Relationship mapping: characters and factions with typed relationships and interactive graph visualization. |
| Phase 3 | Comments, @Mentions, Notifications, Activity Feed (full), Version History, Editing Indicators | Collaboration: threaded comments, notifications, full audit trail with version snapshots and restore. |
| Phase 4 | Changelog, Advanced Search, Cross-Project Search, Export, Bulk Operations, Templates | Polish: 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:
| Decision | Options Under Consideration | Impacts |
|---|---|---|
| Authentication Provider | ASP.NET Identity, Auth0/Clerk, Firebase Auth | Feature 1 abstracts behind ICurrentUserService; implementation details change, contract does not |
| Hosting Platform | Azure, AWS, self-hosted | Affects deployment, scaling strategy, Redis availability, file storage options |
| File Storage | S3/R2/Azure Blob, local filesystem | Character 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 Provider | SendGrid, SES, other | Transactional emails for verification, password reset, invitations |
| Faction Deletion Cascade | Orphan children to top-level vs. recursive delete | Feature 5 currently orphans children (sets parentFactionId to null) |
| Comment Deletion on Content Delete | Cascade delete vs. soft-delete/preserve | Feature 7 notes this as a design decision |