Data Salon·← everything behind the talk
Field notes · building the platform

Building a Home Control Plane From Parts

I set out to build a message relay so I could work away from my desk. By solving one small problem at a time, it turned into the thing that runs my whole home lab off a salvaged ThinkPad.

When I start a new project, my first goal is to define the tight bounds needed to support an MVP. I want to know one thing: what is the minimal version of this system that has to exist for me to have accomplished my task, and in order to do that, I also have to clearly define what that task actually is.

In this case the task was simple to state. I wanted to detach a work session from my physical location and device. I wanted to be able to work from any location, on any device, with the actual work — my interaction, the planning, the execution — decoupled from the machine in front of me. I wanted to be able to get up, close my laptop lid, walk away, and keep going from my phone through a secure chat interface.

The options that already existed

There are pre-existing solutions for this, and each one solves a slightly different problem than mine.

Claude Code has a Remote Control feature that lets you start a session on one device and pick it up on another. That meets the bare minimum of what I wanted. But it added a requirement I didn't ask for: it ties me to a specific app. I don't want to have to use one particular client just to work from any device.

OpenClaw exists and is popular, and honestly it set the standard for the kind of interaction I was after. I chose not to use it, partly for personal reasons; I don't like not knowing how it works under the hood, and the way it prompts never quite sat right with me. But the real technical setback was that it would likely push me toward an API key, or risk getting my account flagged by Anthropic for running a non-standard harness. Mostly, though, I wanted to build this myself and own the whole pipeline.

So I sat down and mentally modeled what the system would look like.

Chat Client → Backend Server → Message Relay → Claude Code backend

My design philosophy is to decompose a system into separate components, each with its own area of concern. That lets me build interfaces between the pieces, which makes the whole thing extensible, which makes it easier to maintain and update. In my opinion that's the first foundational step toward a system that can actually scale. With a mission objective and a set of hard requirements defined, I started a discussion with Claude about what this was and how we could make it work.

The first edition: a Telegram bot

The first version was going to be a simple Telegram bot. That choice came from a good previous experience, I'd once used a Telegram bot to track the status of a hacked Instagram account, and it was phenomenal, very straightforward. So it became the hammer, and this project was the nail.

A research pass on how to actually implement this turned up a beta component in Claude Code I hadn't known about: channels. This was perfect. It let me pass a message into a running Claude session, trigger a response, and keep a long-running session alive. No need to build my own strange harness or fire off individual calls while babysitting my own context window. I could genuinely use a running instance of Claude Code to manage its own session. All I had to do was introduce it to the relay and give it proper instructions on how to use it.

Using that as the instruction set, we built out the whole relay quickly. We called it relayd. It worked exactly the way I wanted.

Security, and an accident

I almost immediately hit a security problem baked into my own narrow spec. I needed a form of authentication. This first version used Telegram, but there's no way to pass credentials downstream to validate a user, so I couldn't trust anything from Telegram's side to prove who someone was. What Telegram does send is a unique ID. So the first real challenge was to build the best authentication I could out of that: block every incoming message at the relay so nothing reaches the model backend before it's approved or denied, and split users into an "admin" and a regular "user." The moment I drew that distinction, I realized this thing could be multi-tenanted.

I reached out to a few friends and had them try to connect, to validate the approval flow and shake out the holes in practice. And out of nowhere, I suddenly had multiple people using my Claude Code session at the same time. It became this odd collaborative space. What surprised me most is how flawlessly Claude handles multiple users. It knows who is talking to it and replies back through the correct channel to that person. To this day I have several people who hold discussions and learning sessions with the model all at once. It treats my account as the admin, and it will actually narc on a user who tries to fish for sensitive information.

An honest warning

This is not safe. The people I share this space with are close friends I've known and trusted for years. The model has proven very good at knowing which bounds not to cross, but relying on a model for security like this will eventually cause problems. Every model, given enough time, is susceptible to social engineering. I would not suggest anyone run a Claude instance like this without understanding the risks. It is extremely important to gate a stochastic system behind a deterministic boundary.

Giving it a heartbeat

The dynamic had shifted. I was now passing messages back and forth through this thing with several people, and I realized I was missing something: a way for the model to have some level of self-determinism, a pseudo-agency. OpenClaw has a concept of a "heartbeat" that keeps an agent running toward its own goals. I don't know exactly how theirs works, but I assume it's something like a scheduler that pings the model, hands its context back to it, and lets it keep working. Not so different from how dreaming works; you let a system run off and iterate on its own thinking.

So I had it build its own scheduler. Now I can create events, schedule tasks to run at specific times, or set one-off tasks to fire after some elapsed time. I also gave the model the freedom to schedule itself, if it decides a task should be handled later in the day, it can set a wake-up and come back to it. That added another dimension again, pushing it much closer to a true assistant than any interaction I've had with one of these machines. It's funny how one small addition can change the whole relationship you have with something.

Captain of its own ship

I made a deliberate choice about where this would live. I have a fairly large collection of ancient ThinkPads that I stress-bought and repaired off eBay over a few years. They make perfect home-lab boxes: reasonably powerful, capable of running real services, more capable than a Raspberry Pi at around the same cost — if you know how to wipe an admin password out of a ThinkPad's BIOS. So I installed a bare-bones Linux distro and let the model take full control of the device. Strip out any software it doesn't need, minimal UI, SSH ingress only through Tailscale, a VPN built on WireGuard, so I can block all traffic that isn't over the LAN or through the tailnet, which becomes a single controlled egress point.

Letting the model act as the captain of its own ship also lets it use every sense it has as a resource. It can scan local traffic, reach out to my router, connect in and do maintenance, stand up ad-hoc ad blockers on the fly. That realization led to the next one: I needed an observability stack. I wanted to capture metrics, logs, and traces from my own servers, from the machine it runs on, and from the devices and signals around me over Bluetooth and Wi-Fi.

So we set up a metrics service (Prometheus), a log service (Loki), and a place to turn that data into useful dashboards and alerts (Grafana).

A nervous system

This was another big change. Now the data lived in one place and was visible. It also meant we could reach out to public APIs for public data, like weather, and track it on dashboards. And once I realized we could route Grafana's alerts back through the relay, it clicked that these alerts were a kind of nervous system — a way for the model to be aware of its own vessel. It can tell when it's running heavy on CPU, low on memory, or short on disk. An alert fires, hits the model, and the model investigates and reaches out to me. It can start to self-heal.

A fun example: using the Bluetooth radio to capture broadcast packets from a home thermometer, I can read my house's temperature, compare it against the outside temperature, and get an alert that tells me when to open my windows.

This had gone from "a simple message relay so I can work remotely" to a central control point for a smart home. With the ability to capture packets from the router, inspect traffic, and port-scan local devices, I found an open port on my TV. I used it to build a frontend remote control, so now I can operate the TV from my phone when I lose the remote.

What comes next

If I get smart lights that expose an open API, they can run from this same command-and-control center, with custom interfaces built exactly how I want them. Further out, the plan is to connect other machines into a distributed Kubernetes cluster and let the model talk directly to the control plane. There's already a local cluster running most of my services, including a personal Mastodon instance I've barely touched yet.

None of this was designed up front. I built the pieces first, each with a clean boundary, and the platform emerged from putting them next to each other. That's the whole point I wanted to make: complex systems that work tend to grow from simple systems that work.

The services, so far