Architecture How every component fits together
ProxyServer is a ~2300-line Node.js application with only 2 npm dependencies (ws and node-forge). Everything else is built from scratch using Node's standard library. This page explains every module, its role, and how they communicate.
System Architecture
Module-by-Module Breakdown
server.js — Entry Point
The orchestrator. It creates every component, wires them together, starts both servers, and handles graceful shutdown on SIGINT/SIGTERM.
- Creates
TrafficStore(5000 entry ring buffer) - Initializes
CertManagerfor TLS MITM - Creates
RuleStore→RuleEngine→ Interceptors - Creates
TLSHandlerwith cert manager and interceptors - Starts
ProxyServeron :9080 - Starts
DashboardServeron :9081 - Attaches
WSBridgeandChatHandlerto dashboard
Proxy Layer
proxy-server.js — HTTP Forward Proxy
The core proxy. Handles two types of requests:
- HTTP requests (absolute-URI format like
GET http://example.com/path) — Parsed, captured to TrafficStore, forwarded to upstream with optional interception - CONNECT tunnels (
CONNECT example.com:443) — Delegated to TLSHandler for HTTPS MITM, or tunneled blindly if no TLSHandler
Handles gzip/deflate/brotli decompression of response bodies for display, while forwarding the original compressed bytes to the client.
tls-handler.js — TLS MITM + HTTP/2
The most complex module. Handles HTTPS interception:
- Receives CONNECT tunnel from proxy-server.js
- Creates a
tls.TLSSocketwrapping the client socket, using a generated certificate for the target hostname - Parses HTTP/1.1 requests from the decrypted stream using
HTTPStreamParser - Sends upstream requests via HTTP/2 (ALPN negotiation) or falls back to HTTP/1.1
- Translates HTTP/2 responses back to HTTP/1.1 for the client tunnel
- Supports keep-alive — multiple requests per CONNECT tunnel
HTTPStreamParser is an inner class that provides robust HTTP/1.1 message parsing from any Node.js stream, supporting:
Content-Lengthbased body readingTransfer-Encoding: chunkedbody reassembly- Connection-close body reading (for responses without content-length)
- Proper header parsing with duplicate header support
cert-manager.js — Certificate Generation
Manages the CA certificate and generates per-host leaf certificates on demand. Uses node-forge for RSA 2048-bit key generation and X.509 certificate creation. Two-tier cache: memory Map + disk (certs/hosts/).
See TLS & Certificates for a deep dive.
request-interceptor.js — Request Hold/Forward/Drop
When intercept is enabled and a rule matches, this module holds the request via a Promise. The promise resolves when the user clicks Forward (with optional modifications) or rejects when they click Drop. A 5-minute timeout auto-forwards to prevent connection leaks.
response-interceptor.js — Response Hold/Forward/Drop
Mirror of RequestInterceptor but for the response phase. Listens for forward-response and drop-response events from TrafficStore. Same 5-minute timeout.
Data Layer
traffic-entry.js — Request/Response Data Model
Each captured request/response pair is a TrafficEntry with:
Two serialization methods: toJSON() (full detail with base64-encoded bodies) and toSummary() (lightweight, no bodies — used for the traffic list).
traffic-store.js — Ring Buffer + Event Emitter
In-memory storage with a configurable size limit (default 5000). Uses a Map for O(1) lookups and an Array for ordered iteration. When full, the oldest entry is evicted (FIFO).
Extends EventEmitter and fires:
add— new entry added (triggers WSBridge broadcast)update— entry state changed (triggers WSBridge broadcast)clear— all entries removed (triggers WSBridge broadcast)forward/drop— user action from dashboard (triggers interceptor resolution)forward-response/drop-response— same for response phase
har-export.js — HAR 1.2 Export
Converts TrafficEntry arrays to the HAR 1.2 format. Maps request/response data, calculates timings, and extracts query strings.
Rules Layer
rule-store.js — Persistent Rule Storage
CRUD operations for intercept rules. Persists to rules.json on every modification. Each rule has a UUID, pattern fields, and a direction.
rule-engine.js — Pattern Matching
Evaluates rules against traffic entries. Converts glob patterns to RegExp (e.g., *api* → /^.*api.*$/i). Matches by URL, method, content-type, and header presence/value. Filters by direction for request vs. response phase.
Dashboard Layer
dashboard-server.js — HTTP Server + REST API
A plain Node.js http.createServer that serves static files and the REST API. No Express, no frameworks — just URL parsing and pattern matching. Serves all files from the static/ directory with proper MIME types.
ws-bridge.js — WebSocket Bridge
Creates a WebSocket.Server attached to the dashboard HTTP server. Listens for TrafficStore events and broadcasts them to all connected clients. Also routes incoming chat:* messages to ChatHandler.
Chat Layer
chat-handler.js — Message Router
Routes chat:send, chat:reset, chat:compact, and chat:context-toggle messages. Manages per-client context toggles and a shared ClaudeSession instance.
claude-session.js — CLI Subprocess Manager
Spawns claude -p <prompt> --output-format stream-json per turn. Maintains conversation history server-side. Parses streaming JSON events and emits chunk and action events for real-time UI updates.
context-builder.js — System Prompt Assembly
Assembles the system prompt from conditional context blocks (traffic summaries, selected entry detail, rules, source code, browser context). Caches source files with fs.watch invalidation. Estimates token counts for the budget indicator.
Frontend Layer
app.js — Main Dashboard UI (Vanilla JS IIFE)
~700 lines of vanilla JavaScript in an IIFE closure. State management via a single state object. WebSocket for real-time updates. Renders the traffic list, detail panel, modals, and handles keyboard shortcuts.
chat.js — Chat Panel UI (Vanilla JS IIFE)
~500 lines in a separate IIFE. Creates its DOM programmatically and mounts to #chat-container. Uses the same WebSocket connection (messages are discriminated by chat: prefix). Handles streaming display, context toggles, and slash commands.
Project Structure
Communication Patterns
EventEmitter (Server-side)
TrafficStore is the central event bus. Components communicate by emitting and listening to events on the store:
WebSocket (Client-Server)
A single WebSocket connection per browser tab carries both traffic updates and chat messages:
| Direction | Message Types |
|---|---|
| Server → Client | init, add, update, clear (traffic); chat:status, chat:chunk, chat:done, chat:error, chat:action (chat) |
| Client → Server | chat:send, chat:reset, chat:compact, chat:context-toggle |
Design Decisions
No Frameworks
ProxyServer uses no web frameworks (no Express, no React). This is intentional — it keeps the codebase small, dependency-free, and educational. Every HTTP handler, every DOM element, every WebSocket message is explicit.
Two Ports
Proxy (:9080) and Dashboard (:9081) are separate to avoid feedback loops where dashboard requests create traffic entries.
Ring Buffer, Not Database
Traffic is stored in memory with FIFO eviction. This keeps the proxy fast and simple — no disk I/O on the hot path. Sessions can be saved to disk explicitly.
Promise-Based Interception
Interceptors use Promises to hold requests. This integrates naturally with Node's async/await and lets the proxy pipeline wait cleanly without callbacks.