Building Comms Nexus: a local command center for all my chats

How I turned a broken Beeper extension into a local, cyberpunk command center that unifies all my chats — the decisions, the architecture, and the bugs that taught me the API.

Sep 12, 2026
Building Comms Nexus: a local command center for all my chats
A build log of how I turned a broken Beeper extension into Comms Nexus — a local, cyberpunk "mission control" that unifies every messaging network I use, running entirely on my own machine. This is the full story: the dead ends, the decisions, and the bugs that taught me the API.

 
notion image

First, what is Beeper?

If you haven't come across it, Beeper is a universal chat app: one inbox that pulls together WhatsApp, Telegram, Signal, Instagram DMs, Messenger, Discord, Slack and a dozen or so other networks, so you can read and reply to everything from a single place instead of juggling ten apps. It was founded by Eric Migicovsky (of Pebble smartwatch fame), is built on the open Matrix protocol, and has been part of Automattic (the company behind WordPress.com) since 2024.
notion image
Beeper's pitch: one inbox for all your chats
Two things about Beeper matter for this story:
  • It's local-first. For most networks, messages connect on-device, straight from your machine to the network, rather than through someone else's cloud.
  • It has a local API. The desktop app exposes a Beeper Desktop API on your own machine (localhost:23373) that lets other software search your chats, read history and send messages, with an access token. It's meant for personal tooling and integrations, and it's the entire reason this project was possible.
  • It plays nicely with AI agents. Beeper also ships an official MCP server (the extension that kicked this whole story off), which means AI agents like Claude can plug straight in to search your chats, pull message history and even send messages, all through the same local API. Your messages become something your agents can work with, not just something you read.
notion image
On-device connections keep chats private
notion image
One account across macOS, iOS, Android, Windows and Linux
notion image
Extras like reminders, scheduling and inbox triage
Comms Nexus is essentially a custom front end I built on top of that API. With that context, here's how it came together.

Where it started: a broken extension

