# 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