13 KiB
Architecture
Analysis Date: 2026-04-27
Pattern Overview
Overall: NestJS modular backend + React feature-based frontend with real-time WebSocket synchronization
Key Characteristics:
- Modular NestJS architecture with feature-based modules (Auth, Characters, Campaigns, Equipment, Battle)
- Feature-first React organization with shared components and hooks
- WebSocket Gateway real-time sync for character and battle state
- Role-based access control (ADMIN, GM, PLAYER) enforced globally
- Prisma ORM for PostgreSQL data persistence
- JWT-based stateless authentication
Layers
Backend: Controller Layer
- Purpose: HTTP request handling and routing
- Location:
server/src/modules/*/[feature].controller.ts - Contains: REST endpoints with decorators (@Post, @Get, @Put, @Patch, @Delete)
- Depends on: Services, DTOs, Guards, Decorators
- Used by: HTTP clients (React frontend)
- Examples:
server/src/modules/characters/characters.controller.ts- Character CRUDserver/src/modules/campaigns/campaigns.controller.ts- Campaign managementserver/src/modules/equipment/equipment.controller.ts- Equipment search/browseserver/src/modules/auth/auth.controller.ts- Login/Register
Backend: Service Layer
- Purpose: Business logic, data processing, validation
- Location:
server/src/modules/*/[feature].service.ts - Contains: Methods for creating, updating, deleting entities; complex calculations
- Depends on: PrismaService, other Services, external APIs (Claude)
- Used by: Controllers, Gateways, other Services
- Examples:
server/src/modules/characters/characters.service.ts- Character operations, access checksserver/src/modules/characters/alchemy.service.ts- Alchemy system logic (formulas, prepared items, vials)server/src/modules/characters/pathbuilder-import.service.ts- Pathbuilder JSON parsingserver/src/modules/equipment/equipment.service.ts- Equipment search with filtersserver/src/modules/battle/battle.service.ts- Battle session and token management
Backend: Gateway Layer (WebSocket)
- Purpose: Real-time bidirectional communication for live updates
- Location:
server/src/modules/*/[feature].gateway.ts - Contains: Socket.io event handlers, authentication, room management
- Depends on: JwtService, PrismaService, Services
- Used by: React WebSocket hooks
- Examples:
server/src/modules/characters/characters.gateway.ts- Character HP, conditions, items, alchemy real-time syncserver/src/modules/battle/battle.gateway.ts- Battle token movement, HP, initiative real-time sync
Backend: Persistence Layer
- Purpose: Database abstraction and ORM
- Location:
server/src/prisma/prisma.service.ts - Contains: Prisma client wrapper, query interface
- Depends on: PostgreSQL database
- Used by: All Services
- Data Models defined in:
server/prisma/schema.prisma
Frontend: Feature Modules
- Purpose: Isolated feature domains with components, hooks, types
- Location:
client/src/features/[feature]/ - Contains: Components, hooks, index.ts barrel exports
- Examples:
client/src/features/auth/- Login/Register pages, useAuthStore hookclient/src/features/characters/- Character sheet page, modals, utilitiesclient/src/features/campaigns/- Campaign list/detail pagesclient/src/features/battle/- Battle canvas, tokensclient/src/features/library/- Battle maps, NPC templates (combatants)
Frontend: Shared Layer
- Purpose: Reusable components, hooks, utilities, types across features
- Location:
client/src/shared/ - Contains:
- Components:
ui/(shadcn/ui),layout.tsx,protected-route.tsx - Hooks:
use-character-socket.ts,use-battle-socket.ts - Lib:
api.ts(API client),utils.ts(helpers) - Types:
index.ts(all TypeScript interfaces)
- Components:
- Used by: All features
Frontend: Router
- Purpose: Navigation and route protection
- Location:
client/src/App.tsx - Contains: React Router v6 routes, protected route wrapper
- Depends on: React Router, Feature components
- Entry point:
client/src/main.tsx
Frontend: State Management
- Purpose: Client-side state persistence
- Location:
client/src/features/auth/hooks/use-auth-store.ts - Contains: Zustand store for authentication state
- Used by: Auth-related components, ProtectedRoute
Data Flow
Character Update (Real-Time WebSocket Sync)
-
User Action (React Component)
- User damages character in
client/src/features/characters/components/hp-control.tsx - Calls
api.updateCharacterHp()→ PATCH/campaigns/:id/characters/:id/hp
- User damages character in
-
Controller (
server/src/modules/characters/characters.controller.ts)- Receives HTTP request
- Validates JWT token via
JwtAuthGuard - Calls
CharactersService.updateHp()
-
Service (
server/src/modules/characters/characters.service.ts)- Checks campaign access (GM or campaign member)
- Checks character ownership (owner or GM can edit)
- Updates character HP in Prisma
- Emits WebSocket event via
CharactersGateway.broadcast()
-
Gateway Broadcast (
server/src/modules/characters/characters.gateway.ts)- Broadcasts
character_updateevent to all clients in character room - Update type:
'hp' - Payload:
{ hpCurrent, hpTemp, hpMax }
- Broadcasts
-
Client Socket Hook (
client/src/shared/hooks/use-character-socket.ts)- Listens for
character_updateevent - Routes to appropriate callback:
onHpUpdate?.(data) - Component state updates via React state
- Listens for
-
Component Re-render
- Character sheet displays new HP values
- All other connected clients see update in real-time
Equipment Database Search
-
User Search (React Component)
- User searches equipment in
client/src/features/characters/components/add-item-modal.tsx - Calls
api.searchEquipment({ query, category, filters, page, limit })
- User searches equipment in
-
Controller (
server/src/modules/equipment/equipment.controller.ts)- Receives GET
/equipmentwith query params - Calls
EquipmentService.search()
- Receives GET
-
Service (
server/src/modules/equipment/equipment.service.ts)- Builds Prisma where clause from filters
- Queries Equipment table (5,482 items)
- Returns paginated results with pagination metadata
- Includes category list for UI dropdown
-
Component Display
- Results displayed with pagination controls
- User can view equipment details, add to inventory
- New item created via
api.addCharacterItem()
Character Creation (Pathbuilder Import)
-
User Upload (React Component)
- User uploads Pathbuilder JSON in
client/src/features/characters/components/import-character-modal.tsx - Calls
api.importCharacterFromPathbuilder(pathbuilderJson)
- User uploads Pathbuilder JSON in
-
Controller (
server/src/modules/characters/characters.controller.ts)- POST
/campaigns/:id/characters/import - Calls
PathbuilderImportService.importCharacter()
- POST
-
Pathbuilder Import Service (
server/src/modules/characters/pathbuilder-import.service.ts)- Parses Pathbuilder JSON structure
- Extracts abilities, skills, feats, spells
- Creates Character with all related entities (CharacterAbility, CharacterSkill, etc.)
- Stores raw pathbuilderData for future reference
-
Database Persistence
- Character created with all nested relations
- Each skill, feat, spell stored as separate records
- Character ready for real-time sync
Battle Session Synchronization
-
Token Movement (React Component)
- User drags token on battle canvas
- Calls
api.moveBattleToken(positionX, positionY)
-
Battle Gateway (
server/src/modules/battle/battle.gateway.ts)- Receives movement update
- Broadcasts
battle_updateevent with type'token_moved' - All clients in session room receive update
-
Client Display
- All participants see token at new position
- No UI lag — updates are instant
Alchemy System State
-
Prepare Items (React Component)
- User selects formulas to prepare daily in
client/src/features/characters/components/alchemy-tab.tsx - Calls
api.dailyPreparation(items)
- User selects formulas to prepare daily in
-
Alchemy Service (
server/src/modules/characters/alchemy.service.ts)- Validates character has formulas
- Creates CharacterPreparedItem records
- Updates CharacterAlchemyState (versatileVialsCurrent, advancedAlchemyMax)
- Emits WebSocket
alchemy_preparedandalchemy_stateupdates
-
Real-Time Sync
- UI updates showing prepared items and vial tracker
- Ready for combat use
Key Abstractions
Prisma ORM Service:
- Purpose: Database abstraction, query builder
- Location:
server/src/prisma/prisma.service.ts - Pattern: Singleton service injected into all modules
- Used for: All CRUD operations, complex queries with relations
JWT Strategy & Guards:
- Purpose: Authentication and authorization
- Location:
server/src/modules/auth/strategies/jwt.strategy.ts- JWT validationserver/src/modules/auth/guards/jwt-auth.guard.ts- Global auth enforcementserver/src/modules/auth/guards/roles.guard.ts- Role-based access control
- Pattern: NestJS guards executed globally on every request
- Metadata:
@Roles()and@Public()decorators control per-endpoint behavior
API Client Service:
- Purpose: HTTP communication abstraction
- Location:
client/src/shared/lib/api.ts - Pattern: Singleton class with axios instance
- Features: Token management, auto-retry, auth interceptors, 401 handling
WebSocket Socket Manager:
- Purpose: Prevent duplicate connections, manage subscriptions
- Location:
client/src/shared/hooks/use-character-socket.ts - Pattern: Global socket singleton with ref counting
- Features: Auto-reconnect, polling fallback, room subscription
Character/Battle Update Types:
- Purpose: Type-safe event dispatch
- Location:
server/src/modules/characters/characters.gateway.ts(CharacterUpdatePayload)server/src/modules/battle/battle.gateway.ts(BattleUpdatePayload)
- Pattern: Union types for event kind discrimination
Entry Points
Server Entry:
- Location:
server/src/main.ts - Triggers:
npm run start:devor deployed container startup - Responsibilities:
- Create NestJS app from AppModule
- Enable global pipes (ValidationPipe)
- Configure CORS from env
- Setup Swagger API docs at
/api/docs - Listen on PORT (default 5000)
Client Entry:
- Location:
client/src/main.tsx - Triggers: Browser page load or
npm run dev - Responsibilities:
- Render React app into #root DOM element
- Wrap with StrictMode
Router:
- Location:
client/src/App.tsx - Pattern: React Router v6 with protected routes
- Flow:
- QueryClientProvider (React Query setup)
- BrowserRouter (React Router)
- AppContent checks auth state
- Public routes: /login, /register
- Protected routes: / (campaigns), /campaigns/:id, /campaigns/:id/characters/:characterId, /campaigns/:id/battle, /campaigns/:id/library
- ProtectedRoute wrapper ensures authentication
Error Handling
Strategy: Structured error responses with HTTP status codes
Backend Patterns:
NotFoundException- 404 when entity not foundForbiddenException- 403 when user lacks permissionBadRequestException- 400 for invalid inputUnauthorizedException- 401 for auth failures- Global error filter could be added for consistent formatting
- Service methods validate access before querying:
checkCampaignAccess(),checkCharacterAccess()
Frontend Patterns:
- API client
response.interceptorscatches 401 → redirects to /login - Components wrapped in error boundaries (future enhancement)
- Failed requests return rejected promises to component
WebSocket Patterns:
- Token verification on connection → disconnect if invalid
- No structured error responses; silent failures with console logging
- Clients auto-reconnect via socket.io configuration
Cross-Cutting Concerns
Logging:
- Backend: NestJS Logger class used in services/gateways
- Frontend: Console.log for development (socket.io events log connection state)
Validation:
- Backend: Global ValidationPipe with DTOs (class-validator)
- Frontend: Form validation in components (manual checks in modals)
- Prisma schema enforces constraints (NOT NULL, unique, enums)
Authentication:
- Backend: JwtAuthGuard applied globally in app.module.ts
- Endpoints opt-out via @Public() decorator
- WebSocket: Token verified in gateway.handleConnection()
- Frontend: Token stored in localStorage (persistent) or sessionStorage (session-only)
Authorization:
- Backend: RolesGuard checks @Roles() metadata
- Service methods verify campaign/character ownership before allowing operations
- Pattern: Check campaign membership → check character ownership → allow operation
Real-Time Sync:
- WebSocket Gateway manages rooms (one room per character/session)
- Clients join room on component mount, leave on unmount
- Broadcasts to all clients in room except sender (optional)
Architecture analysis: 2026-04-27