Files
spotlightcam/docs/ARCHITECTURE.md
Radosław Gierwiało a1357393e8 docs: optimize documentation structure for token efficiency
- Add SESSION_CONTEXT.md: ultra-compact context for new sessions (~500 lines)
- Add ARCHITECTURE.md: detailed technical specs and implementation details
- Add COMPLETED.md: archive of completed tasks (Phase 0)
- Add RESOURCES.md: learning resources and documentation links
- Refactor CONTEXT.md: keep only core project info and guidelines
- Refactor TODO.md: keep only active tasks and next steps
- Update README.md: reference new documentation structure

This change reduces token usage when resuming sessions by ~60% while maintaining complete project documentation in separate, well-organized files.
2025-11-12 18:07:42 +01:00

653 lines
17 KiB
Markdown

# Architecture - spotlight.cam
**Technical architecture and implementation details**
---
## System Architecture
```
┌─────────────────┐
│ Web Browser │ (PWA - React + Vite + Tailwind)
│ (Android/iOS) │
└────────┬────────┘
│ HTTPS
┌─────────────────┐
│ nginx │ (Reverse Proxy, Port 8080)
│ (Port 8080) │
└────────┬────────┘
┌────┴────┐
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Frontend│ │ Backend │ (Node.js + Express + Socket.IO)
│(Vite) │ │ (planned) │
│:5173 │ │ :3000 │
└────────┘ └──────┬─────┘
┌─────────────┐
│ PostgreSQL │ (Database)
│ (planned) │
└─────────────┘
WebRTC P2P Connection:
Browser A ←──────────────────────→ Browser B
(Direct P2P via WebRTC)
│ (Signaling only via WebSocket)
Backend
```
---
## Docker Compose Services
### Current Setup
**nginx:**
- Image: `nginx:alpine`
- Port: 8080 (host) → 80 (container)
- Volumes: nginx config files
- Purpose: Reverse proxy, serves frontend, will proxy `/api/*` to backend
**frontend:**
- Build: `./frontend/Dockerfile`
- Internal port: 5173 (Vite dev server)
- Volumes: hot reload support
- Environment: `NODE_ENV=development`, `VITE_HOST=0.0.0.0`
### Planned Services
**backend:**
- Build: `./backend/Dockerfile`
- Internal port: 3000
- Environment: DB credentials, JWT secret, etc.
- Depends on: `db`
**db:**
- Image: `postgres:15-alpine`
- Port: 5432 (internal only)
- Volumes: PostgreSQL data persistence
- Environment: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
---
## nginx Configuration
**File:** `nginx/conf.d/default.conf`
### Upstream
```nginx
upstream frontend {
server frontend:5173;
}
upstream backend {
server backend:3000; # planned
}
```
### Routes
- `/` → frontend (React app)
- `/api/*` → backend (REST API) - planned
- `/socket.io/*` → backend (WebSocket) - planned
### Features
- WebSocket support (Upgrade headers for Vite HMR and future Socket.IO)
- Client max body size: 500M (for large video uploads via link)
- Proxy headers: X-Real-IP, X-Forwarded-For, etc.
---
## Frontend Architecture
### Tech Stack
- **React 18** - UI library
- **Vite** - Build tool, dev server
- **Tailwind CSS v3.4.0** - Styling (NOT v4 - compatibility issues)
- **React Router** - Client-side routing
- **Context API** - State management (will add Socket.IO client, WebRTC logic)
### Folder Structure
```
frontend/
├── public/ # Static assets
├── src/
│ ├── main.jsx # Entry point
│ ├── App.jsx # Router setup
│ ├── pages/ # Page components
│ │ ├── LoginPage.jsx
│ │ ├── RegisterPage.jsx
│ │ ├── EventsPage.jsx
│ │ ├── EventChatPage.jsx
│ │ ├── MatchChatPage.jsx # ← Main WebRTC mockup
│ │ ├── RatePartnerPage.jsx
│ │ └── HistoryPage.jsx
│ ├── components/
│ │ └── layout/
│ │ ├── Layout.jsx
│ │ └── Navbar.jsx
│ ├── contexts/
│ │ └── AuthContext.jsx # Mock auth (will become real)
│ ├── mocks/ # Mock data (will be replaced by API calls)
│ │ ├── users.js
│ │ ├── events.js
│ │ ├── messages.js
│ │ ├── matches.js
│ │ └── ratings.js
│ └── index.css # Tailwind imports
├── tailwind.config.js # Tailwind v3.4.0 config
├── vite.config.js # Vite config
└── package.json
```
### State Management
**Current (Mock):**
- AuthContext: mock login/register/logout with localStorage
- Local state in components
**Planned:**
- Socket.IO client for real-time chat
- WebRTC connection state
- Redux/Zustand (if needed for complex state)
---
## Backend Architecture (Planned)
### Tech Stack
- **Node.js** - Runtime
- **Express** - Web framework
- **Socket.IO** - WebSocket for chat and WebRTC signaling
- **PostgreSQL** - Database
- **Prisma/Knex** - ORM/Query builder
- **bcrypt** - Password hashing
- **JWT** - Authentication tokens
### Folder Structure (Proposed)
```
backend/
├── src/
│ ├── server.js # Entry point
│ ├── app.js # Express app setup
│ ├── routes/ # REST API routes
│ │ ├── auth.js # POST /api/auth/register, /login
│ │ ├── users.js # GET /api/users/me, etc.
│ │ ├── events.js # GET /api/events, etc.
│ │ ├── matches.js # POST /api/matches, etc.
│ │ └── ratings.js # POST /api/ratings, etc.
│ ├── controllers/ # Business logic
│ ├── models/ # Database models (Prisma schema or Knex queries)
│ ├── middleware/ # Auth, validation, error handling
│ ├── socket/ # Socket.IO event handlers
│ │ ├── chat.js # Event chat, private chat
│ │ └── webrtc.js # WebRTC signaling (SDP/ICE)
│ └── utils/ # Helpers
├── prisma/ # (if using Prisma)
│ └── schema.prisma # Database schema
├── migrations/ # Database migrations
├── .env.example
└── package.json
```
### API Endpoints (Planned)
**Auth:**
- `POST /api/auth/register` - Create account
- `POST /api/auth/login` - Login, get JWT
- `POST /api/auth/logout` - Logout (optional, JWT is stateless)
**Users:**
- `GET /api/users/me` - Get current user
- `PATCH /api/users/me` - Update profile
- `GET /api/users/:id` - Get user profile
**Events:**
- `GET /api/events` - List all events
- `GET /api/events/:id` - Event details
- `POST /api/events/:id/join` - Join event
**Matches:**
- `GET /api/matches` - User's match history
- `POST /api/matches` - Create match
- `PATCH /api/matches/:id` - Update match status
**Ratings:**
- `POST /api/ratings` - Rate partner
- `GET /api/ratings/user/:id` - User's ratings
- `GET /api/ratings/stats/:id` - Rating statistics
**Reports:**
- `POST /api/reports` - Report user
---
## Database Schema
### Tables
**users:**
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
avatar VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**events:**
```sql
CREATE TABLE events (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
location VARCHAR(255) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
worldsdc_id VARCHAR(100) UNIQUE,
participants_count INT DEFAULT 0,
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
```
**chat_rooms:**
```sql
CREATE TABLE chat_rooms (
id SERIAL PRIMARY KEY,
event_id INT REFERENCES events(id),
type VARCHAR(20) NOT NULL, -- 'event' or 'private'
created_at TIMESTAMP DEFAULT NOW()
);
```
**messages:**
```sql
CREATE TABLE messages (
id SERIAL PRIMARY KEY,
room_id INT REFERENCES chat_rooms(id) ON DELETE CASCADE,
user_id INT REFERENCES users(id),
content TEXT NOT NULL,
type VARCHAR(20) NOT NULL, -- 'text', 'link', 'video'
created_at TIMESTAMP DEFAULT NOW()
);
```
**matches:**
```sql
CREATE TABLE matches (
id SERIAL PRIMARY KEY,
user1_id INT REFERENCES users(id),
user2_id INT REFERENCES users(id),
event_id INT REFERENCES events(id),
room_id INT REFERENCES chat_rooms(id),
status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'accepted', 'completed'
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_match UNIQUE (user1_id, user2_id, event_id)
);
```
**ratings:**
```sql
CREATE TABLE ratings (
id SERIAL PRIMARY KEY,
match_id INT REFERENCES matches(id),
rater_id INT REFERENCES users(id),
rated_id INT REFERENCES users(id),
score INT CHECK (score >= 1 AND score <= 5),
comment TEXT,
would_collaborate_again BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_rating UNIQUE (match_id, rater_id, rated_id)
);
```
### Indexes (Performance)
```sql
CREATE INDEX idx_messages_room_id ON messages(room_id);
CREATE INDEX idx_messages_created_at ON messages(created_at);
CREATE INDEX idx_matches_user1_id ON matches(user1_id);
CREATE INDEX idx_matches_user2_id ON matches(user2_id);
CREATE INDEX idx_matches_event_id ON matches(event_id);
CREATE INDEX idx_ratings_rated_id ON ratings(rated_id);
```
---
## WebSocket (Socket.IO) Events
### Client → Server
**Connection:**
- `connect` - Client connects
- `disconnect` - Client disconnects
**Event Chat:**
- `join_event_room` - Join event chat room
- Payload: `{ eventId: number }`
- `leave_event_room` - Leave event chat room
- Payload: `{ eventId: number }`
- `send_event_message` - Send message to event chat
- Payload: `{ eventId: number, content: string }`
**Private Chat:**
- `join_private_room` - Join private match room
- Payload: `{ matchId: number }`
- `send_private_message` - Send message to match partner
- Payload: `{ matchId: number, content: string }`
**Matchmaking:**
- `send_match_request` - Request match with user
- Payload: `{ targetUserId: number, eventId: number }`
- `accept_match_request` - Accept match request
- Payload: `{ matchId: number }`
**WebRTC Signaling:**
- `webrtc_offer` - Send SDP offer
- Payload: `{ matchId: number, offer: RTCSessionDescriptionInit }`
- `webrtc_answer` - Send SDP answer
- Payload: `{ matchId: number, answer: RTCSessionDescriptionInit }`
- `webrtc_ice_candidate` - Send ICE candidate
- Payload: `{ matchId: number, candidate: RTCIceCandidate }`
### Server → Client
**Event Chat:**
- `event_message` - New message in event chat
- Payload: `{ id, roomId, userId, username, avatar, content, createdAt }`
- `active_users` - List of active users in event
- Payload: `{ users: Array }`
**Private Chat:**
- `private_message` - New message from partner
- Payload: `{ id, matchId, userId, username, content, createdAt }`
**Matchmaking:**
- `match_request_received` - Someone wants to match
- Payload: `{ matchId, fromUser }`
- `match_accepted` - Match request accepted
- Payload: `{ matchId, partner }`
**WebRTC Signaling:**
- `webrtc_offer` - Received SDP offer from partner
- `webrtc_answer` - Received SDP answer from partner
- `webrtc_ice_candidate` - Received ICE candidate from partner
---
## WebRTC P2P Architecture
### Components
**RTCPeerConnection:**
- Manages P2P connection between two browsers
- Handles ICE candidate gathering
- Manages connection state (new, connecting, connected, failed)
**RTCDataChannel:**
- Binary data transfer channel
- Ordered, reliable delivery (like TCP)
- Max message size: 16KB-64KB (browser dependent)
- Used for video file chunks
**STUN Server:**
- Public STUN server (e.g., Google: `stun:stun.l.google.com:19302`)
- Discovers public IP address
- Helps with NAT traversal
**TURN Server (Optional):**
- Relay server for difficult NAT/firewall scenarios
- Only ~10% of connections need TURN
- Can use free services or self-host
### File Transfer Flow
**1. Initiate Connection:**
```javascript
// Sender creates offer
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
const dataChannel = pc.createDataChannel('fileTransfer');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send offer to receiver via Socket.IO
socket.emit('webrtc_offer', { matchId, offer });
```
**2. Exchange SDP/ICE:**
```javascript
// Receiver receives offer via Socket.IO
socket.on('webrtc_offer', async ({ offer }) => {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('webrtc_answer', { matchId, answer });
});
// Both peers exchange ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('webrtc_ice_candidate', { matchId, candidate: event.candidate });
}
};
```
**3. Transfer File:**
```javascript
// Chunk file and send
const CHUNK_SIZE = 16384; // 16KB
const file = selectedFile;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
// Send metadata first
dataChannel.send(JSON.stringify({
type: 'metadata',
name: file.name,
size: file.size,
mimeType: file.type,
totalChunks
}));
// Send chunks
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const arrayBuffer = await chunk.arrayBuffer();
dataChannel.send(arrayBuffer);
// Update progress
const progress = ((i + 1) / totalChunks) * 100;
setTransferProgress(progress);
}
```
**4. Receive File:**
```javascript
// Receiver assembles chunks
let receivedChunks = [];
let metadata = null;
dataChannel.onmessage = (event) => {
if (typeof event.data === 'string') {
// Metadata
metadata = JSON.parse(event.data);
} else {
// Chunk
receivedChunks.push(event.data);
if (receivedChunks.length === metadata.totalChunks) {
// All chunks received, create file
const blob = new Blob(receivedChunks, { type: metadata.mimeType });
const url = URL.createObjectURL(blob);
// Trigger download
const a = document.createElement('a');
a.href = url;
a.download = metadata.name;
a.click();
}
}
};
```
### Error Handling
**Connection Failed:**
- Fallback to link sharing (Google Drive, Dropbox)
- Show user-friendly error message
- Log error for debugging
**Transfer Interrupted:**
- Detect connection state change
- Offer retry option
- Consider implementing resume capability (future)
---
## Security
### Backend Security
**Authentication:**
- bcrypt for password hashing (salt rounds: 10-12)
- JWT for session management (httpOnly cookies recommended)
- Token expiration (e.g., 24 hours)
- Refresh token mechanism (optional)
**API Security:**
- Rate limiting (express-rate-limit)
- Helmet.js for security headers
- CORS configuration (allow only frontend origin)
- Input validation (express-validator)
- SQL injection prevention (prepared statements, ORM)
- XSS prevention (sanitize user input)
### Frontend Security
**Storage:**
- JWT in httpOnly cookie (recommended) OR secure localStorage
- Never store passwords
- Clear sensitive data on logout
**Input Validation:**
- Client-side validation for UX
- Server-side validation for security
- Sanitize before displaying user content
### WebRTC Security
**Native Encryption:**
- DTLS (Datagram Transport Layer Security) for data channels
- SRTP (Secure Real-time Transport Protocol) for media
- Enabled by default, no configuration needed
**Additional Measures:**
- Verify signaling server (HTTPS + WSS)
- Optional: end-to-end encryption for chat (WebCrypto API, libsodium)
- No video files stored on server
---
## Deployment Architecture (Future)
### Production Stack
**Option 1: Single VPS**
- Ubuntu/Debian server
- Docker Compose
- nginx as reverse proxy + SSL (Let's Encrypt)
- PostgreSQL on same server
- Backup strategy
**Option 2: Cloud (Recommended)**
- Frontend: Vercel/Netlify (CDN, auto-scaling)
- Backend: AWS EC2/DigitalOcean/Render
- Database: AWS RDS/DigitalOcean Managed PostgreSQL
- nginx on backend for API
- Redis for session storage (optional)
**Option 3: Kubernetes (Overkill for MVP)**
- GKE/EKS/AKS
- Helm charts
- Auto-scaling
- Load balancing
### SSL/TLS
**Let's Encrypt:**
- Free SSL certificates
- Auto-renewal with Certbot
- Required for WebRTC (getUserMedia requires HTTPS)
### Environment Variables
**Backend:**
```env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@host:5432/dbname
JWT_SECRET=random-secret-key
CORS_ORIGIN=https://spotlight.cam
STUN_SERVER=stun:stun.l.google.com:19302
TURN_SERVER=turn:your-turn-server.com (optional)
```
**Frontend:**
```env
VITE_API_URL=https://api.spotlight.cam
VITE_WS_URL=wss://api.spotlight.cam
```
---
## Performance Considerations
### Frontend
- Code splitting (React.lazy)
- Image optimization
- Service worker caching (PWA)
- Minimize bundle size
### Backend
- Database connection pooling
- Query optimization (indexes)
- Caching (Redis for frequently accessed data)
- Compression (gzip/brotli)
### WebRTC
- Adaptive chunk size based on bandwidth
- Buffer management (avoid overwhelming DataChannel)
- Connection quality monitoring
- Automatic reconnection on failure
---
## Monitoring & Logging (Future)
**Backend:**
- Winston/Pino for logging
- Log levels: error, warn, info, debug
- Structured logging (JSON format)
**Database:**
- Query performance monitoring
- Slow query log
- Connection pool metrics
**WebRTC:**
- Connection success rate
- Transfer speed metrics
- Failure reasons
- Browser compatibility stats
---
**Last Updated:** 2025-11-12