16 KiB
System Architecture
Overview
Flashpoint Web is built as a monorepo containing three independent microservices that work together to provide a complete web-based game browsing and playing experience. The architecture follows a clean separation of concerns pattern with distinct services for metadata management, game file serving, and user interface.
Architecture Diagram
graph TB
subgraph "Client Layer"
Browser[Web Browser]
Ruffle[Ruffle Emulator]
end
subgraph "Frontend Service :5173"
React[React SPA]
Router[React Router]
Query[TanStack Query]
Zustand[Zustand Stores]
Vite[Vite Dev Server]
end
subgraph "Backend Service :3100"
Express[Express Server]
GameSvc[GameService]
UserSvc[UserService]
AuthSvc[AuthService]
PlaySvc[PlayTrackingService]
Middleware[Auth/RBAC/Logging]
end
subgraph "Game Service :22500/:22501"
ProxyServer[HTTP Proxy :22500]
ZipServer[GameZip Server :22501]
ZipManager[ZIP Manager]
MimeHandler[MIME Handler]
end
subgraph "Data Layer"
FlashpointDB[(flashpoint.sqlite<br/>READ-ONLY)]
UserDB[(user.db<br/>READ-WRITE)]
FileSystem[File System<br/>Games, Images, ZIPs]
end
subgraph "External Services"
CDN[Flashpoint CDN<br/>infinity.flashpointarchive.org]
end
Browser --> React
React --> Router
React --> Query
React --> Zustand
Query --> Express
Express --> Middleware
Middleware --> GameSvc
Middleware --> UserSvc
Middleware --> AuthSvc
Middleware --> PlaySvc
GameSvc --> FlashpointDB
UserSvc --> UserDB
AuthSvc --> UserDB
PlaySvc --> UserDB
PlaySvc --> FlashpointDB
Express -.proxy.-> ProxyServer
Express -.proxy.-> ZipServer
ProxyServer --> FileSystem
ProxyServer -.fallback.-> CDN
ZipServer --> ZipManager
ZipManager --> FileSystem
React --> Ruffle
Ruffle -.loads.-> ProxyServer
Vite -.dev proxy.-> Express
style Browser fill:#e1f5ff
style React fill:#61dafb
style Express fill:#90ee90
style ProxyServer fill:#ffd700
style FlashpointDB fill:#ff9999
style UserDB fill:#ff9999
Service Architecture
1. Frontend Service (Port 5173)
Purpose: Single-Page Application providing the user interface
Technology Stack:
- React 18 with TypeScript
- Vite for build tooling and development server
- React Router for client-side routing
- TanStack Query for server state management
- Zustand for client-side UI state
- Tailwind CSS for styling
- Ruffle WebAssembly for Flash emulation
Key Responsibilities:
- User interface rendering and interaction
- Client-side routing and navigation
- Server state caching and synchronization
- Flash game embedding via Ruffle
- Authentication state management
- Theme and UI preferences
State Management:
- Server State (TanStack Query): Games, playlists, user data, play stats
- UI State (Zustand): Sidebar visibility, view modes, auth state
- URL State (React Router): Search params, filters, pagination
- Local Storage: Auth tokens, theme preferences
2. Backend Service (Port 3100)
Purpose: REST API server for metadata, authentication, and business logic
Technology Stack:
- Express.js with TypeScript
- BetterSqlite3 for database access
- JWT for authentication
- Bcrypt for password hashing
- Zod for request validation
Key Responsibilities:
- Game metadata queries from flashpoint.sqlite
- User management and authentication
- Role-based access control (RBAC)
- Play session tracking
- Playlist management
- Activity logging
- Database hot-reloading (watches for Flashpoint Launcher updates)
Database Access:
- flashpoint.sqlite: Read-only access with file watcher for external changes
- user.db: Read-write access with schema migrations
Service Layer Pattern:
routes/ → middleware/ → services/ → databases/
3. Game Service (Ports 22500, 22501)
Purpose: Game file serving, proxying, and ZIP mounting
Technology Stack:
- Express.js with TypeScript
- node-stream-zip for ZIP file handling
- Custom MIME type detection (199+ types)
Key Responsibilities:
HTTP Proxy Server (22500):
- Serves legacy web content with fallback chain:
- Local htdocs directory
- Game data directory
- ZIP archives (via zip-manager)
- External CDN fallback
- Local cache for downloaded content
- CORS headers for cross-domain content
- URL decoding and path normalization
GameZip Server (22501):
- Mounts and serves files from ZIP archives
- Streaming support for large files
- Zero-extraction design (files served directly from ZIP)
- LRU cache for ZIP mounts (max 100, 30-min TTL, auto-cleanup on eviction)
- Request body size limit (1MB maximum) for DoS protection
- Automatic cleanup of unused mounts
Data Flow Architecture
Component Communication
graph LR
subgraph "Frontend"
Component[React Component]
Hook[Custom Hook]
API[API Client]
Store[Zustand Store]
end
subgraph "Backend"
Route[Express Route]
MW[Middleware]
Service[Service Layer]
end
subgraph "Database"
DB[(SQLite)]
end
Component --> Hook
Hook --> API
Hook --> Store
API --> Route
Route --> MW
MW --> Service
Service --> DB
DB -.data.-> Service
Service -.response.-> Route
Route -.response.-> API
API -.update.-> Hook
Hook -.render.-> Component
Request Processing Pipeline
Frontend → Backend:
- User interaction triggers component action
- Custom hook calls API client function
- API client adds JWT token via interceptor
- Axios sends HTTP request to backend
- Vite dev proxy forwards to Express (in development)
Backend Processing:
- Express route receives request
- Authentication middleware verifies JWT
- RBAC middleware checks permissions
- Activity logger records action
- Route handler validates request (Zod)
- Service layer executes business logic
- Database query returns data
- Response sent back to client
Frontend State Update:
- TanStack Query receives response
- Cache updated with new data
- Dependent components re-render
- Optimistic updates for better UX
Database Architecture
Flashpoint Database (flashpoint.sqlite)
Ownership: Flashpoint Launcher (external)
Access Pattern: Read-only with hot-reload
Key Tables:
game: Game metadata (100,000+ entries)game_data: Game file paths and launch commandsplatform: Gaming platforms (Flash, HTML5, etc.)tag: Tags and categoriesgame_tags_tag: Many-to-many game-tag relationshipsplaylist: Community playlistsplaylist_game: Playlist entries
Hot-Reload Mechanism:
fs.watch(dbPath, (eventType) => {
if (eventType === 'change') {
closeConnection();
openConnection();
logger.info('Database reloaded');
}
});
User Database (user.db)
Ownership: Flashpoint Web (application-managed)
Access Pattern: Read-write with migrations
Schema Versioning: Managed via migration files in backend/src/migrations/
Key Tables:
Authentication & Authorization:
users: User accounts with hashed passwords (bcrypt)roles: RBAC roles (admin, moderator, user, guest)role_permissions: Permissions assigned to rolesuser_roles: User role assignmentssystem_settings: Global system-wide configuration (auth, app, metadata, features, game, storage, rate limiting)
User Features:
user_playlists: User-created playlistsuser_favorites: Favorited games per useruser_settings: Key-value settings storage (theme, preferences)
Play Tracking:
user_game_plays: Individual play sessions with durationuser_game_stats: Aggregated per-game statisticsuser_stats: Overall user statistics
Activity Logging:
activity_logs: Audit trail of user actions
Security Architecture
Authentication Flow
sequenceDiagram
participant Client
participant Backend
participant UserDB
Client->>Backend: POST /api/auth/login {username, password}
Backend->>UserDB: Query user by username
UserDB-->>Backend: User record
Backend->>Backend: Bcrypt compare password
Backend->>Backend: Generate JWT tokens
Backend->>UserDB: Update last_login_at
Backend-->>Client: {accessToken, refreshToken, user}
Client->>Client: Store in localStorage
Client->>Client: Store in Zustand
Authorization Model
Role-Based Access Control (RBAC):
User → assigned to → Role → has → Permissions
Default Roles:
admin: Full system accessmoderator: Content management, user supportuser: Standard user featuresguest: Read-only access (if enabled)
Permission Format: resource.action
Examples:
games.read: View gamesgames.play: Play gamesplaylists.create: Create playlistsusers.manage: Manage users
Middleware Stack:
router.get('/games',
authenticate, // Verify JWT
requirePermission('games.read'), // Check permission
logActivity('games.list'), // Log action
async (req, res) => { ... }
);
Token Management
Access Token:
- Short-lived (15 minutes default)
- Contains user ID, role, permissions
- Signed with JWT_SECRET
- Sent in Authorization header
Refresh Token:
- Long-lived (7 days default)
- Stored securely in httpOnly cookie (production)
- Used to obtain new access token
- Invalidated on logout
Token Refresh Flow:
// Axios interceptor handles 401 automatically
api.interceptors.response.use(null, async (error) => {
if (error.response?.status === 401 && !originalRequest._retry) {
const tokens = await authApi.refreshToken(refreshToken);
updateAccessToken(tokens.accessToken);
return api(originalRequest); // Retry with new token
}
});
File System Architecture
Directory Structure
D:/Flashpoint/
├── Data/
│ ├── flashpoint.sqlite # Game metadata database
│ ├── Images/ # Game screenshots
│ │ └── {gameId}.png
│ ├── Logos/ # Game logos
│ │ └── {gameId}.png
│ ├── Games/ # ZIP archives
│ │ └── {first-letter}/
│ │ └── {gameId}.zip
│ └── Playlists/ # Playlist JSON files
│ └── {playlistId}.json
├── Legacy/
│ └── htdocs/ # Legacy web content
│ └── {domain}/
│ └── {path}
└── Flashpoint.exe # Flashpoint Launcher
File Access Patterns
Images & Logos:
- Served directly by backend from file system
- Endpoint:
/api/images/{type}/{gameId} - Cached by browser with appropriate headers
Game Content:
- Never served by backend
- Delegated to game-service proxy
- Backend returns launch URLs pointing to game-service
ZIP Archives:
- Mounted by GameZip server on-demand
- Files served without extraction
- Automatic unmounting after inactivity
Scalability Considerations
Current Architecture (Single-Server)
All services run on localhost:
- Frontend: :5173
- Backend: :3100
- Game Service: :22500/:22501
Potential Scaling Strategies
Horizontal Scaling:
- Frontend: Static hosting on CDN (Vercel, Netlify)
- Backend: Multiple instances behind load balancer
- Game Service: Region-specific replicas
Database Scaling:
- flashpoint.sqlite: Read replicas for queries
- user.db: PostgreSQL/MySQL for better concurrency
Caching Layer:
- Redis for session storage
- CDN for static game assets
- HTTP cache for metadata responses
Performance Optimizations:
- Frontend: Route-based lazy loading (38% bundle size reduction)
- Frontend: React.memo with custom comparison (98% fewer re-renders)
- Game Service: LRU cache for ZIP mounts (prevents memory leaks)
- Game Service: Request body size limits (1MB max, DoS protection)
- Backend: Query result caching (statistics, permissions)
- Database connection pooling (future)
- Virtual scrolling for large datasets (future)
Development vs Production
Development Mode
Frontend:
- Vite dev server with HMR
- Proxy configuration for API and game files
- Source maps enabled
- React Developer Tools
Backend:
- tsx with watch mode
- Detailed logging (DEBUG level)
- CORS enabled for localhost:5173
- SQLite in WAL mode
Game Service:
- Node with tsx watch
- Verbose logging
- Local file system paths
Production Mode
Frontend:
- Static build in
dist/ - Minified and tree-shaken
- Served by backend or CDN
- Source maps optional
Backend:
- Compiled to
dist/ - Production logging (INFO level)
- Environment-specific JWT secret
- Rate limiting enabled
Game Service:
- Compiled TypeScript
- Structured logging
- Optimized for concurrent requests
Error Handling Strategy
Frontend Error Boundaries
<ErrorBoundary fallback={<ErrorPage />}>
<GameList />
</ErrorBoundary>
Backend Error Handler
class AppError extends Error {
constructor(public statusCode: number, message: string) {}
}
// Global error middleware
app.use((err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err.message });
}
// Log unexpected errors
logger.error(err);
res.status(500).json({ error: 'Internal server error' });
});
Graceful Degradation
- Guest mode when auth is disabled
- Fallback to CDN when local files missing
- Offline-friendly with cached data
- Error toasts for user feedback
Monitoring and Logging
Activity Logging
All significant user actions are logged:
{
userId: number,
action: 'games.play' | 'users.create' | ...,
targetType: 'games' | 'users' | ...,
targetId: string,
metadata: { ... },
ipAddress: string,
timestamp: ISO8601
}
Performance Metrics
- Play session durations
- API response times
- Database query performance
- Game launch success rate
Log Levels
DEBUG: Development debuggingINFO: Normal operationsWARN: Recoverable issues (DB locked, file not found)ERROR: Critical failures
Technology Choices Rationale
Why TypeScript?
- Type safety across frontend and backend
- Better IDE support and refactoring
- Shared types between services
- Reduced runtime errors
Why BetterSqlite3?
- Synchronous API (simpler code)
- Better performance than async wrappers
- Perfect for read-heavy workloads
- WAL mode for concurrent reads
Why TanStack Query?
- Automatic caching and invalidation
- Background refetching
- Optimistic updates
- Loading and error states
Why Monorepo?
- Shared TypeScript types
- Coordinated development
- Single deployment
- Consistent tooling
Future Architecture Considerations
Potential Enhancements
-
Microservices Evolution:
- Separate playlist service
- Dedicated stats/analytics service
- Image processing service
-
Real-time Features:
- WebSocket for live updates
- Real-time multiplayer coordination
- Live activity feeds
-
Advanced Caching:
- Redis for session management
- Service Worker for offline support
- GraphQL for flexible queries
-
Enhanced Security:
- OAuth2 integration
- Two-factor authentication
- API rate limiting per user
- Content Security Policy
-
Performance:
- Database sharding
- Read replicas
- CDN integration
- Edge computing for game service
Conclusion
The Flashpoint Web architecture balances simplicity with scalability, maintaining clean separation of concerns while enabling efficient communication between services. The three-service design allows independent scaling and deployment while keeping the codebase maintainable and type-safe.