It began with something small and annoying. I had the official Beeper MCP extension (a .dxt file) installed in my Claude desktop app, and it refused to connect. It just sat there "retrying," throwing a cryptic error about a "discover probe" and the connection closing during version negotiation.
Instead of guessing, we cracked the .dxt open (it's really just a zip) and read what was inside: a bundled copy of mcp-remote that acts as a bridge, forwarding requests to Beeper's local API at http://localhost:23373. The extension itself was fine. So we pinged that endpoint directly and got the real answer:
{ "message": "Beeper Client API is disabled", "code": "disabled" }
The extension wasn't broken — the Beeper Desktop API was simply switched off. There's a master toggle in Settings → Integrations, and generating an access token isn't the same as enabling the API. Flipping it on, the MCP handshake immediately succeeded and the extension connected. First lesson: read the actual error before rewriting anything.

The bigger idea

With the API alive, the interesting question wasn't "how do I fix the extension" — it was "what can I build with this?"
Beeper aggregates the messaging side of ~10+ networks (WhatsApp, Telegram, Signal, Instagram DMs, Messenger, Discord, Slack, and more) and exposes a fully local API to search chats, read history, and send messages. My first instinct was to pull everything into Notion — but I caught myself. With the volume of messages I get, syncing all of it into my workspace would be pure chaos. The right model was on-demand, not a firehose.
That led to a reality check worth writing down:
The Beeper API is local-only. It runs inside the app on my machine (localhost:23373) and only works while Beeper is open. Anything cloud-hosted — Notion's hosted tools, cloud workers — physically can't reach my localhost. Whatever talks to Beeper has to run on my machine.
So "let Notion search my messages" wasn't really a thing. But "build my own local tool that does" absolutely was.

From "Jarvis" to a command center

I'd always pictured a kind of local Jarvis — one screen for every conversation across every app. We prototyped exactly that, complete with an AI console that could draft replies.
Then I made a deliberate simplification: take the AI out. I didn't want an assistant; I wanted a command center — one place to see everything and reply myself. So the AI console became a plain reply box, and the app became what I actually wanted: a unified inbox I control.
Key product decisions along the way:
  • Conversations at the center, contacts at the side — a real messaging layout, not a dashboard of stats.
  • Manual, not automated — I click send; nothing goes out on its own.
  • Read/triage/reply, with the option to route specific things into Notion later — never bulk sync.

Designing it: cyberpunk mission control

For the look, I went full cyberpunk — a dark "HUD" aesthetic that makes it feel like an ops console:
  • Neon-on-blue-black palette: cyan and magenta accents, with each network in its own colour (WhatsApp teal-green, Telegram sky-blue, Beeper violet).
  • Chakra Petch for the display type, JetBrains Mono for all the data and timestamps.
  • Clipped panel corners, a faint HUD grid, subtle scanlines, glowing status dots.
  • A three-zone layout: contacts on the left, the live conversation in the middle, a command rail (unread counts, per-network status) on the right.
It's a prototype, but it feels like a product — which matters for something you'll open every day.

Going live: the local runtime

A hosted web page can't call localhost:23373 directly, so the app is a tiny local server plus a single-file front end:
  • Node + Express server that runs on my machine and holds the Beeper access token server-side (never exposed to the browser).
  • The official @beeper/desktop-api SDK for talking to Beeper.
  • A single self-contained HTML dashboard served locally.
The server exposes a handful of thin endpoints:
Endpoint
Job
GET /api/bootstrap
connected accounts + recent chats
GET /api/messages
message history for one chat
GET /api/media
stream an image/video/avatar
POST /api/upload
upload an image/video, get an uploadID
POST /api/send
send text and/or an attachment
POST /api/read
mark a chat read
The front end opens in Sample Mode with placeholder chats if the token isn't set yet, then flips to LIVE automatically once it can reach the API — so it never looks broken.

The bugs that taught me the API

This is where most of the real learning happened. A few highlights:

1. Ordering and "read" state

My first version sorted contacts by unread count, so the loudest groups floated to the top instead of my most recent conversations. Fixed it to sort pinned first, then by last activity — like a normal messenger. I also wired opening a chat to mark it read via the API, and pulled real contact avatars and message previews from fields I'd initially mapped wrong.

2. Messages were showing raw HTML

Some messages came through as literal <strong>…</strong>
and &lt;CODE&gt; — because Beeper returns message bodies as HTML. The fix was to render a safe subset of tags (bold, italic, links, line breaks), auto-link bare URLs, and unwrap anything unknown — sanitized so nothing malicious in a message can execute.

3. Images wouldn't load

Attachments aren't always local files — many are encrypted mxc:// URLs. Trying to read them as files off disk was never going to work. Beeper has a purpose-built endpoint, GET /v1/assets/serve, that downloads and decrypts media on demand and supports range requests (so video seeking works). Proxying that fixed images, video, and stickers in one move.

4. The read-only token red herring

When image upload failed, I introspected the token and saw "scope":"read" — read-only! I was ready to blame permissions… until I realised the token I was testing was one I'd pasted into a chat earlier, not the one in the app's .env. The real token had read write all along. Lesson: verify you're testing the exact credential the app uses.

5. The bug that actually broke image sending

Even with a write token, sending an image returned:
{ "message": "Invalid input", "details": { "issues": [{ "message": "Either text or attachment is required." }] } }
Confusing — I was sending an attachment. The catch: I was using the plural field, which the API silently ignores:
// ❌ ignored — send rejected as "text or attachment required" { attachment**s**: [{ uploadID }] } // ✅ correct — singular object { attachment: { uploadID } }
I verified the fix safely by sending a test image to my own Telegram Saved Messages (a note to self), confirming it arrived as a real image, then deleting the test. One character — attachments vs attachment — was the whole bug.

Finishing touches

Once it worked end to end, the polish is what made it feel real:
  • A "Sending…" state — a button spinner and an animated progress bar on the attachment, so a big photo never feels frozen.
  • Paste & drag-and-drop — copy an image and Ctrl/Cmd+V anywhere, or drop a file onto the conversation, and it becomes an attachment ready to send.
  • A double-click launcher — a .bat and a no-terminal .vbs, so I never have to touch a command line; they auto-install dependencies and open the dashboard.
  • A custom app icon — a .ico of a glowing network hub linking the three network nodes, for a real desktop shortcut.

What I learned

  • Read the error, not your assumptions. The "broken" extension was a disabled toggle; the "permissions" bug was the wrong token; the "invalid input" was one letter.
  • Local-first has hard edges. The best feature (a private, on-device hub) is also the constraint (nothing in the cloud can reach it). Design around it early.
  • Match the tool to the job. I didn't need AI or a Notion sync — I needed one screen I control. Cutting scope made it better.
  • APIs hide their real shape in their errors and their field names. Half of building this was learning Beeper's schema by poking at it.

What's next

A few ideas on the list: loading older history on scroll, sending a caption together with an image, optional per-channel "save this thread to Notion," and maybe a keyword digest across networks. All manual, all local, all on my terms.

Stack, for the curious

  • Runtime: Node + Express (local), @beeper/desktop-api SDK
  • Data: Beeper Desktop API (localhost:23373), local-only
  • Front end: single self-contained HTML/CSS/JS dashboard
  • Type: Chakra Petch + JetBrains Mono · Look: cyberpunk HUD
  • Launch: .bat / .vbs double-click, custom .ico
Built iteratively, one bug at a time — which is the fun part.

🔄
This is a living document. Comms Nexus is still evolving, and I'll keep updating this post as new versions ship, from smarter history loading to tighter Notion integration. Check back for the next iteration.
 
notion image