Architecture
Laurel Proxy is a single Node.js process that runs the intercepting proxy, REST API, web UI, and background cleanup -- all on two ports.
Key Design Decisions
- SQLite with WAL mode -- high write throughput with concurrent reads. WAL (Write-Ahead Logging) allows the proxy to write captured requests while the API serves queries simultaneously without locking.
- Batched writes -- requests are queued in memory and flushed every 100ms to reduce I/O. This avoids a disk write per request during heavy traffic.
- Event batching -- SSE events are buffered for 100ms before flushing to connected clients. This prevents overwhelming the web UI during bursts of traffic.
- Uncompressed capture -- the proxy strips
Accept-Encodingfrom outbound requests, so upstream servers return plaintext and stored bodies are always readable without a decompression step. - Body truncation -- configurable max body size prevents storage bloat. A
truncatedflag is set on affected records so you know when data was clipped. - Per-domain cert caching -- an LRU cache avoids regenerating SSL certificates for frequently accessed domains. Default capacity is 500 entries.
- Independent HTTP/2 negotiation -- the MITM TLS socket offers ALPN
['h2', 'http/1.1']to the client, and the origin is probed separately over its own ALPN handshake, cached perhost:port. Nothing about one hop's protocol influences the other. See HTTP/2 Support. - Shared-pipe throttling -- bandwidth throttling models one shared virtual link per direction rather than one per connection, so concurrent requests contend for the same rate budget the way they would on a real network link.
- Decoupled WebSocket recording -- frame decoding is separate from the relay, so a decoding problem (a malformed frame, an unexpected compression bit) can never break the live connection. The trade-off is that a decode failure silently stops recording for that direction rather than surfacing an error.
Data Storage
All data is stored in ~/.laurel-proxy/data.db (SQLite). The requests table has indexes on timestamp, host, status, path, and content_type for fast querying. A WebSocket connection's 101 handshake is recorded as an ordinary row with kind: 'websocket'; every frame after that is decoded (RFC 6455) and written to a separate websocket_messages table keyed by the handshake's request id.
Request and response bodies are stored as binary blobs. In the API and SSE stream, they are base64-encoded.
Every row also carries client_protocol and origin_protocol(http/1.1 | h2 | null), recording the wire protocol negotiated on each hop independently of the URL's http/httpsscheme. Databases created before this field existed get both columns added on first open after upgrading, guarded by a schema check so it's a no-op on every run after the first, and existing rows are backfilled to http/1.1 for both hops rather than left null -- every exchange recorded before HTTP/2 support existed genuinely spoke HTTP/1.1 on both sides.
File Paths
| Path | Purpose |
|---|---|
~/.laurel-proxy/data.db | SQLite database |
~/.laurel-proxy/config.json | Configuration file (optional) |
~/.laurel-proxy/ca/ca.crt | Root CA certificate |
~/.laurel-proxy/ca/ca.key | Root CA private key |
~/.laurel-proxy/pid | Process ID file |
Project Structure
src/
├── cli/ # CLI entry point, commands, interactive mode
│ ├── index.ts # Command registration (Commander.js)
│ ├── interactive.tsx # Interactive terminal menu (Ink/React)
│ ├── tail-ui.tsx # Real-time tail TUI (Ink/React)
│ ├── format.ts # Table/JSON output formatting
│ ├── commands/ # Individual CLI commands
│ └── system-proxy.ts # macOS system proxy & CA management
├── server/ # Proxy server, API, SSL, events
│ ├── index.ts # LaurelProxyServer orchestrator
│ ├── proxy.ts # HTTP/HTTPS intercepting proxy
│ ├── api.ts # Express REST API + SSE
│ ├── ssl.ts # CA generation & per-domain cert caching
│ ├── events.ts # Pub/sub event manager
│ ├── config.ts # Config loading and merging
│ ├── throttle.ts # Bandwidth throttle presets & shared-link rate limiter
│ └── ws-frames.ts # WebSocket frame decoding (RFC 6455)
├── storage/ # Database and cleanup
│ ├── db.ts # SQLite operations (better-sqlite3)
│ └── cleanup.ts # Auto-cleanup job
├── shared/ # Shared TypeScript types
│ └── types.ts # Config, RequestRecord, RequestFilter
└── ui/ # React web UI (Vite)
├── App.tsx # Main app component
├── api.ts # API client + SSE hook
└── components/ # UI componentsRelated
- REST API - Query captured traffic programmatically
- Configuration - Config file, storage limits, and auto-cleanup
- CLI Reference - All commands, flags, and output formats
- HTTP/2 Support - The client_protocol/origin_protocol fields and their migration