← Blog

Local-first · Distributed Systems · Collaboration

What Does “Correct Reconnection” Mean in a Local-First Collaborative Application?

Reconnection in a local-first collaborative system means more than restoring a socket. It requires preserving offline work, merging replicas, rebuilding presence, revalidating access, and knowing when the application can honestly say it is synced.

Aashaya Aryal

April 14, 2025 · 25 min read

Updated September 25, 2026

Dark installation filled with suspended illuminated screens and reflective structures scattered through a black space.

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:

Diagram

Request

Response

Browser

Server

Database

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:

Diagram

Device B

Device A

Sync when available

Application

Local State

Application

Local State

The user edits the local replica first. Synchronization happens afterward.

That distinction changes what a network outage means. Instead of:

TEXT
Editing unavailable

the system can behave more like:

TEXT
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:

TS
peerConnection.connectionState === "connected";

Can the application immediately show this?

TEXT
✓ 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:

Diagram

Application Semantics

Shared Document State

Replication / CRDT Protocol

Transport

Network Path

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:

TEXT
online
offline

Something closer to this represents reality better:

TEXT
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:

TEXT
const retries = 3;

Their connection disappears. Alice changes the retry count:

TEXT
3 -> 5

while Bob independently adds:

TEXT
const timeout = 5000;

There are now two legitimate replicas evolving at the same time:

Diagram
BobNetworkAliceBobNetworkAliceNetwork partition beginsBoth edits exist locallyReplicas convergeInitial synchronized stateDisconnectedDisconnectedretries = 5add timeout = 5000Connection restoredExchange missing updates

A snapshot-oriented design might frame the recovery problem as:

TEXT
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:

TEXT
If replicas eventually receive the same set of updates,
they should eventually represent the same document.

That is a much stronger model than:

TEXT
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:

TS
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:

TEXT
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:

TEXT
merge(
    local changes,
    remote changes
)

which can yield:

TEXT
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:

TS
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:

TEXT
IndexedDB
    ↓
  Y.Doc

Network updates can populate it:

TEXT
Peer
 ↓
Y.Doc

And the editor itself modifies it:

TEXT
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:

TS
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:

Diagram

No

Yes

Network appears available

Reconnect signaling

Discover current peers

Establish transport

Exchange document state

Converged?

Mark document synced

Refresh ephemeral presence

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:

TEXT
Browser A <------> Browser B

The real network is closer to:

Diagram

discover path

discover path

Direct if possible

Fallback

Browser A

NAT / Firewall

STUN

TURN Relay

NAT / Firewall

Browser B

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:

TEXT
Peer A ───── direct UDP ───── Peer B

while today's reconnect uses:

TEXT
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:

TS
type CollaborationState =
  | "offline"
  | "connecting"
  | "connected"
  | "synchronizing"
  | "synced";

and respond separately to transport and synchronization events:

TS
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:

TEXT
communication is possible

Synchronization means something closer to:

TEXT
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:

TEXT
code
board nodes
text
settings
files
document metadata

Ephemeral state

State that describes what a participant is doing right now:

TEXT
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:

Diagram

Ephemeral

Durable

Shared Document

IndexedDB

Awareness / Presence

Remote Peer

On reconnect:

TEXT
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:

  1. The user edits a document.
  2. The changes persist locally.
  3. The laptop closes.
  4. The application is reopened later without internet.
  5. The browser starts.
  6. IndexedDB begins hydrating the document.

Now the initialization path becomes part of the local-first correctness model.

This is dangerous:

TS
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:

Diagram

Application Starts

Create CRDT Document

Attach Local Persistence

Load Persisted State

Render Usable Workspace

Attach Network Provider

Merge Remote State

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:

TEXT
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:

TEXT
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:

TEXT
signaling
authentication
authorization
peer discovery
TURN
session metadata
key exchange
monitoring

Pure peer-to-peer mesh networking also introduces topology limits.

With three peers:

TEXT
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:

TEXT
P2P may be an excellent fit.

For hundreds of simultaneous participants:

TEXT
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:

TEXT
CRDT + WebRTC

today and later move toward:

TEXT
CRDT + WebSocket server

or:

TEXT
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:

TEXT
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:

TEXT
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:

ScenarioExpected invariant
Peer disconnects while idleReconnect without document change
Peer edits offlineLocal edits survive
Both peers edit offlineReplicas converge after reconnect
Browser refreshes offlineLocal document reloads
Duplicate CRDT updates arriveFinal state does not duplicate content
Signaling reconnects slowlyLocal editing remains usable
Direct WebRTC failsTURN can establish connectivity
Peer reconnects with stale presenceOld awareness state disappears
One peer rejoins after long absenceMissing document state synchronizes
Network repeatedly flapsNo lost edits or listener duplication
Two tabs open the same documentCross-tab state remains consistent
Access is revoked during partitionReconnection 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:

TEXT
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.

TEXT
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.

TEXT
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

TEXT
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:

TEXT
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:

TEXT
Browser A <------> Browser B

and instead treat them as several cooperating systems:

Diagram

Device B

Device A

CRDT Updates

fallback

fallback

UI

CRDT Replica

Local Persistence

Presence

UI

CRDT Replica

Local Persistence

Presence

Signaling Service

TURN Relay

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:

TEXT
WebRTC connectionState === "connected"

It is not:

TEXT
navigator.onLine === true

And it is not merely:

TEXT
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:

TS
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.