Skip to content
scoi.io

IoT · 2 min read

Designing offline-first BLE sync for hardware that goes underwater

Resumable chunked uploads, conflict rules, and why the app should never care whether there’s signal.

scoi.io · Jun 22, 2026

Dark water and a compact marine device with an electric-blue pairing pulse

A dive computer does not get a polite HTTP timeout. It disappears for forty minutes, comes back with a burst of samples, and the phone that is supposed to receive them may be in a locker.

AquaPlane forced that constraint into the product, not into a “sync later” footnote. The app’s job is to look like it was always connected. The radio’s job is to catch up when it can.

The phone is not the source of truth

The device writes a local log. The phone holds a cache. The cloud holds the durable record. Those three copies disagree on purpose for long stretches of time. The mistake is pretending they don’t.

We treat every sample as an immutable append with a device id, a monotonic sequence, and a checksum. The phone never “edits” a dive. It only acknowledges ranges it has fully received.

That sounds obvious until a user pairs a second phone. Then you need a rule: the device is the authority for what happened in the water; the cloud is the authority for what has been stored; phones are caches that can be thrown away.

Chunked, resumable, boring

BLE payloads are small. A dive log is not. We split uploads into chunks the radio can finish even if the swimmer walks out of range mid-transfer. Each chunk is acknowledged. A reconnect starts at the first missing range, not at byte zero.

The UI never shows a spinner that means “hope.” It shows “3 of 11 segments on this phone” or “uploaded.” Those are different states. Collapsing them is how you get support tickets that say “it synced” when it didn’t.

Chunked BLE transfer with missing-range resume
A dropped connection resumes at the first missing range. The log is never rewritten.

Conflict rules you can explain

When two phones have overlapping caches, last-write-wins is the wrong instinct. The log did not change. One phone is simply behind. We merge by sequence number and refuse to invent samples.

If a firmware update changes the schema, the phone stores the raw payload and a parser version. Old apps still upload. New apps still read. The cloud migrates.

The app should never care whether there is signal. It should care whether the last acknowledged range is complete.

That sentence is the whole architecture. Everything else — pairing, OTA, the pretty charts — sits on top of it.

If your hardware leaves coverage as a feature, not a bug, design the log first. The radio is just a way to catch the log up.

← All insights

Building something in this neighbourhood?

A short note is enough. We’ll ask the right questions back.

Start a project