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-Encoding from 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 truncated flag 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 per host: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

PathPurpose
~/.laurel-proxy/data.dbSQLite database
~/.laurel-proxy/config.jsonConfiguration file (optional)
~/.laurel-proxy/ca/ca.crtRoot CA certificate
~/.laurel-proxy/ca/ca.keyRoot CA private key
~/.laurel-proxy/pidProcess 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 components
  • 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