Home Assistant is an extraordinary piece of software with an interface built for configuring a house rather than living in one. Those are different jobs. Configuring means every entity is reachable and equally important. Living in it means five things matter at nine at night — the lights, a scene, the music, the thermostat, is the door locked — and they should be one thumb away.
My house has 1,625 entities. The daily-driver subset is maybe five percent of that, and it was buried under the other ninety-five.
So I built a control surface. It was good. I stopped building it anyway, and that decision is the actual subject of this post.
What got built
home. was an adaptive PWA in front of Home Assistant — the eighth thing in my family app fleet. Home Assistant kept owning entities and automations. This was purely the experience layer.
The idea I still like: the chrome stays fixed and the content reorders by phase of day. Morning, day, evening, night, late. Not on a clock — derived from the sun’s elevation, which Home Assistant already tracks. Above twenty degrees is day. Between zero and twenty is morning or evening depending on which side you’re on. Below the horizon is night, and well below is late. In the evening, music and scenes float up and the thermostat sinks. At night the house shows you the locks first. Nothing hides; things reorder.
Then, over one long run of eleven changes, I hardened it into something that felt native rather than like a webpage in a shell:
Optimistic UI everywhere. Tap a light, it responds instantly, then the command either confirms or rolls back with a toast on a five-second timeout. Double-tap guarded. The physical reality is that a lamp takes a few hundred milliseconds to answer over a WebSocket, and an interface that waits for it feels broken even when it’s working perfectly.
Connection resilience as the critical path, because a home control app that’s a spinner during a hiccup is worse than a light switch. Jittered reconnect backoff. A sixty-second ping/pong watchdog for a socket that’s open but dead — the nastiest failure state, because everything looks fine and nothing is. A fifteen-second timeout on the connect phase itself. Connection metrics and an event ring buffer behind a debug view. And a full state rehydration on every reconnect, because the house changed while you were gone.
Error screens that tell you which layer is broken. A health probe classifies whether you’re offline, whether the network path is the problem, whether the proxy is down, or whether Home Assistant itself has gone away — then shows a real error screen with a retry countdown instead of an empty page. Four different sentences for four different problems.
Deploy safety with a drilled rollback. Builds stamp a version file, the deploy health-checks itself and automatically rolls back to the previous build on failure. Critically: I triggered a real rollback before trusting it. An untested recovery path is a plan, not a capability.
148 unit tests plus Playwright end-to-end against a mock Home Assistant WebSocket server — scripted through outage, reconnecting, restore, rehydrated, interactive, plus the error screen healing itself. Hand-rolled state stores specifically so they’d be unit-testable.
Lighthouse 97/96/100/100. 52KB gzipped. All green.
The adversarial review
Before trusting any of that, I pointed a reviewer with no stake in the outcome at the app, with one instruction: prove this is not production-ready.
No criticals. Four HIGHs, all real, all fixed the same day:
- A stale pending-call map cross-wired promises across reconnects. Commands issued before a drop could have their responses matched to different commands issued after it. Your kitchen light acknowledges, and what actually answered was a scene call from ninety seconds ago.
- An armed backoff timer tore down healthy sockets after a manual retry. You hit retry, it worked, and then the previously-scheduled reconnect fired and killed the good connection. The recovery mechanism was breaking the recovery.
- The rollback path aborted in exactly the mid-swap-death state it exists to handle. The safety net worked in every scenario except the one it was built for.
- 46 leaked store subscriptions, which is the kind of thing that never shows up in a test and slowly eats a long-lived tab.
And the one that still makes me laugh: the PWA icons referenced in the manifest had never existed. Anyone installing the app to their home screen was being served the HTML page as an icon. It had been that way since the beginning, on an app I opened daily, because you never look at your own icon — you just tap where it lives.
That review is the strongest argument I have for adversarial passes. Every one of those four bugs lives in the space between “the tests pass” and “this survives a bad Tuesday.”
The question that ended it
Then came a full-fleet interface sweep, and with it a simple product question: should home. become a complete replacement for the Home Assistant dashboard?
Answering it honestly required auditing what was actually there. Which turned up two things.
First, the house was a mess underneath — 228 devices, 1,625 entities, and only 32 devices assigned to areas. Zero floors. Zero labels. No genuinely curated native dashboard, because I’d never made one. I was comparing my polished app against a default I had never bothered to configure. (The full extent of that rot got its own audit later, and it was worse than this.)
Second, and fatally: home. was a good standalone app that duplicated Home Assistant’s authentication, its WebSocket state, its entity discovery, its service calls, and its native feature coverage. Five systems, re-implemented, worse, forever.
The first approved direction was a compromise — a read-only panel embedded inside Home Assistant, so my interface could live in its shell and inherit its plumbing. I built it. Tested it. Committed it. Briefly deployed it.
Then I actually looked at the native dashboard, properly, for the first time in a year. And the direction changed:
Optimize native Home Assistant. Do not maintain a second implementation of the house.
What actually died
Precision matters here, because “I deleted my app” would be a better story and it isn’t what happened.
home. is still online. The source is intact. The embedded panel’s registration was removed after the native redesign shipped, with its source and rollback preserved. The app remains as a fallback.
What died was the ambition — the plan for it to become the house’s primary interface, and the maintenance burden that plan implied stretching out in front of me forever.
In its place I rebuilt the native dashboard properly: five views instead of a wall of entities, settings demoted out of the primary tabs, cameras folded into security. The security surface uses Home Assistant’s own lock, cover, and alarm controls rather than reimplementing those domains — which is the thesis in one decision. I am not going to write a better confirm-before-you-unlock flow than the people who work on that full time, and my version would be the one running when it matters.
One note from that rebuild I want on the record. I wanted to rank the dashboard by real usage, but the recorder only retains ten days of history and the family hadn’t been home since April, so recent service calls would have described an empty house. There was no older backup. So the hierarchy came from stated priorities and confirmed workflows rather than analytics — and I wrote down that it did, instead of dressing up ten days of an empty house as data.
The part that was hard
The work was good. That’s exactly why it was hard to stop.
Sunk cost doesn’t feel like sunk cost when the thing is well-built. It feels like a responsibility. I had a 97-Lighthouse PWA with a drilled rollback and a hostile review already survived — throwing away the plan for it felt like throwing away the craft, and those are different objects.
Here’s the reasoning that got me over it. When you duplicate a system you don’t own, you don’t inherit its behavior once. You inherit it continuously. Every upstream release adds a feature that becomes a gap in your version. Every change to its state model is a change you have to chase. You’re not maintaining an app — you’re maintaining a diff against a moving target, and the target has more people working on it than you do.
The compounding is invisible at first because in month one your version is genuinely better. It’s better because it’s small and opinionated and you built it for exactly your house. Month eighteen is when you find out that “small and opinionated” has quietly become “missing eleven things and subtly wrong about two.”
The move I’m trying to make more often is to improve the thing I already have before replacing it. It’s less fun. It ships less code. Nobody gets to write a post about the app they built.
Except this one, apparently.