Files
spotlightcam/docs/ARCHITECTURE.md
Radosław Gierwiało 63d528367e docs: update CONTEXT.md and ARCHITECTURE.md to reflect completed MVP
CONTEXT.md updates:
- Changed 'Planned' to 'Implemented' for backend, db, and WebRTC
- Updated Docker Compose components - all services now implemented
- Updated database models section with actual schema
- Updated tech stack - removed 'Planned' labels
- Added test coverage stats (223/223 tests passing)
- Updated Last Updated date to 2025-11-20
- Added MVP complete status

ARCHITECTURE.md updates:
- Updated architecture diagram - marked backend and db as  IMPL
- Changed 'Planned Services' to 'Implemented Services'
- Added production Dockerfile info
- Added test coverage (223/223 passing, 71%)
- Added Prisma ORM details
- Updated Last Updated date to 2025-11-20
- Added production-ready status

Both files now accurately reflect the completed MVP state.
2025-11-20 22:34:05 +01:00

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)  │ │   ✅ IMPL  │
│:5173   │ │  :3000     │
└────────┘ └──────┬─────┘
                  │
                  ▼
           ┌─────────────┐
           │ PostgreSQL  │  (Database with Prisma ORM)
           │   ✅ IMPL   │
           └─────────────┘

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

Implemented Services

backend:

  • Build: ./backend/Dockerfile (dev) / ./backend/Dockerfile.prod (prod)
  • Internal port: 3000
  • Environment: DB credentials, JWT secret, AWS SES, etc.
  • Depends on: db
  • Features: REST API, Socket.IO, JWT auth, Prisma ORM
  • Tests: 223/223 passing (71% coverage)

db:

  • Image: postgres:15-alpine
  • Port: 5432 (exposed in dev, internal in prod)
  • Volumes: PostgreSQL data persistence
  • Environment: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
  • Managed by: Prisma migrations

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 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:

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 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:

// 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-20

Status: All architecture components implemented and production-ready