It started with the dumbest problem in computing: I'm in bed, a movie is
playing on my laptop across the room, and pausing it requires physically
getting up
.

Every existing fix annoyed me in some way. Remote desktop apps are overkill
and route through someone's cloud. Remote-control apps want accounts,
subscriptions, or a native app install. I just wanted my phone to poke my
laptop over my own Wi-Fi.

So I built LapDeck: one Node.js
process on the laptop, a PWA on the phone. Scan a QR code once and your
phone becomes an app launcher, touchpad, keyboard, live screen viewer, and
media/power remote. MIT licensed, plain JavaScript, no build step.

This post is about the four problems that turned out to be more interesting
than I expected.

The architecture in one line

Phone (PWA) ── WebSocket + MJPEG over Wi-Fi ──► Node.js agent (Windows)

The agent serves the PWA, exposes a WebSocket for commands, and streams the
screen as MJPEG. The protocol is deliberately dumb JSON envelopes — no
protobuf, no RPC framework — so a native Android client or a CLI script can
speak it in an afternoon. Windows-specific glue (volume, brightness, power,
capture) is isolated in src/win/, so a macOS/Linux port only has to
reimplement that layer.

Problem 1: mobile keyboards lie to you

The obvious way to build a remote keyboard is to listen for keydown and
forward key codes. On mobile, this collapses immediately: swipe typing,
autocorrect, and IME composition don't emit per-key events. Android will
happily tell you every key is keyCode 229.

The fix: stop listening to keys entirely. I keep a hidden input, and on
every input event I diff the field's value against the last known
state
— compute the common prefix, then emit "delete N chars, type this
string" to the laptop. Swipe-type a whole word and the laptop receives one
clean text insertion. IME composition, autocorrect rewrites, emoji — all
just become diffs.

The lesson generalizes: on mobile web, treat the text field as the source of
truth, never the key events.

Problem 2: streaming a screen without WebRTC

I wanted live screen view with tap-to-click. WebRTC would be the "correct"
answer, but it drags in a signaling dance and a pile of dependencies for
what is fundamentally "pictures, quickly."

MJPEG turned out to be embarrassingly good for this. The agent runs a
capture loop (screenshots via nut.js, resize + JPEG encode via sharp) and
pushes frames as multipart/x-mixed-replace — a <img> tag on the phone
renders it natively, zero client code. Quality presets just change fps,
width, and JPEG quality.

Latency is fine for "tap where you want to click," but a raw MJPEG stream
makes the cursor feel laggy. So the phone renders its own client-predicted
cursor overlay on top of the stream — your finger drives a local cursor
instantly, and the real cursor catches up underneath. The perceived latency
of the whole feature is the latency of that overlay, not the stream.

Problem 3: the security model for "my phone can shut down my laptop"

Giving a web page on the network the power to inject input and kill your
machine deserves paranoia. The rules I settled on:

  • A 256-bit random token, generated on first run, embedded in the QR link. You scan once; the phone stores it. Every WebSocket command and the MJPEG stream require it. Comparison is constant-time.
  • First WebSocket message must authenticate within 3 seconds or the socket drops.
  • Feature switches are enforced server-side. If you disable screen view in settings, the agent refuses those commands — it doesn't just hide the button.
  • Destructive actions are opt-in and abortable. Sleep/shutdown/restart each need to be enabled in settings, require an explicit confirm flag, and shutdown/restart honor a grace period during which one tap aborts. (I have fat-fingered shutdown. The abort window has paid for itself.)

Transport is plain HTTP/WS, which is fine for a home LAN you trust — and
explicitly not fine for the open internet. Which leads to:

Problem 4: remote access without running a server

"Control your laptop from anywhere" usually means either port forwarding
(no) or somebody's relay cloud (also no). The answer that costs nothing and
required surprisingly little code: detect Tailscale.

If the agent sees a Tailscale interface, it prints a second "remote" QR and
the UI offers a one-tap switch to the tailnet URL. WireGuard gives you
end-to-end encryption, MagicDNS + tailscale cert gives you a real HTTPS
URL (which also makes the PWA installable from anywhere), and I run zero
infrastructure. Any VPN that puts your phone on your home network works —
Tailscale is just the zero-config path.

Things I deliberately didn't do

  • No TypeScript, no build step. Plain ESM on Node ≥ 20. Clone, install, run. Contributors can read every line that executes.
  • No Electron, no tray app. It's a console process; a PowerShell script installs a hidden autostart launcher into the Startup folder if you want it permanent (no admin needed).
  • No cloud component, period. There is nothing to sign up for, nothing that phones home, and the whole thing works with Wi-Fi and no internet.

Try it

Windows 10/11, Node ≥ 20, phone on the same Wi-Fi:

git clone https://github.com/ronak-create/LapDeck.git
cd LapDeck
npm install
npm start

Enter fullscreen mode Exit fullscreen mode

Scan the QR, add to home screen, done.

Repo: https://github.com/ronak-create/LapDeck — the WebSocket protocol
is documented in docs/PROTOCOL.md if you want to build your own client.
macOS/Linux ports, multi-monitor support, and code roasts all welcome.

What's the laziest problem you've ever over-engineered a solution for?