Three separate reports, over a few months, that turned out to be one project.

Someone deleted an appliance they no longer owned and found it back in their list a week later. Someone logged in on a new phone, saw an empty list, and assumed years of records were gone. And a number of people had appliance records whose photos — the model label, the repair receipt — were missing, while the text was intact.

None of these were network problems. All three were consequences of a sync design that had been adequate when the app was simpler.

A delete that leaves nothing behind is not a delete

The resurrection bug is the classic one and it is worth spelling out, because the reasoning that produces it is completely sensible.

Sync worked by comparing records. Each device sent what it had, the server merged, everyone converged. Delete an appliance on your phone and the row is gone locally, then sync runs and the server no longer receives it.

But absence carries no information. The server cannot distinguish "this device deleted the record" from "this device has not seen the record yet." And a second device that still has the record will happily send it — so the record comes back, on both devices, looking like it was never deleted.

The fix is a tombstone: deleting a record replaces it with a marker saying this identifier was deleted, at this time. The marker syncs like anything else. A device receiving a tombstone deletes its local copy instead of re-uploading it, and the delete propagates the way the user expected in the first place.

Tombstones bring their own housekeeping — they accumulate, and they have to live long enough that a device which has been offline for months still learns about the delete when it reconnects. We keep them well beyond any plausible offline period and clean up after that. A tombstone is a few bytes; a resurrected appliance is a user who no longer trusts the app.

While we were in there we also fixed the merge rule. Conflicts had been resolved by comparing the modification time each device recorded, which assumes the devices' clocks agree. They frequently do not — a phone with a manually set clock can be minutes or hours off, and it will win or lose every conflict regardless of what actually happened. Ordering now uses the server's view of when a change arrived, so a wrong device clock cannot silently overwrite a correct record.

An empty list is a claim, and we were making it falsely

The second report is not a data bug at all, and it did more damage than the first.

When you log in, there is a window where the app knows who you are and has not yet fetched anything. During that window the local database, scoped to the account, is legitimately empty. The list screen rendered what it had, which was nothing.

So people signed in on a new phone and saw an app confidently displaying "no appliances." Some of them waited. Some of them started re-entering records by hand, and then had duplicates when the sync completed. At least one uninstalled and reinstalled, which is the worst possible response and an entirely reasonable one when an app tells you your data is gone.

The interface now distinguishes three states that were previously one: we have no data yet, we have data and it is empty, and we could not reach the server. Only the second one says there are no appliances. The first says it is loading, and the third says so and offers a retry.

This is not a sophisticated fix. It is the recognition that "empty" and "unknown" are different, and that the list screen had been asserting the first when it meant the second — the same class of mistake as blank versus unknown in an appliance record, one layer up.

Account boundaries during the switch

Related, and sharper: while authentication state was changing, there was a window where a query could run against local data that belonged to the previous account.

Nobody reported seeing another person's records — the queries were scoped, and the exposure was to data the same person had put there under a different account. It was still wrong, and it is the kind of wrong that gets worse rather than better as an app grows.

The account is now part of the data access path rather than something checked alongside it. A query that is not scoped to a specific account does not compile. Signing out clears the in-memory state before the sign-in path can begin, so there is no interval where a stale account and a new one are both plausible.

The settings and account screens were rewritten in the same pass to make the active account visible where you can act on it. Half of the confusion in the original reports came from people not being certain which account they were signed into, which is not a thing anyone should have to guess about.

Photos sync differently from records, and pretending otherwise breaks both

The third report — records present, photos missing — comes from a genuine asymmetry.

A record is a few hundred bytes. A photo of a model label is a few megabytes. They cannot reasonably be uploaded together, so images go through a separate queue that runs when there is a suitable connection and retries on failure.

The old design treated the record as the unit of sync and let the image catch up. In practice that meant a record could be fully synced and appear complete while its attachment had never left the original device — and if that device was wiped or the app reinstalled before the queue drained, the photo was gone. The record was still there, saying it had an image, pointing at nothing.

Now a record's sync state accounts for its attachments. A record whose image has not uploaded is not shown as fully backed up, and the app can tell you what is outstanding. Before an account change or a device migration, there is a plain answer to "is everything actually safe" rather than an assumption.

We also stopped deleting the local copy of an image on the assumption that the remote one exists. Local copies are removed only after the upload is confirmed.

What still needs a human

Sync can protect a path. It cannot recover something that never made it onto the device in the first place.

Let pending work finish before switching accounts or devices. The app now tells you when something is outstanding. It is worth the thirty seconds.

Do not clear app data or uninstall while a backup is in progress. This is the one genuinely unrecoverable action, and it was the reaction people had to the empty-list bug.

Check the active account before editing. Particularly if you use more than one.

After moving to a new device, confirm the list looks right before you start adding. Duplicates created during a sync gap are tedious to clean up.

What we would do differently

Tombstones should have been in the first version. Every sync system needs them, this is well known, and we skipped them because the first version had no delete. When delete arrived, nobody revisited the sync design — the feature was added to the model and the model's assumptions were never re-examined.

The empty-list bug is the one that bothers us more. It was not a hard problem. It was a loading state we never wrote, in the highest-stakes screen in the app, and the cost was people believing their data was gone. Where an app can be wrong, being wrong in the direction of "your records are missing" is the most expensive direction available.