25 KiB
Technology Stack
Introduction
Flashpoint Web is built with modern, production-ready technologies chosen for their performance, developer experience, and ecosystem maturity. This document provides a comprehensive overview of all technologies used across the three services, including rationale for each choice.
Stack Overview
Backend Technologies
| Technology | Version | Purpose |
|---|---|---|
| Node.js | 20+ | JavaScript runtime environment |
| Express | 4.18 | Web application framework |
| TypeScript | 5.4 | Type-safe JavaScript |
| BetterSqlite3 | 12.6 | SQLite database driver |
| JWT | 9.0 | Authentication tokens |
| bcrypt | 6.0 | Password hashing |
| Winston | 3.11 | Logging framework |
| Zod | 3.22 | Schema validation |
Frontend Technologies
| Technology | Version | Purpose |
|---|---|---|
| React | 18.3 | UI library |
| TypeScript | 5.4 | Type-safe JavaScript |
| Vite | 5.2 | Build tool and dev server |
| React Router | 6.22 | Client-side routing |
| TanStack Query | 5.28 | Server state management |
| Zustand | 4.5 | Client state management |
| Tailwind CSS | 3.4 | Utility-first CSS framework |
| Shadcn UI | Latest | Component library |
| Ruffle | 0.2.0-nightly | Flash emulator |
Game Service Technologies
| Technology | Version | Purpose |
|---|---|---|
| Node.js | 20+ | JavaScript runtime |
| Express | 4.18 | HTTP server framework |
| TypeScript | 5.3 | Type safety |
| node-stream-zip | 1.15 | ZIP archive streaming |
| Axios | 1.6 | HTTP client for CDN |
| Winston | 3.11 | Logging |
Development Tools
| Tool | Purpose |
|---|---|
| ESLint | Code linting and style enforcement |
| TypeScript Compiler | Type checking and compilation |
| Vitest | Unit testing framework |
| tsx | TypeScript execution and hot reload |
| Concurrently | Run multiple dev servers |
| Docker | Containerization |
Backend Stack Deep Dive
Node.js (v20+)
What it is: JavaScript runtime built on Chrome's V8 engine
Why we chose it:
- Ecosystem Alignment: Same language as frontend (JavaScript/TypeScript)
- Async I/O: Excellent for handling concurrent API requests
- Package Ecosystem: npm has packages for everything we need
- Performance: V8 engine provides great JavaScript execution speed
- Community: Large community with extensive resources
Use cases in our project:
- REST API server
- Database operations
- File watching (flashpoint.sqlite hot-reload)
- Background jobs (abandoned session cleanup)
Configuration:
{
"engines": {
"node": ">=20.0.0"
}
}
Express (v4.18)
What it is: Minimal and flexible Node.js web application framework
Why we chose it:
- Maturity: Battle-tested in production for 10+ years
- Middleware Pattern: Perfect for auth, logging, CORS
- Ecosystem: Huge collection of compatible middleware
- Simplicity: Easy to understand and maintain
- Performance: Fast routing with minimal overhead
Use cases in our project:
- Route handling (
/api/games,/api/auth/login) - Middleware stack (auth, RBAC, rate limiting)
- Request parsing (JSON, URL-encoded)
- Error handling
Key middleware used:
// CORS for frontend origin
app.use(cors({ origin: process.env.DOMAIN }));
// Security headers
app.use(helmet());
// Request compression
app.use(compression());
// Rate limiting on auth endpoints
app.use('/api/auth', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5 // 5 attempts
}));
TypeScript (v5.4)
What it is: Typed superset of JavaScript
Why we chose it:
- Type Safety: Catch errors at compile time, not runtime
- IDE Support: Excellent autocomplete and refactoring
- Code Quality: Enforces better code structure
- Documentation: Types serve as inline documentation
- Maintainability: Easier to refactor large codebases
Use cases in our project:
- Type definitions for database models
- API request/response types
- Service layer interfaces
- Utility type safety
Example type definitions:
// Game type from database
interface Game {
id: string;
title: string;
platform: string;
developer?: string;
publisher?: string;
releaseDate?: string;
library: string;
}
// API response type
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
}
Configuration highlights (tsconfig.json):
- Strict mode enabled
- ES2022 target
- Node.js module resolution
- Source maps for debugging
BetterSqlite3 (v12.6)
What it is: Fastest and simplest SQLite3 library for Node.js
Why we chose it over alternatives:
- Synchronous API: Simpler code than async wrappers
- Performance: 2-3x faster than async alternatives
- Reliability: Fewer race conditions with sync operations
- Prepared Statements: Compiled once, executed many times
- Transaction Support: ACID guarantees
Alternatives considered:
sqlite3(async): More complex error handlingsql.js(WebAssembly): Slower, no native performance- PostgreSQL: Overkill for self-hosted app
Use cases in our project:
// Read-only connection to flashpoint.sqlite
const fpDb = new Database(FLASHPOINT_DB_PATH, { readonly: true });
// Read-write connection to user.db
const userDb = new Database(USER_DB_PATH);
// Prepared statement for game search
const stmt = fpDb.prepare(`
SELECT * FROM game
WHERE title LIKE ?
AND platform = ?
`);
const games = stmt.all(`%${search}%`, platform);
Benefits:
- No separate database server required
- Perfect for self-hosted applications
- Flashpoint compatibility (uses same format)
- Fast read-heavy workloads
Limitations:
- Single writer at a time
- Not ideal for high-concurrency writes
- File-based (no network clustering)
JWT (jsonwebtoken v9.0)
What it is: JSON Web Token implementation for authentication
Why we chose it:
- Stateless: No server-side session storage required
- Scalable: Works across multiple backend instances
- Standard: RFC 7519 industry standard
- Flexible: Embed user data and permissions in token
- Secure: Signed with secret, optionally encrypted
Use cases in our project:
// Generate access token (1 hour expiry)
const accessToken = jwt.sign(
{
userId: user.id,
username: user.username,
permissions: user.permissions
},
JWT_SECRET,
{ expiresIn: '1h' }
);
// Generate refresh token (7 days expiry)
const refreshToken = jwt.sign(
{ userId: user.id },
JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
Security considerations:
- Secret stored in environment variable
- Short-lived access tokens (1 hour)
- Long-lived refresh tokens (7 days)
- Refresh token rotation on use
bcrypt (v6.0)
What it is: Password hashing library based on Blowfish cipher
Why we chose it:
- Security: Designed specifically for passwords
- Slow by Design: Resistant to brute-force attacks
- Salt Included: Automatic salt generation and storage
- Adaptive: Cost factor increases as hardware improves
Alternatives considered:
scrypt: More complex configurationargon2: Newer but more dependenciespbkdf2: Lower security margins
Use cases in our project:
// Hash password on registration
const hashedPassword = await bcrypt.hash(password, 10);
// Verify password on login
const isValid = await bcrypt.compare(password, user.hashedPassword);
Configuration:
- Cost factor: 10 (2^10 iterations)
- Balances security with performance
- ~100ms hash time on modern hardware
Winston (v3.11)
What it is: Versatile logging library for Node.js
Why we chose it:
- Flexible: Multiple transports (console, file, HTTP)
- Structured Logging: JSON format support
- Log Levels: error, warn, info, debug
- Performance: Async logging doesn't block
- Ecosystem: Integrates with monitoring tools
Use cases in our project:
// Logger configuration
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Usage
logger.info('User logged in', { userId, username });
logger.error('Database error', { error: err.message });
Zod (v3.22)
What it is: TypeScript-first schema validation library
Why we chose it:
- Type Inference: Automatic TypeScript type generation
- Composable: Build complex schemas from simple ones
- Error Messages: Detailed validation errors
- Runtime Safety: Validates external input at runtime
Use cases in our project:
// Define schema
const loginSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(8)
});
// Validate and infer type
const result = loginSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.errors });
}
// result.data is now type-safe
const { username, password } = result.data;
Frontend Stack Deep Dive
React (v18.3)
What it is: JavaScript library for building user interfaces
Why we chose it:
- Component Model: Reusable, composable UI components
- Ecosystem: Largest React library ecosystem
- Performance: Virtual DOM and reconciliation
- Developer Experience: Great tooling and DevTools
- Community: Massive community support
React 18 features we use:
- Automatic Batching: Multiple state updates batched
- Transitions: Smooth UI updates without blocking
- Suspense: Better async rendering (future)
Use cases in our project:
// Component example
function GameCard({ game }: { game: Game }) {
return (
<Card>
<CardHeader>
<CardTitle>{game.title}</CardTitle>
</CardHeader>
<CardContent>
<p>{game.platform}</p>
<p>{game.developer}</p>
</CardContent>
</Card>
);
}
Vite (v5.2)
What it is: Next-generation frontend build tool
Why we chose it over alternatives:
- Dev Server Speed: Instant HMR via native ES modules
- Build Performance: esbuild for fast production builds
- Simple Config: Less complex than Webpack
- Modern: Built for modern browsers
- Plugin Ecosystem: Growing collection of plugins
Alternatives considered:
- Create React App: Slower HMR, less flexible
- Webpack: More complex configuration
- Parcel: Less ecosystem support
Features we use:
// vite.config.ts
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': 'http://localhost:3100',
'/proxy': 'http://localhost:22500'
}
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router': ['react-router-dom'],
'ui': ['@radix-ui/react-dialog', '@radix-ui/react-select']
}
}
}
}
});
TanStack Query (v5.28)
What it is: Powerful data synchronization library for React
Why we chose it:
- Automatic Caching: Smart cache management
- Background Refetching: Keep data fresh
- Loading States: Built-in loading/error states
- Devtools: Excellent debugging experience
- TypeScript: First-class TypeScript support
Alternatives considered:
- Redux Toolkit Query: More boilerplate
- SWR: Less features, simpler
- Apollo Client: GraphQL-specific
Use cases in our project:
// Fetch games with automatic caching
function useGames(filters: GameFilters) {
return useQuery({
queryKey: ['games', filters],
queryFn: () => api.getGames(filters),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes
});
}
// Component usage
function GameList() {
const { data, isLoading, error } = useGames({ platform: 'Flash' });
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <GameGrid games={data.games} />;
}
Benefits:
- Eliminates 90% of data fetching boilerplate
- Automatic request deduplication
- Optimistic updates
- Infinite scroll support
Zustand (v4.5)
What it is: Small, fast state management library
Why we chose it over alternatives:
- Simplicity: Minimal boilerplate
- Performance: Optimized re-renders
- TypeScript: Excellent type inference
- DevTools: Redux DevTools compatible
- Size: Only 1KB gzipped
Alternatives considered:
- Redux: Too much boilerplate
- MobX: Requires decorators
- Context API: Performance issues with frequent updates
Use cases in our project:
// Auth store
interface AuthState {
user: User | null;
token: string | null;
login: (token: string, user: User) => void;
logout: () => void;
}
const useAuthStore = create<AuthState>((set) => ({
user: null,
token: localStorage.getItem('token'),
login: (token, user) => {
localStorage.setItem('token', token);
set({ token, user });
},
logout: () => {
localStorage.removeItem('token');
set({ token: null, user: null });
},
}));
// Component usage
function Header() {
const user = useAuthStore((state) => state.user);
const logout = useAuthStore((state) => state.logout);
return user ? <UserMenu user={user} onLogout={logout} /> : <LoginButton />;
}
State managed in Zustand:
- Authentication state
- Theme preferences
- UI state (sidebar, view mode)
- Filter preferences
Tailwind CSS (v3.4)
What it is: Utility-first CSS framework
Why we chose it:
- Productivity: Build UIs without writing custom CSS
- Consistency: Design system baked in
- Performance: Purges unused CSS
- Responsive: Mobile-first breakpoints
- Customization: Fully configurable theme
Alternatives considered:
- Styled Components: Runtime overhead
- CSS Modules: More boilerplate
- Bootstrap: Less customizable
Configuration:
// tailwind.config.js
export default {
darkMode: 'class',
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
// Shadcn UI color system
border: 'hsl(var(--border))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
},
},
},
plugins: [require('tailwindcss-animate')],
};
Shadcn UI (Latest)
What it is: Collection of re-usable components built with Radix UI and Tailwind
Why we chose it:
- Customizable: Copy components to your project
- Accessible: Built on Radix UI primitives
- Consistent: Pre-designed component system
- No Runtime: Just code you own
- TypeScript: Fully typed components
Alternatives considered:
- Material UI: Less customizable
- Ant Design: Heavy bundle size
- Chakra UI: Runtime style generation
Components we use:
- Button, Card, Dialog, DropdownMenu
- Input, Select, Checkbox, Switch
- Toast, Tooltip, Popover
- Table, Pagination, Scroll Area
Example:
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
function GameActions({ game }: { game: Game }) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Details</Button>
</DialogTrigger>
<DialogContent>
<GameDetails game={game} />
</DialogContent>
</Dialog>
);
}
Ruffle (v0.2.0-nightly)
What it is: Flash Player emulator written in Rust, compiled to WebAssembly
Why we need it:
- Adobe Flash EOL: Flash Player no longer supported
- Browser Support: Runs in modern browsers without plugin
- Security: Sandboxed WebAssembly execution
- Open Source: Community-driven preservation
Integration in our project:
// Ruffle player component
function RufflePlayer({ gameUrl }: { gameUrl: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const ruffle = window.RufflePlayer.newest();
const player = ruffle.createPlayer();
containerRef.current.appendChild(player);
player.load({
url: gameUrl,
parameters: '?scale=showall',
allowScriptAccess: true,
});
return () => player.remove();
}, [gameUrl]);
return <div ref={containerRef} className="ruffle-container" />;
}
Limitations:
- Not 100% Flash compatible
- Some ActionScript 3 features unsupported
- Performance slower than native Flash Player
Deployment:
- Files copied to
public/ruffle/via postinstall script - Loaded from CDN in production for better caching
Game Service Stack
node-stream-zip (v1.15)
What it is: Zero-dependency ZIP file streaming library
Why we chose it:
- Streaming: No extraction required
- Memory Efficient: Random access to ZIP contents
- Fast: Direct file reads from archive
- Simple API: Easy to use
Alternatives considered:
adm-zip: Loads entire archive to memoryjszip: Async API complexityyauzl: More complex API
Use cases in our project:
import StreamZip from 'node-stream-zip';
// Open ZIP archive
const zip = new StreamZip.async({
file: '/path/to/game.zip'
});
// Stream file from ZIP
app.get('/gamezip/:gameId/*', async (req, res) => {
const filePath = req.params[0];
const entry = await zip.entry(filePath);
if (!entry) return res.status(404).send('Not found');
res.setHeader('Content-Type', getMimeType(filePath));
res.setHeader('Content-Length', entry.size);
const stream = await zip.stream(entry.name);
stream.pipe(res);
});
Benefits:
- Games stay compressed on disk
- Instant file serving (no extraction wait)
- Scales to thousands of ZIP files
Development Tools
ESLint (v8.57)
Purpose: Code linting and style enforcement
Configuration:
{
"extends": [
"@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
tsx (v4.7)
Purpose: TypeScript execution and hot reload in development
Why we use it:
- Faster than
ts-node - Built on esbuild
- Better watch mode
Usage:
tsx watch src/server.ts # Backend dev server
Vitest (v1.3)
Purpose: Unit testing framework
Why we chose it:
- Vite integration (shares config)
- Fast test execution
- Compatible with Jest API
- TypeScript support
Example test:
import { describe, it, expect } from 'vitest';
import { GameService } from './GameService';
describe('GameService', () => {
it('should search games by title', () => {
const games = GameService.search({ title: 'Mario' });
expect(games).toHaveLength(10);
expect(games[0].title).toContain('Mario');
});
});
Technology Decision Matrix
Decision Criteria
For each technology, we evaluated:
- Performance: Runtime and build performance
- Developer Experience: Ease of use, documentation
- Ecosystem: Available libraries and tools
- Type Safety: TypeScript support quality
- Community: Community size and activity
- Maintenance: Active development and updates
- Bundle Size: Impact on final application size
- Learning Curve: Time to productivity
Key Technology Decisions
React vs Vue vs Svelte
| Criteria | React | Vue | Svelte |
|---|---|---|---|
| Ecosystem | Excellent | Good | Growing |
| TypeScript | Excellent | Good | Good |
| Community | Largest | Large | Smaller |
| Learning Curve | Moderate | Easy | Easy |
| Bundle Size | Larger | Medium | Smallest |
| Decision | ✓ Chosen | ✗ | ✗ |
Reason: Largest ecosystem (Shadcn UI, TanStack Query)
TanStack Query vs Redux Toolkit Query vs SWR
| Criteria | TanStack | RTK Query | SWR |
|---|---|---|---|
| Features | Most | Many | Fewer |
| TypeScript | Excellent | Good | Good |
| DevTools | Excellent | Excellent | Basic |
| Boilerplate | Low | Medium | Lowest |
| Caching | Advanced | Advanced | Basic |
| Decision | ✓ Chosen | ✗ | ✗ |
Reason: Best balance of features and DX
SQLite vs PostgreSQL vs MongoDB
| Criteria | SQLite | PostgreSQL | MongoDB |
|---|---|---|---|
| Setup | Embedded | Server | Server |
| Scaling | Read-heavy | Excellent | Excellent |
| Flashpoint | Native | Migration | Migration |
| Complexity | Lowest | Medium | Medium |
| Decision | ✓ Chosen | ✗ | ✗ |
Reason: Flashpoint compatibility, self-hosted simplicity
Package Dependencies Summary
Backend Production Dependencies
{
"axios": "^1.13.2", // HTTP client
"bcrypt": "^6.0.0", // Password hashing
"better-sqlite3": "^12.6.0", // SQLite driver
"compression": "^1.7.4", // Response compression
"cors": "^2.8.5", // CORS middleware
"dotenv": "^16.4.5", // Environment variables
"express": "^4.18.3", // Web framework
"express-rate-limit": "^7.1.5", // Rate limiting
"helmet": "^7.1.0", // Security headers
"jsonwebtoken": "^9.0.3", // JWT tokens
"winston": "^3.11.0", // Logging
"zod": "^3.22.4" // Schema validation
}
Frontend Production Dependencies
{
"@ruffle-rs/ruffle": "^0.2.0-nightly",
"@tanstack/react-query": "^5.28.9",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.2.0",
"axios": "^1.6.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.22.3",
"zustand": "^4.5.2",
"lucide-react": "^0.358.0",
"tailwindcss": "^3.4.1"
}
Game Service Production Dependencies
{
"express": "^4.18.3",
"node-stream-zip": "^1.15.0",
"winston": "^3.11.0",
"cors": "^2.8.5",
"axios": "^1.6.8",
"dotenv": "^16.4.5"
}
Performance Characteristics
Bundle Sizes
Frontend (Production Build):
- Main bundle: ~150 KB (gzipped)
- React vendor: ~130 KB (gzipped)
- Shadcn UI: ~50 KB (gzipped)
- Ruffle: ~2.5 MB (loaded separately)
- Total first load: ~330 KB + Ruffle
Backend:
- Docker image: ~200 MB
- Runtime memory: ~50-100 MB
- Database: 100 MB (user.db), 4 GB (flashpoint.sqlite)
Game Service:
- Docker image: ~150 MB
- Runtime memory: ~30-50 MB per instance
Runtime Performance
Backend API:
- Average response time: <50ms
- Game search: <100ms (with filters)
- Database queries: <10ms (indexed)
Frontend:
- First contentful paint: <1s
- Time to interactive: <2s
- Game grid render: <100ms (virtualized)
Game Service:
- File streaming: Wire speed
- ZIP file access: <50ms
- CDN fallback: 200-500ms (network dependent)
Version Compatibility
Node.js Versions
- Required: Node.js 20+
- Recommended: Node.js 20 LTS
- Tested: Node.js 20.11.0
Browser Support
Frontend:
- Chrome/Edge: Last 2 versions
- Firefox: Last 2 versions
- Safari: Last 2 versions
- Opera: Last 2 versions
Required features:
- ES2020 support
- WebAssembly (for Ruffle)
- CSS Grid and Flexbox
- localStorage
Future Technology Considerations
Potential Upgrades
- React 19: Automatic memoization, Server Components
- Vite 6: Improved cold start times
- TanStack Query v6: Enhanced cache management
- PostgreSQL: Better concurrent write performance
- Redis: Distributed caching layer
Experimental Features
- WebSocket: Real-time play session updates
- PWA: Offline game browsing
- WebRTC: Peer-to-peer game sharing
- WASM: Port critical backend logic to Rust
Related Documentation
- Architecture Overview - System design and service communication
- Getting Started - Quick setup guide
- Backend Guide - Backend development details
- Frontend Guide - Frontend development details
- Deployment Guide - Production deployment
Summary
Flashpoint Web's technology stack is carefully chosen for:
- Developer Experience: TypeScript, Vite, TanStack Query reduce boilerplate
- Performance: Vite HMR, BetterSqlite3, TanStack Query caching
- Type Safety: TypeScript across all services prevents runtime errors
- Maintainability: Well-established technologies with active communities
- Self-Hosting: SQLite, Node.js enable simple deployment
The stack balances modern best practices with pragmatic choices for a self-hosted game archive application.