- 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.
17 KiB
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
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 accountPOST /api/auth/login- Login, get JWTPOST /api/auth/logout- Logout (optional, JWT is stateless)
Users:
GET /api/users/me- Get current userPATCH /api/users/me- Update profileGET /api/users/:id- Get user profile
Events:
GET /api/events- List all eventsGET /api/events/:id- Event detailsPOST /api/events/:id/join- Join event
Matches:
GET /api/matches- User's match historyPOST /api/matches- Create matchPATCH /api/matches/:id- Update match status
Ratings:
POST /api/ratings- Rate partnerGET /api/ratings/user/:id- User's ratingsGET /api/ratings/stats/:id- Rating statistics
Reports:
POST /api/reports- Report user
Database Schema
Tables
users:
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:
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:
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:
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:
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:
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)
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 connectsdisconnect- Client disconnects
Event Chat:
join_event_room- Join event chat room- Payload:
{ eventId: number }
- Payload:
leave_event_room- Leave event chat room- Payload:
{ eventId: number }
- Payload:
send_event_message- Send message to event chat- Payload:
{ eventId: number, content: string }
- Payload:
Private Chat:
join_private_room- Join private match room- Payload:
{ matchId: number }
- Payload:
send_private_message- Send message to match partner- Payload:
{ matchId: number, content: string }
- Payload:
Matchmaking:
send_match_request- Request match with user- Payload:
{ targetUserId: number, eventId: number }
- Payload:
accept_match_request- Accept match request- Payload:
{ matchId: number }
- Payload:
WebRTC Signaling:
webrtc_offer- Send SDP offer- Payload:
{ matchId: number, offer: RTCSessionDescriptionInit }
- Payload:
webrtc_answer- Send SDP answer- Payload:
{ matchId: number, answer: RTCSessionDescriptionInit }
- Payload:
webrtc_ice_candidate- Send ICE candidate- Payload:
{ matchId: number, candidate: RTCIceCandidate }
- Payload:
Server → Client
Event Chat:
event_message- New message in event chat- Payload:
{ id, roomId, userId, username, avatar, content, createdAt }
- Payload:
active_users- List of active users in event- Payload:
{ users: Array }
- Payload:
Private Chat:
private_message- New message from partner- Payload:
{ id, matchId, userId, username, content, createdAt }
- Payload:
Matchmaking:
match_request_received- Someone wants to match- Payload:
{ matchId, fromUser }
- Payload:
match_accepted- Match request accepted- Payload:
{ matchId, partner }
- Payload:
WebRTC Signaling:
webrtc_offer- Received SDP offer from partnerwebrtc_answer- Received SDP answer from partnerwebrtc_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:
// 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:
// 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:
// 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:
// 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:
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:
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