index · all work
09 · dev tool

Localhost Mirror

Like ngrok, except the tunnel never leaves your tailnet.

Year2026
Statusshipped
RoleSolo · design + engineering
Built withTypeScript 5.8 · Node.js · Express 5 · http-proxy · Commander · +3
localhost mirror · live tunnelsopen ↗

Live demo. Click Run to load the real app and use it right here, or open it full-screen.

01Overview

Localhost Mirror exposes a port running on your machine to your other devices, and only your other devices. It rides on Tailscale, so a dev server on your laptop becomes reachable from your phone, tablet, or another Mac, but never from the public internet. Run `lm expose 3000` and you get a private URL the rest of your tailnet can open.

It ships as two halves that share one background daemon: a terminal CLI for people who live in the shell, and a native macOS menu-bar app that scans every port your machine is listening on, recognizes what is running there (Next.js, Vite, Postgres, Ollama, and dozens more), names the owning project, and exposes any of them from the menu bar. A single-file dashboard, optional per-tunnel tokens, health checks, and a daemon that puts itself to sleep round it out.

02The problem

Sharing a local dev server usually means a public tunnel that hands a URL to the entire internet and often wants an account. Localhost Mirror keeps the one-command convenience but scopes access to devices you already own, so the QR-code-on-your-phone workflow never exposes a half-built app to strangers.

03Highlights
  • Tailnet-only by construction: every HTTP request and WebSocket upgrade is CIDR-checked against Tailscale's 100.64.0.0/10 range plus loopback before it reaches a local port, so there is no public listener to discover.
  • One daemon multiplexes every tunnel on a single port via an lm_tunnel cookie; hitting /<name> or ?tunnel=<port> sets it and proxies you to the right localhost port, WebSockets included.
  • Native SwiftUI menu-bar app scans listeners with lsof and labels them from 87 known dev ports plus 28 process-name heuristics, then walks up the filesystem for package.json / Cargo.toml / go.mod / .git to name the owning project.
  • Optional per-tunnel token auth: `lm expose 3000 --token` mints a nanoid that the tunnel URL then requires as ?token=; without it the daemon returns 401.
  • Live health and resource monitoring: HEAD-pings each target every 10s to flip active/target-down, and the menu bar raises alerts for CPU over 80%, RAM over 500MB, or zombie processes.
  • Self-managing daemon: auto-starts on the first expose, persists atomic JSON state (temp-file then rename) to ~/.localhost-mirror, and shuts itself down 60s after the last tunnel closes unless run with --persistent.
04By the numbers
87
dev ports auto-identified
28
runtime process heuristics
10s health checks, 60s idle auto-shutdown
~2,300
LOC across CLI, daemon, and macOS app
What's inside7 parts
  • lmcli

    Terminal CLI (the `lm` bin) whose expose, list, stop, status, dashboard, and shutdown commands drive the daemon over its localhost management API on 127.0.0.1:19099.

  • LocalhostMirrorapp

    SwiftUI macOS menu-bar app (a MenuBarExtra, LSUIElement) that lists every listening port and lets you expose, stop, copy the tailnet URL, or kill any of them, polling the daemon every three seconds.

  • daemonservice

    Background service running a localhost-only management API on :19099 plus a tailnet-facing proxy and dashboard server on :19100, auto-starting on the first expose and shutting itself down 60 seconds after the last tunnel closes unless run with --persistent.

  • apilibrary

    Request router that maps an lm_tunnel cookie or a /name path to a per-port http-proxy instance (one cached per target port), forwards WebSocket upgrades, gates optional per-tunnel nanoid tokens, and serves the tunnel CRUD REST API.

  • PortScannerlibrary

    Swift scan engine that lists TCP listeners with lsof every three seconds, reads per-process CPU, RAM, and zombie state from ps, finds the owning project by walking up for package.json, Cargo.toml, go.mod, or .git, and raises alerts for CPU over 80 percent, RAM over 500MB, or a process that died.

  • CommonPortslibrary

    Lookup table that labels and colors a detected port from 87 known dev ports (Next.js, Vite, Postgres, Redis, Ollama, and more) plus 28 process-name heuristics.

  • dashboardapp

    Single-file dark web dashboard (index.html served at /lm/) that the daemon serves over the tailnet to list, expose, and stop tunnels from the browser.

·Tags
dev toolnetworkingTailscalereverse proxymacOS appCLI
Full tech stack 8
TypeScript 5.8Node.jsExpress 5http-proxyCommanderSwift 5.9SwiftUITailscale