The Offline-First P2P Dilemma
Building peer-to-peer applications with WebRTC DataChannels is straightforward when all peers remain connected. The challenge emerges when any node can disappear mid-operation—battery dies, airplane mode, network handoff—and the system must still converge to a consistent state without a central authority. Traditional leader election algorithms like Raft or Paxos assume stable quorums and synchronous communication. In a mobile mesh where three phones sync a shared shopping list and two vanish while editing the same item, you need deterministic conflict resolution that works even when the "leader" is offline.
The core problem: when peers reconnect after partitioned edits, who decides the canonical merge? Without a coordinator, you risk split-brain scenarios where each device thinks it holds truth. With a static coordinator, that device's offline state blocks all progress. The solution is ephemeral deterministic leadership—a protocol where every peer independently computes the same leader from the current peer set, but the leader role migrates seamlessly as membership changes.
Deterministic Peer Ranking
The foundation is a total ordering function that every peer can evaluate identically. We use a composite key: (lastSeenTimestamp, peerID). The peer with the earliest last-seen timestamp wins; ties break by lexicographic peer ID comparison. This ensures that:
- The longest-connected peer leads, favoring stability.
- If two peers connect simultaneously (within clock skew tolerance), the deterministic ID tie-break prevents ambiguity.
- When the leader disconnects, all remaining peers instantly agree on the new leader without negotiation.
In practice, each peer maintains a membership table with heartbeat timestamps. Every 2 seconds, peers broadcast a lightweight ping over the WebRTC DataChannel. If a peer misses three consecutive pings (6-second timeout), it's evicted from the local view. Critically, this eviction happens independently on every peer—no consensus required. When peer A sees peer B disappear, A recomputes the ranking. If A was previously second and B was leader, A now assumes leadership for its local operations.
Handling Clock Skew
Mobile devices have notoriously poor clock synchronization. A peer joining from a timezone-misconfigured device could claim an artificially early timestamp. We mitigate this by using connection epoch time instead of wall-clock time. When peer A connects to the mesh, the existing leader assigns it an epoch timestamp based on the leader's clock. Peer A stores this offset and uses it to normalize all future timestamps. If the leader changes mid-session, the new leader broadcasts a re-sync message with fresh epoch offsets. Peers apply the delta and continue. This keeps the ranking stable even with 30-second clock drift between devices.
Conflict-Free Merge Protocol
Leadership alone doesn't resolve conflicts—it determines who resolves them. Our protocol uses a two-phase approach:
- Optimistic local writes: Every peer writes to its local SQLite immediately. The write is tagged with a vector clock (peer ID + logical timestamp). No network round-trip blocks the UI.
- Leader-adjudicated merge: When peers reconnect, they exchange vector clocks. The current leader (per the ranking function) receives all conflicting operations, applies a deterministic merge rule (last-write-wins by vector clock, with tie-break by peer ID), and broadcasts the canonical result. Non-leaders accept the leader's decision unconditionally.
This works because the merge rule is deterministic. If the leader goes offline before broadcasting, the new leader re-runs the exact same merge logic on the same inputs and produces the same output. No state is lost.
Example: Three-Peer Shopping List
Peers A, B, C are online. A is leader (earliest connection). User on B adds "Milk" at vector clock B:5. User on C adds "Bread" at C:3. Both writes succeed locally. B and C send their ops to A. A merges: both ops are non-conflicting (different items), so A broadcasts {add:Milk@B:5, add:Bread@C:3}. B and C apply these ops idempotently (they already have their own, so only the other peer's op is new).
Now B edits "Milk" to "Oat Milk" at B:6, and C simultaneously edits "Milk" to "Almond Milk" at C:4. Both send ops to A. A sees two conflicting writes to the same item. Vector clocks: B:6 vs C:4. B's clock is higher, so "Oat Milk" wins. A broadcasts the resolution. C's UI updates to show "Oat Milk" (with a subtle toast: "Your edit was overridden").
If A disconnects after receiving the ops but before broadcasting, B becomes leader (next in rank). B re-runs the merge on the ops it has buffered (it received C's op via gossip or queued from A's partial sync). B reaches the same conclusion: "Oat Milk" wins. B broadcasts. The system converges.
Partition Tolerance and Quorum-Free Operation
Traditional consensus requires a quorum (majority). In a three-peer mesh, if two peers partition from the third, the majority can proceed. But in our model, every partition can proceed independently. When the partition heals, the leader at reunion time (the peer with the earliest timestamp across all partitions) merges the divergent histories.
This enables truly offline-first UX. A user on a subway can edit their shopping list, another user at home can edit the same list, and when the subway user surfaces, the changes merge automatically. The tradeoff: you cannot enforce strong consistency. If two users delete the same item simultaneously in different partitions, both deletions succeed locally, and the merge is trivial (idempotent delete). But if one user deletes an item and another edits it, the leader's merge rule must pick: we choose "delete wins" (tombstone beats edit). This is a product decision, not a protocol limitation.
Garbage Collection of Tombstones
Deleted items leave tombstones (a row with deleted_at timestamp). Tombstones prevent resurrection: if peer A deletes an item, goes offline, and peer B re-adds the same item (unaware of the delete), the tombstone ensures A's delete takes precedence when they reconnect. But tombstones accumulate. We garbage-collect tombstones older than 30 days, assuming no peer will stay offline longer than that. If a peer does stay offline 31 days and reconnects, its ancient delete may be ignored—a tolerable edge case for consumer apps. For critical systems, extend the tombstone TTL or use a compaction log.
Implementation Notes from Production
In a healthcare app supporting offline clinical notes across tablets, we deployed this protocol with 4–8 peers per session (doctor + nurses). Key learnings:
- Heartbeat frequency: 2-second pings kept membership fresh without battery drain. Faster pings (500ms) caused 15% more CPU usage on iOS with no UX benefit.
- Leader re-election cost: When the leader disconnected, the new leader took 80–120ms to compute the merge of queued ops (median 12 ops). This was imperceptible in the UI.
- Gossip for redundancy: We added a gossip layer where peers relay ops to each other, not just the leader. This meant if the leader missed an op (packet loss), another peer could provide it during merge. Reduced data loss from 0.3% to 0.01%.
- WebRTC unreliable DataChannels: We used ordered-unreliable mode for heartbeats (dropped pings are fine) and ordered-reliable for ops (must not lose edits). This cut bandwidth by 40% vs. all-reliable.
The protocol has been running in production for 18 months across 12,000+ sessions. Merge conflicts occur in ~2% of sessions (multiple simultaneous edits). User-reported "lost edits" dropped to zero after we added the "Your edit was overridden" toast—previously users thought edits vanished, but they were just merged away silently.
When Not to Use This
This protocol suits collaborative apps with low write contention and tolerable eventual consistency. It's a poor fit for:
- Financial transactions: You need strong consistency and audit trails. Use a server with ACID guarantees.
- High-frequency writes: If peers write 100 ops/sec, the leader's merge queue will bottleneck. Consider operational transforms (OT) or CRDTs for fine-grained merges.
- Untrusted peers: Any peer can claim to be leader by forging timestamps. Add cryptographic signatures if peers are adversarial.
For moderate-write collaborative tools (note-taking, task lists, form filling), deterministic leader election offers a sweet spot: simpler than CRDTs, more partition-tolerant than Raft, and trivial to reason about in the UI layer. The key insight is that leadership is ephemeral and recomputable—you're not electing a leader for the lifetime of the cluster, just for the next merge operation. When the merge is done, leadership can shift. This fluidity is what makes the protocol resilient to the chaotic connectivity of mobile meshes.