Skip to Content

Multiplayer

Node-based UIs are often visual and explorative in nature. For collaborative features that usually means there’s no satisfying “middle-ground” like sparsely syncing state between users, because it would break visual consistency or make collaborative exploration more complicated. Often a full live multiplayer system needs to be put in place. This guide explores best practices for adding realtime multiplayer collaboration to a Vue Flow application with a sync engine such as Yjs .

What are multiplayer applications?

Although realtime collaboration would be the correct term, we refer to it as multiplayer, which was most likely popularized in this context by Figma. Besides being more intuitive, it also does a better job of communicating the commitment to something truly realtime by putting it in the same category as competitive video games.

To figure out what degree of collaboration your application supports, look at how closely it resembles a multiplayer game. Those features likely include:

  • No manual saving is required; all users share a world where actions are persisted.
  • Any change made by another user is immediately visible to you.
  • Not only completed actions but transitive states are synced too (dragging a node, not only dropping it).
  • You can see other users (their cursors or viewport positions).

A word of caution

Making multiplayer applications is hard. When users collaboratively edit shared objects, conflicts, delays, connection drops, and concurrent operations are all expected. The hardest challenge is conflict resolution: you need a sync engine that can merge changes coming from the server (or other clients) into the (often optimistic) client state, handle conflicts and disconnects gracefully, and provide a reliable network layer.

The simplest sync engine would be “first come, first served”, with failed requests ignored. Try to handle this by hand and a wide variety of edge cases emerge: networks are slow, clients disconnect, and the order of actions matters.

Local-first and CRDTs

Depending on how far you take conflict and disconnect handling, you quickly land in local-first territory. When your sync engine lets a user stay disconnected for an indefinite amount of time, you have a local-first application. For a good primer, we recommend Ink & Switch’s essay  that coined the term.

A CRDT (Conflict-free Replicated Data Type) is a data structure that lets multiple users edit the same data simultaneously on different devices, then automatically merges all changes without conflicts — no central server needed. Rather than storing only the current value, CRDTs keep the operation history on every connected client, so any replica can merge changes into the same final state regardless of the order updates arrive. A couple of popular solutions are Yjs , Automerge , and Loro .

What Vue Flow state should I sync?

Among your first considerations is how you want to sync what parts of your app’s local state.

Ephemeral vs durable

There are many ways to sync state between clients, but they fall into two groups: ephemeral and durable (atomic) changes. Ephemeral changes are not persisted, and neither their consistency nor their correctness is very important — things like cursor or viewport positions, or transient interaction state. Missing some of these updates won’t break anything; after a connection loss a client can just listen for the newest changes and restore any lost state.

Updates to the nodes and edges, on the other hand, should be persistent and consistent. Imagine Alice deletes a node and Bob misses the update — now if Bob moves that node, the application state is inconsistent. This kind of data needs a solution that handles disconnects more aggressively and can discard impossible actions like moving a deleted node.

At the core of a multiplayer Vue Flow app is syncing the nodes and edges. But not all of their state should be synced: which nodes and edges are selected should differ per client, and some fields are computed or transient (dragging, resizing, measured). Here’s an overview of what to sync and what not to.

Nodes

FieldDurableEphemeralExplanation
id✅❌Important, always needs to be in sync
type✅❌Important, always needs to be in sync
data✅❌Important, always needs to be in sync
position✅✳️Important, includes transient state
width, height✅✳️Important, includes transient state
dragging❌✅Transient interaction state
resizing❌✅Transient interaction state
selected❌❌Per-user UI state
measured❌❌Computed from the DOM

Edges

FieldDurableEphemeralExplanation
id✅❌Important, always needs to be in sync
type✅❌Important, always needs to be in sync
data✅❌Important, always needs to be in sync
source✅❌Important, always needs to be in sync
target✅❌Important, always needs to be in sync
sourceHandle✅❌Important, always needs to be in sync
targetHandle✅❌Important, always needs to be in sync
selected❌❌Per-user UI state

Connections and cursors

Good examples of ephemeral data are connections (the transient part of an edge being created) and cursors.

Sharing each user’s connection status improves visual consistency across clients, but losing it on disconnect doesn’t affect the correctness of the shared data — it’s purely presentational. The same goes for cursors: they help create the immersion of a shared workspace, but they aren’t essential to the app’s integrity. As long as the nodes and edges stay in sync, cursor state can be safely discarded.

Both can be shared as purely ephemeral state. To reduce the frequency of live updates you can debounce them, and smooth other users’ cursor movements with a library like perfect-cursors .

Third-party libraries and services

There’s no one-size-fits-all backend. The choice between CRDT-based local-first libraries (like Yjs  and Jazz ) and server-authoritative approaches (like Liveblocks , Supabase , and Convex ) shapes how you build:

  • CRDT libraries make offline-first multiplayer easier, with automatic conflict resolution for free. But if your app is also structured around a database, you’ll need to write your own adapters between the CRDT library and the database.
  • Server-authoritative solutions are easier to pair with a database and classical application logic, but complicate offline-first support, conflict resolution, and development.
NameArchitectureOffline supportConflict resolutionNotes
YjsCRDT (local-first)✅ Full offline supportAutomatic (CRDT)Collaborative editing, offline-first CRDT for local-first apps.
JazzCRDT (local-first)✅ Full offline supportAutomatic (CRDT)A full, offline-first database solution that handles multiplayer natively.
LiveblocksServer-authoritative, CRDT-like🟡 Partial offline supportUses CRDTs under the hoodReal-time collaboration platform with an open-source backend.
SupabaseServer-authoritative❌ Requires connectionManual / RLS policiesClassic SQL-based database with realtime support, but no CRDT (resolve conflicts manually).
ConvexServer-authoritative❌ Requires connectionOptimisticTypeScript-first database with real-time queries and a sync engine.
AutomergeCRDT (local-first)✅ Full offline supportAutomatic (CRDT)The canonical CRDT library; can be paired with a database like Convex.
LoroCRDT (local-first)✅ Full offline supportAutomatic (CRDT)A fast CRDT library in Rust and WebAssembly with history tracking and version control.
Last updated on