When the cloud goes dark, your house shouldn't

An IoT app's biggest risk isn't a bug — it's losing the network. Five engineering choices that keep smart-home apps useful during hours-long internet outages.

The lights in a home are run by a phone app. So is the water pump, the water heater, and a handful of outlets. The app talks to the breakers over the vendor’s cloud platform, which works fine when the home internet is up. The trouble is that the link drops for hours at a time, sometimes in the middle of the night. When it does, the vendor’s own app stops being able to flip a switch. The homeowner still needs the lights.

This post is about the five engineering choices we made for that app — choices that look paranoid if you live somewhere with reliable internet, and that look obvious once you’ve watched a customer explain why their kid can’t take a shower at 9 PM.

What this gets you

  • A mobile app that keeps controlling the devices when the home link is dead.
  • A single codebase for Android and iOS that talks to two transports without branching the UI.
  • Vendor credentials that never enter the source code, and that the vendor can revoke.
  • A domain rule that prevents a single-line bug from doubling every total in the app.
  • A small enough surface that one developer can keep it correct over years.

The constraint

The vendor sells smart breakers and gives a free app for free. That app is fine for ninety percent of the people who buy the breakers. This homeowner was not one of them. Two constraints ruled the design:

  1. The internet link to the home is unreliable. Hours-long drops, often at night, often for reasons no one explains. Any feature that depends on the cloud being reachable in that moment is broken.
  2. The vendor’s app does not show total house consumption. Each breaker has a meter; the vendor app shows each meter; the homeowner has to add them up by hand. And, if the homeowner adds up a parent breaker and its children, the sum is wrong, because the parent’s meter already counts the children.

We also had a budget constraint. There was no room for a custom server to sit in the middle of the breaker control path, and no appetite for one. Whatever we built had to live in the phone and talk to the vendor’s existing infrastructure.

Five engineering choices

1. No server of ours sits between a switch and the thing that flips it

The first decision was the cleanest: on the phone, for control, there is nothing of ours in the middle. The app talks directly to the vendor’s cloud OpenAPI, and signs every call with the homeowner’s vendor credentials. We did not invent a new protocol, and on mobile we did not put a relay in front of it.

We do run a small backend, because some jobs genuinely need one. It has three, and none of them is relaying a switch:

  • the web build signs its cloud calls from a function of ours, because a browser cannot hold the vendor secret safely;
  • meter readings are stored in our own database, which is what makes the household’s power history and charts possible;
  • the solar-battery automation rules are evaluated there, so they keep working when the phone is asleep.

The household also signs in with an account of ours, so the app can keep its own history instead of relying on the vendor’s account. None of that touches a breaker command.

The rule we wrote down first is the one that matters: no server of ours may sit between a switch and the thing that flips it. Everything else can live wherever it is cheapest to run.

2. There is a second transport, and it is always there

The vendor’s platform has a second interface: a local network protocol that talks to the breakers over Wi-Fi, inside the home, with no cloud involvement. The protocol is encrypted (AES-GCM over a TCP connection on port 6668), it requires a per-device session key, and it is documented well enough to implement against.

So the app has two transports. When the cloud link is up, it uses the cloud. When the cloud link is down — and the phone is on the home Wi-Fi — it switches to the local protocol. The same screens handle both transports; the user does not need to know which one is active, and there is no “mode” toggle.

The discipline is that both transports implement the same domain interface. A use case like “turn breaker X off” does not care whether the call goes over HTTPS to the vendor’s cloud or over TCP to the breaker itself. If we ever have to add a third transport — say, Bluetooth — it slots into the same seam.

3. The vendor secret is never in the source code

The vendor issues a client ID and a secret per project. Both are needed to sign OpenAPI calls. Both are sensitive.

Neither one is written in the source tree. They are injected when the app is built or run, from a local .env file that is gitignored, together with a generated file that is gitignored as well. A developer who wants to run the app locally fills in their own copy of the credentials; nobody commits theirs.

That rule is the one we actually care about: no secret lands in the repository. The shipped build does contain the value — any client-side credential does, because the device has to sign with it — so we do not pretend obfuscation is a wall. Obfuscation raises the cost of a casual extraction. The real protection is revocation: the credentials belong to a vendor project, and the vendor can re-issue them without a code change.

4. The total-consumption rule is in the domain, not in the UI

This was the bug we almost shipped. The vendor returns a meter reading per breaker. The naive sum in the UI is: add them all up. That is wrong. Each breaker in the home has a parent — the breaker upstream of it that feeds its circuit. The parent’s meter already counts everything the child counts. Summing the parent and the child double-counts the child’s draw.

The fix is not in the UI. It is in the domain model: the topology (parent → children) lives next to the breaker list, and the house total is computed by summing only the breakers that have no parent — the heads. The UI asks the domain for the total. The UI never iterates the breaker list and adds.

If the topology is wrong, the total is wrong. But the wrongness is contained: one place to fix, one place to test, one place to display.

5. The architecture has no Flutter in the domain

The domain layer is pure Dart. No Flutter, no platform channels, no HTTP client, no sockets. The repository interfaces are abstract; the implementations (cloud and LAN) live in the data layer; the UI talks to use cases that return values, not futures that wrap exceptions.

This matters for the offline story more than it looks. When the transport changes — from cloud to LAN — the UI does not change. When the protocol changes — from v3.5 to a hypothetical v4 — only the data layer changes. When we want to add a third transport, we add one more implementation of the same interface. The domain does not know what HTTP is.

The discipline is mechanical: domain/ does not import Flutter, does not import the HTTP client, does not import the crypto library. It imports only other Dart code. The CI can check this with a single grep.

What we did not build

Three things we deliberately left out:

  • A push-notification system. The vendor handles its own pushes. We did not need to build one on top.
  • A marketing analytics pipeline. The vendor already sees the device events; building our own would have been vanity. What we do store server-side is the household’s own meter history — that is a feature for the family, not analytics about the family.
  • A protocol of our own. The vendor’s API was designed for third-party clients signing calls with project credentials. We used it as it is instead of building something beside it.

Each of these is a thing that someone will eventually ask for. None of them was on the table for V1.

What’s next

The roadmap is short. The next round is letting the homeowner edit the breaker topology inside the app, propagating an off command from a parent to its children, and adding charts for power use over time. The longer-term plan — automating the water heater when the battery is low, or switching the pump when a stored condition fires — needs an always-on piece in the home, and that decision is still open with the client.

The architecture supports each of those without rewrites. That is the only test that matters for a small, private, residential app: it has to be small enough to keep correct, and shaped enough to grow without breaking.

Building a connected product that has to keep working when the link drops? Book an intro call. We’ll tell you honestly whether we can help.