12 KiB
12 KiB
Backend Architecture
Architectural patterns and design decisions for the Flashpoint Web backend service.
Architecture Overview
Service layer pattern with clear separation of concerns:
┌─────────────────────────────────────────────────────────────┐
│ HTTP Layer (Routes) │
│ - Handle HTTP requests/responses │
│ - Input validation, Response formatting │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Middleware Layer │
│ - Authentication, RBAC, Activity logging, Error handling │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ - Business Logic, Database operations, Transactions │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Data Access Layer │
│ - DatabaseService (flashpoint.sqlite, read-only) │
│ - UserDatabaseService (user.db, read/write) │
└─────────────────────────────────────────────────────────────┘
Design Patterns
1. Service Layer Pattern
Routes handle HTTP concerns; Services contain business logic.
// Route (routes/users.ts)
router.get('/', authenticate, async (req, res) => {
const users = await userService.getUsers(1, 50);
res.json(users);
});
// Service (services/UserService.ts)
async getUsers(page: number, limit: number) {
const offset = (page - 1) * limit;
return UserDatabaseService.all(
'SELECT * FROM users LIMIT ? OFFSET ?',
[limit, offset]
);
}
2. Singleton Pattern
Single database connection per process, shared across all requests.
export class DatabaseService {
private static db: BetterSqlite3.Database | null = null;
static async initialize(): Promise<void> {
this.db = new BetterSqlite3(config.flashpointDbPath);
}
static getDatabase(): BetterSqlite3.Database {
if (!this.db) throw new Error('Database not initialized');
return this.db;
}
}
3. Middleware Chain Pattern
Composable request processing; each middleware handles one concern.
router.post(
'/users',
authenticate, // Verify JWT
requirePermission('users.create'), // Check permission
logActivity('create', 'users'), // Log activity
async (req, res) => {
/* handle */
}
);
4. Repository Pattern (via Database Services)
Abstract data access from business logic; centralized query logic.
Architectural Decisions
1. Two-Database Architecture
Rationale:
- Flashpoint database managed by external Launcher
- Prevents conflicts from simultaneous writes
- User data needs different schema and write access
- Clear separation of concerns
2. Synchronous Database Access (better-sqlite3)
Rationale:
- SQLite is single-threaded; async provides no benefit
- Simpler, more readable code
- Better performance for SQLite workloads
- No callback/promise overhead
3. JWT-Based Authentication
Rationale:
- Stateless authentication (no session storage)
- Works across multiple backend instances
- Mobile-friendly
Implementation:
- Access token: 1 hour expiry
- Refresh token: 30 days expiry, stored in DB
4. RBAC over ACL
Rationale:
- Easier to manage permissions for groups of users
- Standard pattern for most applications
- Simpler permission checks
- Sufficient granularity
5. Activity Logging
Rationale:
- Audit trail for security
- User behavior analytics
- Debugging user issues
- Compliance requirements
Implementation:
- Non-blocking (setImmediate)
- Automatic cleanup (90-day retention)
6. Integrated Game Content Serving
Rationale:
- Simplifies deployment (single service instead of two)
- Removes inter-service HTTP calls (direct function calls instead)
- Unified error handling and logging
- Shared resource pools (database, file watchers)
Implementation:
- Game content routes (
/game-proxy/*,/game-zip/*) registered before auth middleware - Direct function calls to game server internals (gamezipserver.mountZip(), legacy proxy handlers)
- Serves legacy htdocs content, game ZIPs, and HTML5 games
- CGI/PHP execution support for interactive content
7. Play Session Tracking
Rationale:
- Detailed play history per user
- Calculate accurate statistics
- Support for session duration
- Abandoned session detection
Error Handling Strategy
AppError Class
export class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message);
}
}
Error Categories
400: Bad Request (invalid input)401: Unauthorized (auth failed)403: Forbidden (permission denied)404: Not Found (resource missing)409: Conflict (duplicate resource)429: Too Many Requests (rate limit)500: Internal Server Error (unexpected)
Database Architecture
Connection Management
Flashpoint Database:
- Single read-only connection per process
- File watcher for hot-reload
- Automatic reconnection on file change
- Debounced reload (500ms)
User Database:
- Single read/write connection per process
- Foreign key enforcement enabled
- Schema migrations on startup
Query Optimization
- Indexes on foreign keys and frequently queried columns
- Pagination to prevent memory issues
- Prepared statements for reusable queries
- Selective columns (avoid SELECT *)
- Flash SWF filtering: Flash games without
.swflaunchCommands excluded from browse/search - Reduced column selection for list views (7 essential fields vs. full detail)
Optimization Patterns:
- Scalar subqueries in SELECT: Merge multiple COUNT queries into one SELECT with scalar subqueries. Example:
SELECT COUNT(*) as total, (SELECT COUNT(DISTINCT x) FROM ...) as count FROM ...reduces roundtrips - Window functions for pagination: Use
COUNT(*) OVER()window function to get total count in the same query as paginated results, eliminating separate COUNT query - Map-based data merging: When joining application data with external lookups, build a
Mapfor O(1) lookups instead of O(N²).find()loops through arrays - Direct file access first: For file-based data (playlists), try direct path access (e.g.,
{id}.json) before falling back to directory scans. O(1) fast path with O(N) fallback - Parallel file I/O: Use
Promise.allSettled()to read multiple files concurrently instead of sequential loops
Transaction Strategy
const transaction = db.transaction(() => {
UserDatabaseService.run('INSERT INTO users ...');
UserDatabaseService.run('INSERT INTO user_stats ...');
});
transaction(); // Execute atomically
Security Architecture
Authentication Flow
- User sends credentials to
/api/auth/login - AuthService verifies password (bcrypt)
- Generate access token (JWT, 1h expiry)
- Generate refresh token (random, 30d expiry)
- Store refresh token in database
- Return both tokens to client
Authorization Flow
- Client sends Authorization header
- Auth middleware verifies JWT token
- Extract user info from token payload
- Load user permissions from database
- RBAC middleware checks required permission
- Allow or deny request
Password Security
- Hashing: bcrypt with 10 rounds
- Salt: Automatic per-password salt
- Strength: Not enforced (can be added)
- Reset: Not implemented (can be added)
Login Protection
- Attempt tracking (log all attempts)
- Rate limiting (5 failed attempts per 15 minutes)
- IP-based tracking
- Auto-cleanup (remove attempts >24 hours old)
Performance Considerations
Database Performance
- Better-sqlite3 faster than async alternatives
- Connection reuse (single per process)
- Prepared statements cached and reused
- Comprehensive indexing strategy
Response Time Targets
- Game search: < 100ms (p95)
- Game details: < 50ms (p95)
- User operations: < 200ms (p95, includes bcrypt)
- Play tracking: < 50ms (p95)
Caching Strategy
Currently Implemented:
- Permission caching in-memory (5-minute TTL)
- 90%+ reduction in permission queries
Future Recommendations:
- Redis for distributed session storage
- Cache game metadata queries
- Cache filter options (platforms, tags)
Monitoring and Observability
Logging
Winston logger with structured logging:
logger.info('User logged in', { userId, username, ipAddress });
logger.error('Database error', { error: err.message });
Activity Logs
Database-backed activity logging:
- All user actions logged
- IP address and user agent captured
- Queryable for analytics
- Automatic retention policy (90 days)
Health Checks
/health endpoint returns:
- Server status
- Database connection status
- Flashpoint path
- Game service URL
- Timestamp
Extension Points
Adding New Permissions
- Insert into
permissionstable - Assign to roles via
role_permissions - Use
requirePermission()in routes
Adding New Services
- Create service class in
src/services/ - Implement business logic methods
- Use database services for data access
- Export and use in routes
Adding Middleware
- Create middleware function in
src/middleware/ - Export function
- Apply to routes or globally
Database Migrations
- Create SQL file in
src/migrations/with sequential number - Add migration logic to
UserDatabaseService.runMigrations() - Test on clean database
Best Practices
Service Methods
- Return typed data (use TypeScript interfaces)
- Throw
AppErrorfor business logic errors - Keep methods focused and single-purpose
- Use transactions for multi-step operations
Route Handlers
- Validate input early
- Use middleware for auth/permissions
- Handle errors with try/catch
- Return consistent JSON structure
Database Queries
- Use parameterized queries (prevent SQL injection)
- Add indexes for frequently queried columns
- Use LIMIT for potentially large results
- Prefer prepared statements for reusable queries
Error Handling
- Use
AppErrorfor expected errors - Log unexpected errors with full stack trace
- Never expose internal errors to clients
- Provide helpful error messages
Testing Strategy
Unit Tests
- Test individual service methods
- Mock database services
- Focus on business logic
Integration Tests
- Test route handlers end-to-end
- Use test database
- Verify HTTP responses
Related Documentation
- README.md - Backend overview
- Configuration - Environment setup
- Database Schema - Schema reference