Home Assistant is extraordinary software. Its interface is built to configure a house, not to live in one. Those are different jobs. In configuration, each entity is reachable and equally important. In life, five things matter at nine at night: the lights, a scene, the music, the thermostat, and the door lock. Those five must be one thumb away.
My house has 1,625 entities. Several of them are the same lamp. The daily subset is about five percent of that. The other ninety-five percent buried it.
So I built a control surface. It was good. I stopped work on it anyway. That decision is the real subject of this post.
The build
home. was an adaptive PWA in front of Home Assistant. It was the eighth item in my family app fleet. Home Assistant kept the entities and the automations. home. was only the experience layer.
I still like the core idea: the chrome stays fixed, and the content reorders by the phase of the day. Morning, day, evening, night, late. The phase does not come from a clock. It comes from the sun’s elevation, which Home Assistant already tracks. Above twenty degrees is day. Between zero and twenty degrees is morning or evening, by side of noon. Below the horizon is night, and well below is late.
In the evening, the music and the scenes float up, and the thermostat sinks. At night, the house shows the locks first. Nothing hides. Things reorder.
Then, in one long run of eleven changes, I hardened it. It felt native, not like a webpage in a shell:
Optimistic UI everywhere. Tap a light. It responds immediately. Then the command confirms, or it rolls back with a toast after a five-second timeout. A guard blocks the double-tap. A real lamp answers over a WebSocket after some hundreds of milliseconds. An interface that waits for the lamp feels broken, even when it works correctly.
Connection resilience as the critical path. A home control app that shows a spinner is worse than a light switch. So: reconnect backoff with jitter. A sixty-second ping/pong watchdog for a socket that is open but dead. That is the worst failure state, because everything looks fine and nothing is fine. A fifteen-second timeout on the connect phase itself. Connection metrics and an event ring buffer behind a debug view. A full state rehydration on each reconnect, because the house changed while you were gone.
Error screens that name the broken layer. A health probe classifies the failure. You are offline, or the network path failed, or the proxy is down, or Home Assistant itself is gone. Then the app shows a real error screen with a retry countdown, not an empty page. Four different sentences for four different problems.
Deploy safety with a drilled rollback. Each build stamps a version file. The deploy health-checks itself, and on failure it rolls back to the previous build. The important part: I triggered a real rollback before I trusted it. An untested recovery path is a plan, not a capability.
148 unit tests, plus Playwright end-to-end tests against a mock Home Assistant WebSocket server. The script walks through outage, reconnect, restore, rehydration, and interaction, plus an error screen that heals itself. I wrote the state stores by hand, so that they were unit-testable.
Lighthouse 97/96/100/100. 52KB gzipped. All green.
The adversarial review
Before I trusted any of that, I pointed a reviewer at the app. The reviewer had no stake in the outcome. It had one instruction: prove that this app is not production-ready.
It found no critical defects. It found four HIGH defects. All four were real. I fixed all four on the same day:
- A stale pending-call map cross-wired the promises across reconnects. A command sent before a drop could match the response of a different command sent after the drop. Your kitchen light acknowledges, and the real answer came from a scene call, ninety seconds old.
- An armed backoff timer tore down healthy sockets after a manual retry. You press retry, and it works. Then the earlier scheduled reconnect fires and kills the good connection. The recovery mechanism broke the recovery.
- The rollback path aborted in the exact mid-swap-death state that it exists to handle. The safety net worked in each scenario except the one it was built for.
- 46 leaked store subscriptions. A test never shows this defect. It slowly eats a long-lived tab.
One finding still makes me laugh: the PWA icons in the manifest never existed. A person who installed the app to a home screen received the HTML page as an icon. It was that way from the start, on an app that I opened daily. You never look at your own icon. You tap where it lives.
That review is my strongest argument for adversarial passes. Each 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, with a simple product question: should home. replace the Home Assistant dashboard completely?
An honest answer required an audit of what was there. The audit found two things.
First, the house was a mess underneath. 228 devices, 1,625 entities, and only 32 devices with an assigned area. Zero floors. Zero labels. No curated native dashboard, because I never made one. I compared my polished app against a default that I never configured. (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. It duplicated the authentication, the WebSocket state, the entity discovery, the service calls, and the native feature coverage. Five systems, implemented again, worse, forever.
The first approved direction was a compromise: a read-only panel embedded inside Home Assistant. My interface would live in its shell and inherit its plumbing. I built it. I tested it. I committed it. I deployed it for a short time.
Then I looked at the native dashboard with attention, for the first time in a year. The direction changed:
Optimize native Home Assistant. Do not maintain a second implementation of the house.
The thing that died
Precision matters here. “I deleted my app” is the better story, and it is not what happened.
home. is still online. The source is intact. I removed the embedded panel’s registration after the native redesign shipped, and I kept its source and its rollback. The app remains as a fallback.
The ambition died. The plan for it to become the primary interface of the house died. The maintenance burden of that plan died with it, and that burden stretched out in front of me forever.
In its place, I rebuilt the native dashboard: five views instead of a wall of entities. The settings moved out of the primary tabs. The cameras folded into security. The security view uses Home Assistant’s own lock, cover, and alarm controls, and that decision is the thesis in one line. I will not write a better confirm-before-unlock flow than the people who work on it full time, and my version would be the one that runs when it matters.

One note from that rebuild goes on the record. I wanted to rank the dashboard by real usage. But the recorder keeps ten days of history, and those ten days were not a representative sample. So the hierarchy came from stated priorities and confirmed workflows, not from analytics. I wrote that down, instead of a costume that makes ten unrepresentative days look like data.
The hard part
The work was good. That is the exact reason it was hard to stop.
A sunk cost does not feel like a 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 survived hostile review. To discard the plan felt like a discard of the craft. Those are different objects.
This reasoning got me past it. When you duplicate a system that you do not own, you do not inherit its behavior once. You inherit it continuously. Each upstream release adds a feature that becomes a gap in your version. Each change to its state model is a change that you must chase.
You do not maintain an app. You maintain a diff against a moving target, and the target has more people than you do.
The compound cost is invisible at first, because in month one your version is better. It is better because it is small and opinionated, and you built it for your exact house. In month eighteen, you learn that “small and opinionated” became “missing eleven things and subtly wrong about two.”
The move that I want to make more often: improve the thing that I have, before I replace it. It is less fun. It ships less code. No person gets to write a post about the app that they built.
Except this one, it seems.