A collaborative application loses its network connection. The Wi-Fi icon disappears, one user keeps typing, and another user somewhere else keeps editing the same document. Thirty seconds later, the first machine reconnects.
What should happen?
The obvious answer is: synchronize the changes. But that answer hides most of the difficult engineering. Does reconnection mean the WebSocket opened again? Does it mean WebRTC found a viable ICE candidate? Does it mean every local update reached another replica and every remote update reached this one? Are the documents merely connected, or have they actually converged? Should an old cursor reappear? What if the user refreshed while offline, the original peer-to-peer route disappeared, or two people changed the same logical object during the partition?
Most importantly, at what exact point is it safe for the interface to say:
“Synced.”
These questions became concrete for me while building FeatherSpace, a local-first collaborative coding workspace using Yjs CRDTs, WebRTC, WebSocket signaling, TURN traversal, browser persistence, and offline-to-reconnect recovery. Getting two browsers to edit the same state while both are online is the easy part. Reconnection is where the architecture reveals what it actually guarantees.
The central distinction is simple but important:
Network reconnection and state reconnection are not the same thing.
A transport can be healthy while application state is stale. A document can be converged while presence information is wrong. And a user can continue editing safely even when no network connection exists at all.
Understanding those distinctions is the difference between adding a collaborative feature and designing a distributed collaborative system.
Local-First Changes What “Offline” Means
Traditional web applications often treat the server as the authoritative copy of application state:
If the server cannot be reached, many operations cannot proceed because the client is fundamentally waiting on the authoritative system.
A local-first application deliberately changes that relationship:
The user edits the local replica first. Synchronization happens afterward.
That distinction changes what a network outage means. Instead of:
Editing unavailable
the system can behave more like:
Editing continues locally.
Replication is temporarily delayed.
The original Ink & Switch work on local-first software describes this model in terms of data remaining usable on the device while independently modified replicas synchronize and merge once connectivity becomes available again.
That creates a very different recovery problem. The goal is no longer to restore access to the place where the document lives. The document is already available. The goal is to reconcile replicas that may have continued evolving independently during the partition.
Connection Is Not Synchronization
Suppose the browser reports:
peerConnection.connectionState === "connected";
Can the application immediately show this?
✓ Synced
Not necessarily.
The network layer has established that the peers can currently communicate. The application still needs to determine whether those replicas contain the same durable document state.
Those are separate properties.
I find it useful to model collaboration as several layers:
A failure at one layer does not automatically destroy the layers above it. Wi-Fi can disappear while the local document remains perfectly healthy. WebRTC can reconnect while the CRDT protocol is still exchanging missing updates. The document may converge while stale presence data still claims that a disconnected collaborator is editing line 42.
That is why a collaborative application's internal state usually needs more precision than:
online
offline
Something closer to this represents reality better:
offline
connecting
connected
synchronizing
synced
degraded
Those states describe different guarantees. Compressing them into one green dot may simplify the interface, but the application itself still needs to know which one it is actually in.
What Happens During an Offline Edit?
Imagine Alice and Bob begin with the same document:
const retries = 3;
Their connection disappears. Alice changes the retry count:
3 -> 5
while Bob independently adds:
const timeout = 5000;
There are now two legitimate replicas evolving at the same time:
A snapshot-oriented design might frame the recovery problem as:
Alice's document
vs.
Bob's document
and then try to decide which copy should win.
A CRDT-based model approaches the problem differently. Instead of treating one full snapshot as authoritative, the shared state is represented so that independently generated updates can later be combined deterministically.
Yjs separates the shared document from the providers responsible for moving or persisting its updates. A local persistence provider can retain edits on the device while a network provider later exchanges whatever state another replica is missing.
The important property is convergence:
If replicas eventually receive the same set of updates,
they should eventually represent the same document.
That is a much stronger model than:
Reconnect and download the latest document.
The latter assumes there is one copy whose recency makes it authoritative. Local-first collaboration cannot safely make that assumption because valid work may have been created independently on both sides.
Reconcile, Do Not Replace
One of the most dangerous reconnect implementations would look like this:
async function reconnect() {
const remoteDocument = await fetchDocument();
editor.setValue(remoteDocument);
}
That can make sense in an architecture where the remote server owns the document.
In a local-first system, it can destroy valid offline work.
Imagine the replicas contain:
Previously shared state:
A B C
Offline local state:
A B C D
Remote collaborator state:
A B C E
Replacing the local document with the remote snapshot loses D. Uploading the local snapshot as authoritative loses E.
The required operation is conceptually closer to:
merge(
local changes,
remote changes
)
which can yield:
A B C D E
assuming the operations are semantically compatible.
The network returning should therefore trigger reconciliation, not reset. CRDT-based systems generally exchange state differences or updates rather than blindly replacing the current document with whichever snapshot happens to be reachable after reconnection.
This is also why locally persisted state cannot be treated as an expendable cache. Some of it may represent valid work that has never existed anywhere else.
A Yjs-Style Local-First Model
A simplified local-first Yjs setup might look like this:
import * as Y from "yjs";
import { IndexeddbPersistence } from "y-indexeddb";
import { WebrtcProvider } from "y-webrtc";
const roomId = "workspace-123";
const doc = new Y.Doc();
const persistence = new IndexeddbPersistence(roomId, doc);
const network = new WebrtcProvider(roomId, doc);
The interesting detail is that neither persistence nor networking owns the document.
The Y.Doc does.
Local persistence can populate it:
IndexedDB
↓
Y.Doc
Network updates can populate it:
Peer
↓
Y.Doc
And the editor itself modifies it:
Editor
↓
Y.Doc
All three paths meet at the same shared document model.
That decomposition is powerful because losing one mechanism does not mean losing the document. IndexedDB can preserve state while there is no peer connection. WebRTC can later synchronize missing changes without becoming the authority over what the document is. The editor can continue producing updates regardless of whether a network provider is currently healthy.
This is one of the architectural properties that makes offline editing and later reconciliation possible without inventing separate "offline documents" and "online documents."
Reconnection Is a Protocol, Not an Event
A naïve implementation might listen for connectivity returning:
window.addEventListener("online", () => {
reconnect();
});
That signal can be useful, but it is nowhere near sufficient.
The browser believing that internet connectivity exists does not prove that the signaling service is reachable, another peer is available, WebRTC negotiation succeeded, the CRDT finished exchanging state, authorization is still valid, or every update required for convergence has arrived.
A more realistic recovery pipeline looks like this:
Each stage answers a different question.
Network available means the browser believes connectivity may exist.
Signaling connected means the application can reach the service used to discover or negotiate with peers.
Transport connected means a viable communication path has been established.
Synchronizing means the replication layer is exchanging missing document state.
Synced means the system has enough evidence to claim synchronization has completed according to the protocol's guarantees.
Only after that should ephemeral collaboration state such as presence be rebuilt around the new session.
The UI may eventually collapse some of those states into simpler language, but the architecture should not collapse them internally.
WebRTC Adds Another Recovery Boundary
Peer-to-peer collaboration is often drawn like this:
Browser A <------> Browser B
The real network is closer to:
WebRTC uses ICE to discover a viable path between peers. A direct connection is preferable, but NAT behavior and firewalls can make one impossible. TURN provides a relay when direct connectivity cannot be established.
That creates another useful architectural principle:
The collaboration model should not depend on one particular network path.
Yesterday, a session may have used:
Peer A ───── direct UDP ───── Peer B
while today's reconnect uses:
Peer A ─── TURN relay ─── Peer B
The document semantics should not care.
The transport is infrastructure. The application-level requirement is that missing updates can eventually be exchanged and the replicas can converge.
This separation also makes the collaboration architecture easier to evolve. A CRDT should not have to change its document semantics simply because the transport moved from a direct WebRTC path to a TURN relay, or eventually to another provider entirely.
Define “Synced” Precisely
The word connected sounds obvious until it appears in a collaborative UI.
What does it promise?
A signaling connection?
A WebRTC peer connection?
At least one available collaborator?
A document that has completed synchronization?
Remote persistence?
Those are not equivalent guarantees.
Yjs' WebSocket provider, for example, distinguishes the WebSocket transport being connected from the document being synchronized with the provider. That distinction is useful even if the final UI chooses simpler language.
Internally, the application might represent collaboration state with something like:
type CollaborationState =
| "offline"
| "connecting"
| "connected"
| "synchronizing"
| "synced";
and respond separately to transport and synchronization events:
transport.on("connected", () => {
setStatus("connected");
});
provider.on("sync", (isSynced) => {
if (isSynced) {
setStatus("synced");
} else {
setStatus("synchronizing");
}
});
The exact provider API is less important than the distinction.
A connection means:
communication is possible
Synchronization means something closer to:
the replication protocol believes the required state
has been exchanged
A collaborative product should not promise the second merely because it has evidence for the first.
Durable State and Presence Need Different Lifecycles
Collaborative applications usually carry at least two fundamentally different categories of shared state.
Durable state
State that belongs to the document itself:
code
board nodes
text
settings
files
document metadata
Ephemeral state
State that describes what a participant is doing right now:
cursor position
selection
active file
online status
user color
currently typing
Those categories should not have the same recovery semantics.
If Alice disconnects for 45 minutes, her document edits must survive. Her cursor position from 45 minutes ago probably should not.
Yjs represents awareness and presence separately from durable document data. That allows a collaborative application to treat disappearance and reconnection differently for each kind of state.
Conceptually:
On reconnect:
Document state -> merge and preserve.
Presence state -> refresh or recreate.
Persisting presence as though it were durable data produces strange product behavior. A collaborator who left yesterday should not still appear to be editing a particular line simply because their last cursor location happened to be saved.
The architecture should encode that distinction instead of relying on UI cleanup to hide it.
Convergence Is Not Product Correctness
CRDTs solve an extremely difficult class of distributed-state problems.
They do not solve every collaborative product decision.
Suppose Alice deletes a code node while Bob, disconnected at the time, edits that same node. The CRDT can deterministically converge those operations, but the product still needs an answer to a different question: what should that result mean to the user?
Should Bob's later edit restore the node?
Should deletion dominate?
Should Bob's content survive somewhere else?
Should the UI tell Bob that concurrent work was discarded or transformed?
Those are application semantics.
Figma's multiplayer engineering work illustrates the same distinction around undo and redo. A collaboration algorithm can make replicas agree while the product still has to define what undo should mean when one person's action occurred between another person's edit and their later attempt to reverse it.
That leads to a broader principle:
Convergence means replicas agree. It does not automatically mean users agree with the result.
Distributed correctness and product correctness overlap, but they are not identical. The CRDT determines how shared state converges; the application determines whether the converged state expresses the product behavior users expect.
Offline Startup Is Part of Reconnection
A partition does not always happen while the application remains open.
Consider this sequence:
- The user edits a document.
- The changes persist locally.
- The laptop closes.
- The application is reopened later without internet.
- The browser starts.
- IndexedDB begins hydrating the document.
Now the initialization path becomes part of the local-first correctness model.
This is dangerous:
const doc = createEmptyDocument();
render(doc);
// Later...
const persisted = await loadFromIndexedDB();
replaceDocument(persisted);
The interface may briefly construct an empty document before local persistence finishes loading it. Depending on how the rest of the application behaves, that empty state may even trigger writes or initialization behavior that competes with valid persisted work.
A safer lifecycle treats local hydration as part of application readiness:
Yjs' IndexedDB persistence provider exposes a synchronization signal for knowing when available local content has been loaded. That small lifecycle detail matters because persisted local state may contain valid work that has never been replicated anywhere else.
Local persistence is not merely a cache.
In a local-first system, it may be the only durable copy of recent work.
Recovery Can Create a Burst of Work
Imagine a user edits offline for eight hours.
When connectivity returns, the application may suddenly need to process document synchronization, presence state, peer discovery, signaling, transport negotiation, and whatever application-level recovery happens around them.
Conceptually:
thousands of document updates
+
presence refresh
+
peer discovery
+
signaling
+
transport recovery
If many clients reconnect after the same outage, recovery traffic may be significantly larger than normal steady-state collaboration.
The pattern is familiar from backend systems:
failure ends
↓
everyone retries
↓
recovery traffic spikes
Collaborative applications therefore need to think about incremental synchronization, batching, update compaction, reconnect jitter, backpressure, memory growth, and large-document behavior.
This is an important difference between testing collaboration while two peers remain continuously connected and testing recovery after a long partition. A system that performs beautifully while processing small, steady updates may behave very differently when hours of missing state suddenly need to be reconciled.
Reconnection should therefore be treated as a load pattern, not merely a UI state transition.
Peer-to-Peer Does Not Mean Infrastructure-Free
WebRTC removes servers from some application data paths. It does not remove infrastructure from the system.
A collaborative application may still require services for:
signaling
authentication
authorization
peer discovery
TURN
session metadata
key exchange
monitoring
Pure peer-to-peer mesh networking also introduces topology limits.
With three peers:
A ↔ B
A ↔ C
B ↔ C
the arrangement is straightforward. As the room grows, each participant may need connections to more peers, increasing network and client-side resource requirements.
That means topology should follow the collaboration shape.
For a small coding room:
P2P may be an excellent fit.
For hundreds of simultaneous participants:
a pure mesh may not be.
This is another reason the shared document model and network provider should remain conceptually decoupled. A system might use:
CRDT + WebRTC
today and later move toward:
CRDT + WebSocket server
or:
CRDT + hybrid topology
without redefining what the document itself means.
Transport architecture can evolve independently if application correctness is not tied to one specific connection mechanism.
Authorization Still Matters After a Partition
Reconnection also introduces a less obvious security problem.
Suppose a user has permission to join a collaborative session. They disconnect for several hours. While offline, their access is revoked. Then their browser reconnects.
Possession of an old local replica does not automatically imply permission to reconnect to every network service involved in collaboration.
Authentication and authorization still need to exist at infrastructure boundaries the application controls, such as:
signaling
TURN credentials
room discovery
persistent server synchronization
shared-key distribution
End-to-end encryption makes this boundary even more interesting. A server may be capable of transporting encrypted updates without understanding the content, but the application still needs policies for admitting new peers and distributing current session material.
Questions remain:
Who may obtain current session material?
How are new peers admitted?
What happens after membership changes?
Can an old peer continue sending updates?
Local-first architecture does not eliminate access control. It changes where access control can meaningfully be enforced.
There is also an important limitation that follows from local-first design: revoking future collaboration access cannot retroactively erase a valid replica already stored on somebody else's device. Authorization can control future participation in infrastructure and key distribution; it cannot pretend locally owned state never existed.
Test Transitions, Not Just Steady State
The happy path tells you very little about whether reconnection is correct.
A more useful test matrix looks like this:
| Scenario | Expected invariant |
|---|---|
| Peer disconnects while idle | Reconnect without document change |
| Peer edits offline | Local edits survive |
| Both peers edit offline | Replicas converge after reconnect |
| Browser refreshes offline | Local document reloads |
| Duplicate CRDT updates arrive | Final state does not duplicate content |
| Signaling reconnects slowly | Local editing remains usable |
| Direct WebRTC fails | TURN can establish connectivity |
| Peer reconnects with stale presence | Old awareness state disappears |
| One peer rejoins after long absence | Missing document state synchronizes |
| Network repeatedly flaps | No lost edits or listener duplication |
| Two tabs open the same document | Cross-tab state remains consistent |
| Access is revoked during partition | Reconnection policy is enforced |
That is much closer to the real test surface of a collaborative system than opening two tabs and checking that typing appears in both.
For FeatherSpace, I added tests around joins, synchronization, reconnect behavior, performance, memory, and reliability because the interesting failures tend to occur during transitions rather than while the system is sitting comfortably in one state.
Useful injected network conditions include:
offline
high latency
packet loss
short disconnect
long disconnect
rapid online/offline flapping
signaling failure
TURN-only connectivity
The interesting question is never simply:
Does it reconnect?
It is:
What invariant still holds after it reconnects?
That change in framing turns reconnection testing from checking a networking feature into testing the actual correctness model of the application.
The Invariants of Correct Reconnection
For a local-first collaborative workspace, I would define correct reconnection with explicit invariants rather than with one connection-state flag.
Local edits survive disconnection
A network failure must not erase locally durable work.
offline edit
+
restart
=
edit still exists
Concurrent valid edits eventually converge
If replicas eventually receive the same document updates, they should settle on the same durable shared state.
Replica A
Replica B
↓
exchange updates
↓
same document state
Synchronization is idempotent
Receiving the same update again should not duplicate its semantic effect.
Networks retry. Providers reconnect. Duplicate messages occur. The replication layer needs to expect that rather than treating duplicates as exceptional corruption.
Network state does not masquerade as document state
connected ≠ synchronized
The UI should not promise more than the application can justify.
Presence is disposable
Cursors, selections, typing indicators, and online status should represent the current session rather than being restored indefinitely like document content.
Local persistence protects unsynchronized work
A newly reachable remote replica must never casually overwrite locally durable edits that have not yet synchronized.
Transport changes do not change document semantics
Whether updates travel over a direct WebRTC connection or through TURN should not affect the resulting document.
Controlled boundaries are revalidated
A previous connection does not imply permanent authorization to signaling, TURN, key distribution, or other protected collaboration infrastructure.
Recovery is observable
Useful operational signals might include:
reconnection attempts
time to transport recovery
time to synchronization
sync failures
TURN fallback rate
peer count
document update volume
IndexedDB hydration time
Without that visibility, the first indication of a broken synchronization system may be a user saying:
“My friend's code disappeared.”
That is not an observability strategy.
A Better Mental Model
The biggest shift for me was to stop drawing collaborative applications as:
Browser A <------> Browser B
and instead treat them as several cooperating systems:
Each part owns a different responsibility.
The CRDT handles replica convergence.
IndexedDB or another local store handles durability on the device.
WebRTC or WebSockets transport updates.
Signaling helps peers discover one another and negotiate connections.
STUN and TURN help establish viable paths across real networks.
Awareness represents transient collaboration state.
The application layer determines what the resulting shared state means to the user.
Once those responsibilities are separated, reconnection becomes much easier to reason about. Instead of asking whether one giant "collaboration system" is connected, the application can ask which guarantee each layer currently provides and which guarantees are still being restored.
So What Does “Correct Reconnection” Mean?
It is not:
WebRTC connectionState === "connected"
It is not:
navigator.onLine === true
And it is not merely:
WebSocket opened successfully
Those states can all be useful pieces of evidence, but none is a complete definition of recovery.
Correct reconnection means that the application can recover from a period of independent operation while preserving the guarantees that made offline work safe in the first place.
A useful definition is:
After connectivity returns, valid durable local work survives; replicas exchange missing state and converge; ephemeral session state is rebuilt instead of blindly restored; controlled authorization boundaries are re-evaluated where necessary; and the application does not claim synchronization has succeeded before the system has evidence to support that claim.
That definition crosses networking, persistence, distributed systems, security, and UX.
Which is exactly why reconnect logic deserves more thought than:
socket.on("connect", sync);
Final Thought
Real-time collaboration demos are impressive because everything appears to happen instantly. Production-quality collaboration becomes interesting for the opposite reason: what happens when nothing is instant?
A peer disappears while somebody keeps working. Another replica changes independently. The browser closes and later starts offline. Local persistence restores work that may never have reached another machine. Connectivity returns through a different network path. Missing updates begin moving again. Old presence information has to disappear. Permissions may have changed while the replicas were separated.
And somehow the application still needs to make all of that feel uneventful.
That is the paradox of local-first software. Underneath the interface is a distributed system dealing with partitions, replicas, eventual synchronization, peer discovery, persistence, topology, conflict semantics, and trust boundaries. To the person using it, losing Wi-Fi should ideally be boring.
They keep working.
Eventually the network returns, and the system quietly makes the replicas agree again.
The hardest part of reconnection is therefore not reconnecting the network. It is making sure the application still knows what correct means when the network comes back.
References
-
Ink & Switch — Local-first software: You own your data, in spite of the cloud. The foundational local-first essay describing offline-capable replicas, CRDT-backed collaboration, ownership, and synchronization after independent editing.
-
Ink & Switch — PushPin: Towards production-quality peer-to-peer collaboration. Explores moving peer-to-peer collaboration beyond prototypes and identifies NAT-traversal reliability as a practical challenge.
-
Figma Engineering — How Figma's multiplayer technology works. A practical account of applying CRDT concepts to production collaborative editing, including difficult product semantics such as multiplayer undo and redo.
-
Yjs Documentation — Offline Support. Describes combining Yjs with IndexedDB persistence so local changes survive offline operation and synchronize when network providers become available.
-
Yjs Documentation — y-websocket. Demonstrates the distinction between transport connectivity and document synchronization state.
-
Yjs Documentation — Awareness. Documents ephemeral collaboration state such as peer presence and mechanisms for representing clients becoming unavailable.
-
MDN — Introduction to WebRTC protocols. Explains ICE, STUN, NAT traversal, and TURN relay behavior underlying practical WebRTC connectivity.
-
Cloudflare Engineering — Real-Time Communications at Scale. Discusses real-world WebRTC connectivity challenges, TURN, NAT and firewall behavior, and the limits of peer-to-peer mesh topologies.
