A companion app for connected hardware has two connectivity dependencies most web apps don't: a link to the internet/backend, and a link to the device itself (often BLE). Either one can be down independently, and a design that assumes both are always available breaks in exactly the situations users most want it to work — no signal in a basement where a smart-home hub lives, a device out of Bluetooth range, a car with no cell coverage. Offline-first design treats these as expected states, not edge cases.

Local-first data, synced when possible

The core pattern is storing data locally on the device (in a local database, not just in-memory state) as the source of truth for what the app displays, with backend sync happening asynchronously in the background rather than being a precondition for the UI to function. The app reads from and writes to local storage immediately, and a sync layer reconciles local and remote state whenever connectivity allows — the user experience doesn't stall waiting for a network round trip for every interaction.

Queueing actions taken while offline

Actions a user takes while offline — changing a setting, triggering a command — shouldn't fail outright; they should queue locally and execute once connectivity (to the device, the backend, or both) returns. This requires the app to track pending actions explicitly, show the user their queued/pending state honestly (not silently pretend the action succeeded), and handle the case where a queued action is no longer valid by the time it executes — a command queued for a device that's since been removed from the account, for instance.

Conflict resolution when local and remote state diverge

If a user changes a setting locally while offline, and the device or backend state changes independently in the meantime (another user made a change, the device's actual state changed), the app needs a defined way to reconcile the two when sync resumes — not an undefined behavior that surprises the user. Common approaches include last-write-wins with a visible timestamp, explicit conflict prompts for changes that can't be automatically reconciled, or domain-specific merge logic where it's clear which value should win (device-reported sensor readings should generally not be overwritten by a stale local cache, for instance).

Distinguishing "no device connection" from "no internet"

These are different failure states that call for different UI treatment and, often, different recovery actions — "can't reach your device, check it's powered on and in range" versus "can't reach our servers, some features may be limited." Collapsing both into a generic "connection error" message forces the user to guess which problem they're actually facing and how to fix it. Distinct, accurate error states are a small design decision with an outsized effect on how trustworthy the app feels when something's wrong.

What should still work fully offline

How we approach this

We design the local-data and sync layer as a first-class part of the app architecture, not an add-on handled after the "happy path" UI is built. See our mobile & web app work for how this fits into building companion apps for connected hardware.