RFC-0002: Frontend Architecture
| Metadata | Value |
|---|---|
| Status | Accepted |
| Created | 2025-02-13 |
| Updated | 2026-04-17 |
| Author | LessUp Team |
| Category | Architecture |
Context
The frontend is a vanilla JavaScript WebRTC client with modular architecture. No build tools, no frameworks — just pure ES6+ modules served directly by the Go HTTP server.
Decision
Technology Stack
- Language: JavaScript (ES6+)
- Build: None (no bundler, no transpiler)
- Styling: CSS3 with responsive design
- Templating: HTML5
Module Architecture
1
2
3
4
5
6
7
8
9
10
web/
├── index.html # UI structure
├── app.js # Main entry point, initialization
├── app.config.js # Configuration and capability detection
├── app.media.js # Media stream handling
├── app.peers.js # PeerConnection management
├── app.signaling.js # WebSocket signaling
├── app.ui.js # UI rendering
├── app.stats.js # Connection statistics
└── styles.css # Responsive styles
Module Responsibilities
app.config.js
Configuration and capability detection.
Exports:
DEFAULT_RTC_CONFIG— Default ICE server configurationROOM_STATE_TEXT— State display text mappingRECONNECT_DELAYS_MS— Reconnection backoff delayscreateClientId()— Generate unique client IDgetCapabilities()— Browser capability detectiongetRtcConfig()— Merge user/default ICE config
app.media.js
Media stream handling.
Exports:
ensureLocalMedia()— Get camera/mic stream (getUserMedia)currentVideoTrack()— Get active video tracksyncPeerMedia()— Update peer connection tracksstartScreenShare()— Begin screen capture (getDisplayMedia)stopScreenShare()— End screen capturestartRecording()— Begin MediaRecorderstopRecording()— End recording, trigger download
app.peers.js
PeerConnection management for Mesh topology.
Exports:
ensurePeer(peerId)— Create/get peer connectionapplyDescription(peerId, sdp)— Handle SDP offer/answerhandleCandidate(peerId, candidate)— Handle ICE candidatestartCall(peerId)— Initiate call to peerclosePeer(peerId)— End single connectioncloseAllPeers()— End all connectionssendChat()— Send DataChannel message
app.signaling.js
WebSocket signaling connection.
Exports:
connectWS()— Establish WebSocket connectionleaveRoom()— Clean exit from room
app.ui.js
UI rendering and state management.
Exports:
getElements()— DOM element cachecreateUI()— UI controller factorysetError(msg)— Display error messagesetRoomState(state)— Update room state displayrenderMembers(list)— Render member listensureRemoteTile(peerId)— Create video tile for peerremoveRemoteTile(peerId)— Remove peer video tileupdateControls()— Update button states
app.stats.js
Connection statistics monitoring.
Exports:
createStatsController(state)— Factorystart()— Begin stats pollingstop()— End stats polling
State Management
Global state object:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
state = {
myId: string, // Unique client ID
ws: WebSocket | null, // WebSocket connection
roomId: string, // Current room name
roomState: string, // 'idle' | 'connecting' | 'joined' | 'reconnecting' | 'calling'
localStream: MediaStream, // Local media
screenStream: MediaStream, // Screen share stream
usingScreen: boolean, // Currently screen sharing
muted: boolean, // Audio muted
cameraOff: boolean, // Video disabled
peers: Map<string, Peer>, // Peer connections
recorder: MediaRecorder, // Active recorder
recordedChunks: Blob[], // Recording data
}
Event Handling
| Event | Trigger | Action |
|---|---|---|
join.click |
Room input filled | connectWS() |
call.click |
Peer selected | startCall(peerId) |
hangup.click |
Active call | closePeer() or closeAllPeers() |
mute.click |
Local stream | Toggle audio track |
camera.click |
Local stream | Toggle video track |
screen.click |
In room | Toggle screen share |
Consequences
Positive:
- Zero dependencies, instant load
- Easy to understand and modify
- No build step required
- Clear module boundaries
Trade-offs:
- No TypeScript type safety (mitigated by TypeScript definitions in API docs)
- Manual DOM manipulation (acceptable for this scale)
- No component reuse library