Architecture overview
ProxyPro splits its work across two processes joined by a Unix domain socket. This page describes the shape of the system so you can read the source without getting lost.
Process model
┌───────────────────────────────────────────────────┐ ┌────────────────────────────┐│ Electron App │ │ proxypro-engine (Go) ││ │ │ ││ ┌─────────────┐ contextBridge ┌──────────┐│gRPC│ ││ │ Renderer │◀──ipcRenderer──────▶│ Main │◀┼─UDS─▶ HTTP MITM proxy on 9090 ││ │ React+Vite │ (typed) │ Node ││ │ capture stream + bodies ││ │ shadcn/ui │ │ grpc-js ││ │ rules engine ││ └─────────────┘ └──────────┘│ │ cert + mobileconfig │└───────────────────────────────────────────────────┘ └────────────────────────────┘ │ ▼ HTTP proxy :9090 browsers / apps / devicesEngine (Go)
- Single static binary at
engine/bin/proxypro-engine(universal arm64+amd64). - Spawned and supervised by Electron’s main process.
- Two listeners:
- Unix socket — gRPC control plane (
/tmp/proxypro-<pid>.sock) - TCP
:9090— HTTP proxy that browsers + apps configure.
- Unix socket — gRPC control plane (
Electron main (Node.js)
- Owns the engine subprocess lifecycle (spawn, restart, graceful shutdown).
- Hosts the
@grpc/grpc-jsclient that talks to the engine. - Exposes a narrow, typed IPC surface (
window.api.*) to the renderer viacontextBridge. Renderer cannot touch rawipcRenderer.
Electron renderer (React)
- Pure React 18 + TanStack Table + Tailwind. No grpc, no Node APIs.
- Subscribes to the capture stream through the main process.
- Renders the capture list (virtualized), detail tabs, rules editor, and mobile setup UI.
Why two processes?
We could put everything in the Electron main process via Node addons or re-implementing the MITM in TypeScript. We split because:
| Concern | Won by |
|---|---|
| Performance under load (10k flows/sec, gzip decode) | Go |
| Cross-compile to a single static binary | Go |
| Mature TLS / HTTP/2 stack | Go (crypto/tls, golang.org/x/net/http2) |
| Quick UI iteration | React + Vite HMR |
| Massive component ecosystem | npm |
| Sandboxed by default (contextIsolation) | Electron |
Keeping them separate also means the engine can ship headless someday — a CLI consumer of the same gRPC API.
IPC contracts
Two layers:
- Engine ⇄ main — gRPC, schema in
proto/engine.proto. Service:Enginewith RPCsPing,GetVersion,StreamCaptures,GetRootCA,GetBody,UpdateRules,ListRules,GetLANInfo,GetMobileConfig. - Main ⇄ renderer — Electron IPC, typed channels declared in
app/src/shared/ipc-channels.ts.
The renderer never speaks gRPC. The main process bridges the two layers, batching capture events at 30fps before pushing to the renderer so the UI applies at most one re-render per frame.
Capture model
A request through the proxy emits a chain of FlowEvent records:
STARTED → REQUEST_HEADERS → REQUEST_BODY →RESPONSE_HEADERS → RESPONSE_BODY → COMPLETEDEach event references the same flow_id. The renderer collapses them
into a single FlowSummary per flow, upserting fields as events arrive.
The engine keeps a 10k-event in-memory ring buffer + a 100MB LRU for
oversized request/response bodies. Bodies ≤ 64KiB ship inline in the
event; larger ones live in the LRU and require an explicit GetBody
fetch.
Cert + key management
On first run the engine:
- Generates an ECDSA P-256 root CA with 10-year validity.
- Persists
root.pem+root.key.pem(mode 0600) under~/Library/Application Support/ProxyPro/ca/. - Mints per-SNI leaf certificates on demand, cached for 24h in memory.
The leaf cert advertises both h2 and http/1.1 in ALPN. After the
client TLS handshake, the engine branches on the negotiated protocol and
dispatches to either http.Server (h1) or http2.Server.ServeConn (h2).
Build artifacts
| Target | Output |
|---|---|
make engine | engine/bin/proxypro-engine (universal binary, ldflags-stamped version) |
make app | app/out/{main,preload,renderer} (Vite production bundles) |
make dist | app/dist/ProxyPro.app + .dmg (ad-hoc signed via electron-builder) |
make dev | builds engine + runs pnpm dev with HMR |
make test | runs Go unit + integration tests |
pnpm --dir app test | Vitest renderer + main unit tests |
pnpm --dir app test:e2e | Playwright Electron E2E |
Further reading
- Protocol coverage — what we decrypt and what we can’t.
- Contributing — repo layout, testing conventions, plan files.
- Source on GitHub.