Skip to content

Architecture

This document describes peekr's internal architecture, module responsibilities, data flows, and key design decisions.

Directory Structure

bin/
  peekr.mjs              CLI entry point, subcommand dispatch (run/ui/logs/proxy)

lib/
  args.mjs               getArg, getAllArgs, hasFlag, listenOnAvailablePort helpers
  child-runner.mjs       Spawns child process with stdio piped, logs to .peekr/app.log
                          + in-memory buffer (1000 lines) + onLogEntry callback for SSE
  config.mjs             Loads optional JSON port config and resolves CLI/config/default precedence
  intercept-template.mjs Generates loader code that monkey-patches node:http/node:https,
                          injected via NODE_OPTIONS=--import or --require
  logger.mjs             Colored console logging + setLogFile
  logs-command.mjs       peekr logs implementation (tail .peekr/app.log)
  proxy-core.mjs         Outgoing HTTP proxy server. Receives intercepted requests, logs them,
                          optionally forwards to upstream. Integrates rules engine.
  reverse-proxy.mjs      Reverse proxy for incoming traffic. Captures IN requests with timing
                          data (durationMs, responseSize, resHeaders).
  rules-engine.mjs       In-memory rules store. addRule/removeRule/getRules/findMatch.
  run-command.mjs        peekr run implementation. Sets up proxy + child runner.
  ui-command.mjs         peekr ui implementation. Wires proxy + reverse proxy + ui-server + rules.
  ui-server.mjs          Serves ui/index.html, SSE endpoint with named events, REST API for rules.

ui/
  index.html             Single-file dashboard (~970 lines). Dark theme, CSS Grid, real-time SSE.

Module Dependency Graph

bin/peekr.mjs
  ├── lib/run-command.mjs
  │     ├── lib/proxy-core.mjs
  │     │     └── lib/rules-engine.mjs
  │     ├── lib/child-runner.mjs
  │     │     └── lib/logger.mjs
  │     ├── lib/intercept-template.mjs
  │     └── lib/args.mjs
  ├── lib/ui-command.mjs
  │     ├── lib/proxy-core.mjs
  │     ├── lib/reverse-proxy.mjs
  │     ├── lib/child-runner.mjs
  │     ├── lib/ui-server.mjs
  │     │     └── lib/rules-engine.mjs
  │     ├── lib/intercept-template.mjs
  │     └── lib/args.mjs
  ├── lib/logs-command.mjs
  └── lib/args.mjs

Data Flow

Proxy Mode (peekr --target <host>)

  Your App
    │  HTTP request (manually configured to use proxy)
  peekr proxy (localhost:49999)
    ├── Rules engine check
    │     ├── block  → 403 Forbidden
    │     └── mock   → custom response
    │  (if no rule matches)
  Upstream HTTPS server

The proxy server (proxy-core.mjs) listens on a local port and receives HTTP requests. Each request is logged and checked against the rules engine. If no rule matches, the request is forwarded to the upstream target.

Run Mode (peekr run -- <command>)

  peekr run -- node app.js
    ├── Writes ESM or CJS loader to /tmp/peekr-loader
    ├── Sets NODE_OPTIONS=--import or --require /tmp/peekr-loader
    ├── Starts proxy server (localhost:49999)
    └── Spawns child process
          │  Child's http/https modules are monkey-patched
          │  All outgoing requests route through peekr proxy
        peekr proxy (localhost:49999)
        Upstream server

The intercept template (intercept-template.mjs) generates an ESM loader for Node 18.19+ / 20+, and a CJS loader for older Node 18 releases where --import is rejected in NODE_OPTIONS. The loader patches node:http and node:https request methods. The child process's outgoing traffic is transparently routed through the proxy without any code changes in the target application.

Child process stdout/stderr are piped to: - .peekr/app.log (file) - In-memory circular buffer (1000 lines) - SSE broadcast via onLogEntry callback

UI Mode (peekr ui)

  External Client
  Reverse Proxy (:49998)  ──captures IN requests──►  Dashboard (:49997)
    │                                                      ▲
    ▼                                                      │
  Your App (:3000)                                    SSE events
    │                                                 (request, app-log,
    │  outgoing HTTP                                   rules-change)
  Proxy (:49999)  ──captures OUT requests──────────────────┘
  Upstream server

UI mode combines all components: - Reverse proxy (:49998) sits in front of the user's app, capturing incoming requests with timing data - Outgoing proxy (:49999) captures all outgoing HTTP/HTTPS from the child process - Dashboard (:49997) serves the single-file HTML UI and SSE endpoint

Both incoming and outgoing requests are visible in the dashboard in real time.

SSE Protocol

The UI server (ui-server.mjs) exposes an SSE endpoint that streams named events to the dashboard.

Event Types

Event Payload Description
request Request object (JSON) New captured HTTP request (incoming or outgoing)
app-log Log line (JSON string) Child process stdout/stderr line
rules-change Rules array (JSON) Rules list updated (add/remove)

Connection Behavior

On client connect, the server replays: 1. All buffered requests (from the in-memory circular buffer) 2. Current rules state

This ensures the dashboard is fully populated even if the client connects after traffic has already been captured.

REST API

The UI server also exposes a REST API for rules management:

Method Path Description
GET /api/rules List all rules
POST /api/rules Add a new rule
DELETE /api/rules Remove a rule

Rule changes trigger an SSE rules-change broadcast to all connected clients.

Rules Engine

Rules are stored in memory and matched against incoming requests.

Rule structure:

{
  "id": "unique-id",
  "host": "example.com",
  "method": "GET",
  "path": "/api/users",
  "action": "block | mock",
  "mockConfig": { "status": 200, "body": "..." }
}

Matching priority: host (exact) → method (wildcard supported) → path (prefix match). First match wins.

Actions: - block — responds with 403 Forbidden - mock — responds with custom status, headers, and body from mockConfig

Design Decisions

Zero Dependencies

peekr uses only Node.js built-in modules (node:http, node:https, node:fs, node:child_process, node:net, etc.). This eliminates supply chain risk, keeps install instant, and avoids version conflicts.

ESM Only

The project uses ES modules exclusively ("type": "module" in package.json). Runtime interception still generates a temporary CJS loader as a compatibility fallback for Node 18 releases before 18.19, because those versions reject --import in NODE_OPTIONS.

Single HTML Dashboard

The entire dashboard UI is a single index.html file with inline CSS and JavaScript — no build step, no bundler, no framework. This keeps the development workflow simple and eliminates frontend tooling complexity.

In-Memory State

All captured requests, logs, and rules live in memory. Nothing persists across restarts. This is intentional — peekr is a development tool, not a monitoring system. Simplicity over durability.

Monkey-Patching via Loader

The --import or --require flag injects a temporary loader that patches http.request and https.request at the module level. A double-patch guard prevents re-patching if the loader is imported multiple times.

Port Configuration

The default ports are 49999 for the outgoing proxy, 49998 for the reverse proxy, 49997 for the dashboard, and 3000 for the app target. config.mjs resolves values in this order: CLI flag, JSON config file, default. Config files are discovered as peekr.config.json or .peekrrc.json, or explicitly passed with --config <path>.

Loop Detection

The proxy core detects request loops (proxy calling itself) and responds with HTTP 508 (Loop Detected) to prevent infinite recursion.

ANSI Stripping

Log lines from child processes may contain ANSI escape codes. These are stripped for log-level filtering and clean display in the dashboard.