Skip to main content

Overview

Plant Together enables seamless real-time collaboration using Yjs CRDT (Conflict-free Replicated Data Type) technology and WebSocket connections. Multiple users can edit diagrams simultaneously with instant synchronization and automatic conflict resolution.

How Collaboration Works

Architecture

Plant Together’s collaboration system consists of three main components:
1

Yjs Document (Y.Doc)

A shared data structure that holds the diagram content. Each document in a room has its own Yjs document.
// Source: umlEditor.component.tsx:134
const doc = new Y.Doc()
const type = doc.getText('monaco') // Shared text type
2

WebSocket Provider

Maintains real-time connection to the server and synchronizes changes between all connected clients.
// Source: umlEditor.component.tsx:136
const provider = new WebsocketProvider(
  serverWsUrl,       // WebSocket server URL
  wsID,              // Room + Document ID
  doc                // Yjs document
)
3

Monaco Binding

Connects the Monaco code editor to the Yjs document, enabling real-time text synchronization.
// Source: umlEditor.component.tsx:163-169
const binding = new MonacoBinding(
  type,                        // Y.Text type
  editor.getModel()!,         // Monaco editor model
  new Set([editor]),          // Set of editors
  provider.awareness          // Awareness protocol
)
All changes are transmitted as binary update messages, making synchronization extremely efficient even for large diagrams.

Real-time Editing Features

Instant Synchronization

Every keystroke is synchronized in real-time:
  1. User types in the editor
  2. Monaco binding detects the change
  3. Yjs generates an update message
  4. WebSocket sends the update to the server
  5. Server broadcasts to all connected clients
  6. Other clients apply the update to their local document
  7. Monaco updates their editor view
All of this happens in milliseconds.

Seeing Other Users

User Awareness

Plant Together shows you who else is in the room and where they’re editing: Features:
  • Colored cursors show each user’s position
  • Username labels appear on hover
  • Selection highlighting shows what text others have selected
  • Unique colors automatically assigned to each user
// Source: umlEditor.component.tsx:139-142
provider.awareness.setLocalStateField('user', {
  name: username,      // Display name or generated username
  color: userColor     // Generated from username hash
})

Username Generation

Users who are logged in see their account display name:
// Source: umlEditor.component.tsx:45
const username = userContext.context?.displayName
Example: “Sarah Chen”, “alex.dev”

Color Assignment

Each user gets a unique color generated from their username:
// Source: userIdentity.utils.ts
generateColorFromString(username: string)
// Returns consistent color like: "#3B82F6", "#10B981"
Colors are deterministic - the same username always gets the same color across sessions.

Document Management

Multiple Documents Per Room

Each room can contain multiple documents, allowing you to organize related diagrams:
1

Create a Document

Click the + New Document button in the sidebar.
// Source: collabRoom.page.tsx:76-83
const createNewDocument = async (roomId: string, documentName: string) => {
  await plantService.createDocumentInRoom(
    socket!,
    documentName,
    ({ id }) => {
      setRoomDocuments(docs => [...docs, { id, name: documentName }])
      setCurrDocument({ id, name: documentName })
    }
  )
}
2

Switch Between Documents

Click any document name in the sidebar to switch to it. Your WebSocket connection will automatically reconnect to the new document.
// Source: umlEditor.component.tsx:51-53
useEffect(() => {
  setWsID(`${roomId}${currDocument.id}`) // Update connection ID
}, [currDocument, roomId])
3

Rename Documents

Double-click a document name to edit it inline.
// Source: collabRoom.page.tsx:92-108
const updateDocument = async (documentId: string, newName: string) => {
  await plantService.updateDocumentInRoom(
    socket!,
    documentId,
    newName,
    ({ documentName }) => {
      // Update local state
      const updatedDoc = roomDocuments.find(doc => doc.id === documentId)
      updatedDoc!.name = documentName
    }
  )
}
4

Delete Documents

Right-click and select delete (or use the delete icon).
Deleting a document is permanent and will affect all users in the room.

Document Synchronization Events

All document operations are broadcast to other users:
// Source: collabRoom.page.tsx:151-157
socket?.on('/document', ({ code, documentName, id }) => {
  if (code != 200) {
    alert('Unable to update new document')
  }
  setRoomDocuments(docs => [...docs, { id, name: documentName }])
})

Offline Sync and Conflict Resolution

How Offline Sync Works

