Mowa: Letting My Cluster Talk to macOS
In "The Birth of My Homelab" I mentioned a new project called Mowa, short for MacOS Web API, and said more would come in a follow-up post. That was August 2025. This is the follow-up.
polaris, my Mac Mini, is part of the homelab but it doesn't run Kubernetes. What it has that nothing else in the cluster does is iMessage, Reminders, and the rest of macOS's iCloud integration, and none of that exposes a remote API a Linux node can call. mowa is the Go service I wrote to close that gap: an HTTP server that runs on polaris and turns those macOS features into endpoints anything else on the LAN can call.
The first commit is dated August 3, 2025, the same Hackweek that got the cluster's first node running. The plan I described at the time was to eventually add a Raspberry Pi 4 "to run arm64 workloads and connect to physical peripherals like a doorbell and a camera." That Pi needed a way to reach my phone, and macOS was the only machine in the house that already knew how.
What it does today
- Sends iMessages through the Messages app, to individual numbers or to groups defined in mowa's own config.
- Manages Apple Reminders: create, list, edit, complete, and delete, across lists (PR #15).
- Saves and retrieves arbitrary YAML files under a configured storage directory.
- Installs itself as a
launchduser agent (mowa install) so it starts at login and stays running (PR #14). - Checks daily for a macOS update that needs a restart, and messages me when one is waiting, since an unattended update reboots into Setup Assistant and silently de-registers iMessage until someone signs back in (PR #18).
It's a single Go binary built on the Echo framework, with Swagger docs embedded in it.
Where it's actually used
thuroros, the doorbell Pi, is the original reason mowa exists. It runs Kairos and watches a GPIO pin for a physical button press; when it fires, it POSTs to mowa's /api/messages endpoint, and mowa relays it through Messages.app to my phone over iMessage. That path runs alongside a second, independent one straight to Home Assistant over MQTT, so a stuck AppleScript call can't take down the Home Assistant notification and vice versa.
The other consumer showed up almost a year later, in the "Building a Software Factory" series: a due date on an internal board ticket now becomes an entry in a shared Apple Reminders list, synced through mowa, so the notification comes from the OS instead of from a process I have to keep alive myself.
How it's reachable
polaris advertises itself over mDNS, so anything on the LAN reaches mowa at polaris.local:8080 without a static IP. mowa listens on all interfaces by default, not just localhost, because that's the point: thuroros and everything else on the network need to reach it. There's an optional api_key to gate /api/* requests, but it's off by default, and mowa starts anyway and just logs a warning when it is, so an existing install doesn't break on upgrade.
The README lists volume control and app launching as places to extend it next. Neither exists yet.