Write the protocol down first
A protocol that exists only as code in two repositories is not a protocol, it is a pair of assumptions. We produce a written specification covering message framing, field semantics, error responses, and version negotiation, and both sides implement against it.
Versioning belongs in the first release, not the second. Retrofitting a version field once devices are deployed means supporting an unversioned legacy dialect indefinitely.
Design for the reconnect, not the connect
The interesting cases are all after the first successful connection: a phone that walks out of range mid-session, a gateway restarted during an upload, a device that has been logging for a week with nothing listening.
Sequence numbers, resumable transfers, and commands that are safe to repeat turn those situations into a short delay instead of lost data or a duplicated action.
Treat provisioning as a product feature
Onboarding is the first thing a customer experiences and the flow with the most ways to fail: permissions not granted, the device not advertising, credentials mistyped, the network unreachable. Each of those needs a distinct, recoverable state in the flow rather than one generic failure message.
Budget the payload
On a battery device, bytes cost energy, because transmit time is where the power goes. Batching, compression, and sending differences rather than full state are worth real engineering attention when the radio dominates the energy budget, and are unnecessary complexity when it does not.