openbrain
Concepts

Collaboration

How real-time multi-user editing works in flow.

Collaboration

flow supports real-time multi-user editing. Multiple users can work on the same workflow simultaneously, with changes syncing instantly via Yjs CRDT and Hocuspocus WebSocket server.

Architecture

┌─────────────┐         ┌──────────────────┐         ┌─────────────┐
│  Browser A  │ ◄─ ws ─►│   Hocuspocus     │◄─ ws ──►│  Browser B  │
│             │         │   Collab Server   │         │             │
│  Yjs Doc    │         │                  │         │  Yjs Doc    │
│  (replica)  │         │   Yjs Doc        │         │  (replica)  │
└─────────────┘         │   (authority)    │         └─────────────┘
                        └────────┬─────────┘

                           persistence

                        ┌────────┴─────────┐
                        │     MongoDB      │
                        │                  │
                        │  - Yjs binary    │
                        │  - JSON nodes    │
                        │  - JSON edges    │
                        └──────────────────┘

Each browser client maintains a local replica of the Yjs document. The Hocuspocus server acts as the central authority, merging changes from all clients and broadcasting updates. MongoDB stores the persisted document state.

What is synced where

DataStorageSyncLifecycle
Node positionsYjs Y.MapDurable, real-timePersisted across sessions, synced between all users
Node configYjs Y.MapDurable, real-timePersisted across sessions, synced between all users
Node handleCountsYjs Y.MapDurable, real-timePersisted across sessions, synced between all users
Node previewIndexYjs Y.MapDurable, real-timePersisted across sessions, synced between all users
EdgesYjs Y.ArrayDurable, real-timePersisted across sessions, synced between all users
ViewportYjs Y.MapDurable, real-timePersisted across sessions, synced between all users
activeRunIdYjs runtime Y.MapDurable, real-timeRuntime state, cleared when run completes
runsVersionYjs runtime Y.MapDurable, real-timeCounter that triggers run data fetches
selectedReact stateEphemeral, per-clientEach user has their own selection
measuredReact stateEphemeral, per-clientReact Flow node dimensions, local only
_output, _inputsReact stateEphemeral, per-clientDerived by computeDerived(), local only
_runs (NodeRun records)MongoDBOn-demand fetchFetched when runsVersion changes

Yjs document structure

The Yjs document for a workflow contains three top-level shared types:

// Node data — keyed by node ID
const yNodes: Y.Map<Y.Map<any>>  // doc.getMap("nodes")

// Edge list
const yEdges: Y.Array<object>     // doc.getArray("edges")

// Runtime state (activeRunId, runsVersion)
const yRuntime: Y.Map<any>        // doc.getMap("runtime")

Each node in yNodes is itself a Y.Map containing the node's position, data, and metadata. This structure allows fine-grained conflict resolution — if User A moves a node while User B changes its config, both changes merge cleanly.

Persistence

The collab server persists the Yjs document to MongoDB using two mechanisms:

  1. Debounced flush — after any change, the server waits 100ms for additional changes, then writes the Yjs binary state to the database. This batches rapid edits into a single write.

  2. Explicit flush — before workflow execution, the webapp calls flushCollabState to force an immediate write. This ensures the executor reads the latest state.

In addition to the Yjs binary, the collab server writes denormalized JSON representations of nodes and edges to the database. The executor reads these JSON fields directly instead of decoding the Yjs binary.

Conflict resolution

Yjs uses a CRDT (Conflict-free Replicated Data Type) algorithm for conflict resolution. When two users make concurrent changes:

  • Different fields on the same node — both changes are preserved (e.g. User A changes the prompt while User B moves the node)
  • Same field on the same node — the last write wins, determined by Yjs's internal clock
  • Adding/removing edges — operations commute, so concurrent edge additions both succeed
  • Adding/removing nodes — operations commute, so concurrent node additions both succeed

In practice, conflicts are rare because users typically work on different parts of the graph.

Scaling with Redis

A single Hocuspocus instance can handle many concurrent WebSocket connections. If you need multiple collab server instances (for redundancy or geographic distribution), configure REDIS_URL on all instances.

The Hocuspocus Redis extension uses Redis pub/sub to synchronize document changes across instances:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  Instance A │ ──► │    Redis     │ ◄── │  Instance B │
│  (US-East)  │ ◄── │   Pub/Sub   │ ──► │  (EU-West)  │
└─────────────┘     └─────────────┘     └─────────────┘

Each instance publishes its document updates to a Redis channel, and subscribes to updates from other instances. This ensures all instances maintain consistent document state regardless of which instance a client connects to.

On this page