Plant Together uses Yjs CRDTs which automatically handle conflicts and enable offline editing.
  1. Connection lost - WebSocket disconnects
  2. Continue editing - Changes are stored locally in the Yjs document
  3. Connection restored - Provider automatically reconnects
  4. Sync happens - Local changes are sent to server
  5. Conflicts resolved - Yjs merges changes intelligently
Your work is never lost during temporary disconnections.

Connection States

Indicator: Cursor updates appear instantlyBehavior:
  • All changes sync in real-time
  • Other users visible with cursors
  • Full collaboration features enabled
Indicator: Brief moment when switching documentsBehavior:
  • Editor remains functional
  • Changes queued for sync
  • Usually resolves in less than 500ms
Indicator: Other users’ cursors disappearBehavior:
  • Editor still works normally
  • Changes stored locally
  • Auto-reconnect attempts every few seconds
  • All changes will sync when reconnected
If disconnected for extended periods, ensure you don’t close the browser tab to preserve your changes.

Best Practices for Collaboration

Communicate Changes

Use external chat (Slack, Discord) to coordinate major changes:
  • “Refactoring the class diagram now”
  • “Adding new sequence diagram to Doc3”
  • “Can everyone review before I merge?”

Organize Documents

Create separate documents for different diagram types:
  • architecture-overview
  • api-sequence-diagrams
  • database-schema
  • deployment-diagram

Avoid Simultaneous Major Edits

While Yjs handles conflicts well, coordinate on large refactors:
  • Assign different documents to different people
  • Take turns for major restructuring
  • Use comments in the diagram code

Save Regularly

Although auto-sync is reliable, periodically export important work:
  • Use the export feature
  • Keep local backups of critical diagrams
  • Export before major changes

Troubleshooting

Possible causes:
  • Network connectivity issues
  • WebSocket server temporarily down
  • Browser tab in background (throttled)
Solutions:
  1. Check your internet connection
  2. Refresh the page to reconnect
  3. Ensure browser isn’t throttling the tab
  4. Check browser console for WebSocket errors
Possible causes:
  • They’re viewing a different document
  • Awareness state not synchronized yet
  • Browser cache issues
Solutions:
  1. Confirm you’re on the same document
  2. Wait a few seconds for awareness to sync
  3. Refresh the page (you won’t lose work)
Possible causes:
  • Very large diagram (>1000 lines)
  • Slow network connection
  • Many simultaneous users (10+)
Solutions:
  1. Split large diagrams into multiple documents
  2. Check network speed and latency
  3. Close other bandwidth-intensive applications
  4. Consider breaking up the room for team sub-groups
Cause: Document was deleted by another userSolution: You’ll automatically switch to the first available document. If all documents were deleted, create a new one.

Technical Details

WebSocket Connection

// Source: collabRoom.page.tsx:137-144
const authToken = await plantService.retrieveToken()

const newSocket = io(serverHttpUrl, {
  extraHeaders: {
    'room-id': roomId,
    Authorization: `Bearer ${authToken}`,
  },
})

Yjs Document Lifecycle

// Source: umlEditor.component.tsx:122-178
const setBinding = useCallback(() => {
  // Clean up previous connection
  if (providerRef.current) {
    providerRef.current.destroy()
    bindingRef.current?.destroy()
  }

  // Create new document and provider
  const doc = new Y.Doc()
  const provider = new WebsocketProvider(serverWsUrl, wsID, doc)
  
  // Set user awareness
  provider.awareness.setLocalStateField('user', {
    name: username,
    color: userColor.current,
  })

  // Listen for awareness changes (other users joining/leaving)
  provider.awareness.on('change', () => {
    const statesArray = Array.from(provider.awareness.getStates())
    updateClientStyleSheets(statesArray)
  })

  // Bind to Monaco editor
  const type = doc.getText('monaco')
  if (editorRef.current) {
    const binding = new MonacoBinding(
      type,
      editorRef.current.getModel()!,
      new Set([editorRef.current]),
      provider.awareness,
    )
    bindingRef.current = binding
  }

  // Cleanup function
  return () => {
    provider.awareness.destroy()
    provider.destroy()
    bindingRef.current?.destroy()
  }
}, [wsID, username])

Next Steps

Export Your Work

Learn how to export diagrams in multiple formats

Private Room Access

Control who can access your collaborative rooms

API Documentation

Integrate document management into your apps

Architecture

Understand the technical architecture

Build docs developers (and LLMs) love