Skip to main content

Realtime Collaboration

Songbird supports realtime collaboration — multiple users can work on the same project simultaneously. Changes merge automatically using structured, music-aware merge strategies that understand the .bird file format, mixer state, and plugin state.

How It Works

Collaboration is built on three layers:
  1. Collab Server — A lightweight Node.js WebSocket server that manages project rooms, syncs file changes, and merges concurrent edits
  2. React Client — A WebSocket client in the React UI that auto-pushes changes on every commit and applies remote changes
  3. Rust State Sync — The songbird-state collab modules (collab.rs, collab_state_sync.rs, collab_mixer_sync.rs) handle file reading for push, and write + reload remote files with echo suppression via the sync engine’s guard flags
┌──────────────┐         ┌──────────────┐
│  Songbird A  │         │  Songbird B  │
│  (User 1)    │         │  (User 2)    │
└──────┬───────┘         └──────┬───────┘
       │  WebSocket              │  WebSocket
       ▼                         ▼
┌─────────────────────────────────────────┐
│         Collaboration Server            │
│                                         │
│  ┌─────────────┐  ┌──────────────────┐  │
│  │  Room Mgmt  │  │  Merge Engine    │  │
│  │  (invite    │  │  (.bird-aware)   │  │
│  │   codes)    │  │  (.json-aware)   │  │
│  └─────────────┘  └──────────────────┘  │
│                                         │
│  ┌─────────────┐  ┌──────────────────┐  │
│  │  Presence   │  │  File State      │  │
│  │  & Cursors  │  │  (per-project)   │  │
│  └─────────────┘  └──────────────────┘  │
└─────────────────────────────────────────┘

Quick Start

1. Start the Server

# On your server (or locally for testing)
./utils/collab-server.sh

# Custom port
PORT=9000 ./utils/collab-server.sh --port 9000
The server starts on ws://localhost:8765 by default.

2. Create a Room

In Songbird, open the Collaboration panel. Enter the server URL, your name, and click Connect. Then click Create Room — you’ll receive an invite code (e.g., a1b2c3d4).

3. Invite a Collaborator

Share the invite code. The other user connects to the same server and clicks Join Room with the invite code. They receive the current project files automatically.

4. Collaborate

Changes sync automatically:
  • Edit notes, arrangements, or structure in .bird → auto-synced
  • Move faders, pan knobs, toggle mute/solo → auto-synced
  • Tweak plugin parameters → auto-synced

Sync Protocol

Message Types

DirectionTypeDescription
Client → ServercreateCreate a new room (returns invite code)
Client → ServerjoinJoin a room with invite code
Client → ServerpushPush file changes after a commit
Client → ServerpullRequest current file state
Client → ServerpresenceSend cursor/tool position
Client → ServerstartTunnelStart a Cloudflare tunnel to expose the server port
Client → ServerstopTunnelStop the active tunnel
Client → ServermixerUpdateReal-time mixer change (vol/pan/mute/solo)
Client → ServercursorMoveFigma-style cursor position update
Server → ClientcreatedRoom created confirmation + invite code
Server → ClientjoinedJoined room + current files + peer list
Server → ClientupdateRemote peer pushed changes (merged)
Server → ClientpushAckPush acknowledged (may include merge result)
Server → ClientpresenceRemote peer cursor update
Server → ClientpeerJoined / peerLeftPeer connectivity
Server → ClienttunnelStartedTunnel active — includes public URL
Server → ClienttunnelStoppedTunnel shut down
Server → ClientmixerUpdateRemote peer changed mixer state
Server → ClientcursorMoveRemote peer cursor position

Sync Flow

User A edits kick pattern in verse
  → commitAndNotify("[mixer] kick pattern")
  → historyChanged event fires in React
  → collabSocket auto-push (100ms delay)
  → collabGetProjectFiles (reads .bird + .state.json + .edit.json from Rust)
  → WebSocket send → server

Server receives push:
  → Three-way merge (base vs current state vs incoming)
  → .bird: track/section-level merge
  → .json: per-property deep merge
  → Broadcast merged result to User B

User B receives update:
  → collabApplyRemoteFiles (writes files via Rust backend)
  → Rust reloads changed files (same path as undo/redo)
  → undo_redo_in_progress guard prevents echo commits
  → Local git commit records the remote change
  → historyChanged updates the UI

Intelligent Merge

The server doesn’t do naive line-by-line text merge. It understands Songbird’s file formats.

.bird File Merge

The merge engine parses .bird files into a structured tree:
bird file
├── sig (bpm, key, scale)
├── tracks (global definitions)
├── arr (section order + bar counts)
└── sections
    ├── sec intro
    │   ├── trk 1 kick: {p, v, n, t, sw, ...}
    │   └── trk 2 bass: {p, v, n, t, sw, ...}
    └── sec verse
        ├── trk 1 kick: {p, v, n, t, sw, ...}
        └── trk 2 bass: {p, v, n, t, sw, ...}
Merging happens at the track-per-section level:
ScenarioResult
User A edits trk 1 kick in sec verse, User B edits trk 2 bass in sec verse✅ Auto-merge (different tracks)
User A edits trk 1 in sec intro, User B edits trk 1 in sec verse✅ Auto-merge (different sections)
User A adds a new section, User B adds a different section✅ Auto-merge (both appended)
User A changes BPM, User B edits notes✅ Both applied (independent blocks)
User A and B both edit trk 1 kick in sec verse🔄 Last-write-wins

JSON File Merge (daw.state.json, daw.edit.json)

JSON files are merged with recursive deep-merge at the property level:
ScenarioResult
A changes track 1 volume, B changes track 2 pan✅ Auto-merge
A mutes track 3, B solos track 3✅ Both applied (different properties)
A and B both change track 1 volume🔄 Last-write-wins

Conflict Resolution

All conflicts use last-write-wins — the most recent push takes precedence. This is intentionally simple: users naturally learn to avoid stepping on each other’s toes, and the granularity (track + section level) means conflicts are rare in practice.

Presence

The server broadcasts real-time presence updates:
  • Who’s online — list of connected peers
  • Cursor position — which section/track/tool each user is working with
  • Activity feed — human-readable log of remote changes
Presence includes Figma-style cursor overlays: each peer’s mouse position is rendered as a colored arrow with their name label in the arrangement view, piano roll, and mixer. Cursors fade after 3 seconds of inactivity and are removed after 10 seconds.

Tunnel (Share Session)

The collab server has a built-in Cloudflare Tunnel integration via the untun npm package. This lets you expose the collab server to the internet with a single click — no manual Cloudflare/Tailscale setup, no port forwarding.
  1. Click Share Session (Public Tunnel) in the Collaboration panel
  2. The server spawns a Cloudflare Tunnel on the collab port (default 8765)
  3. A public https:// URL is generated and displayed — share it with collaborators
  4. Remote peers connect to the tunnel URL instead of localhost
  5. Click Stop tunnel when done
The tunnel is TLS-encrypted in transit and uses Cloudflare’s free tier (no account required).

Optimistic Mixer Sync

Mixer fader changes (volume, pan, mute, solo) are sent optimistically over the WebSocket in real time, bypassing the git commit cycle. This provides ~5-20ms latency for continuous controls like fader drags, compared to ~100-300ms via the commit path.
  • Outbound: Local changes are throttled to ~30Hz and sent as mixerUpdate messages
  • Inbound: Remote changes are applied directly to the Zustand store
  • Local lock: While the user is dragging a slider, remote updates for that control are ignored to prevent jitter
  • Source of truth: The git commit layer remains authoritative. Optimistic updates are just a fast preview.

Deployment

The collab server is a single Node.js process with no external dependencies (no database, no Redis). It can run on any machine with Node.js 18+.

Local Development

./utils/collab-server.sh

Production (Ubuntu Server)

# Install Node.js if needed
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Clone and install
cd /opt/songbird-collab
npm install

# Run with systemd or pm2
PORT=8765 node server.js

Audio Assets

Audio samples and recordings are not synced via the collab server. For large audio assets, use a cloud storage bucket (GCS or AWS S3) with Git LFS.

Key Files

FilePurpose
server/server.jsWebSocket server — room management, push/pull sync, presence, merge orchestration
server/merge/bird-merge.js.bird parser, serializer, and three-way structured merge
server/merge/json-merge.jsRecursive deep-merge for JSON state files
server/test-merge.jsMerge engine test suite (7 tests)
react_ui/src/services/collabSocket.tsWebSocket client — connect, push/pull, auto-push on commit
react_ui/src/data/slices/collab.tsZustand store — connection state, peers, activity feed
react_ui/src/components/CollabPanel.tsxUI — connect dialog, room create/join, peer list, tunnel controls
react_ui/src/components/CollabCursors.tsxFigma-style peer cursor overlay component + tracking hook
react_ui/src/services/collabMixerSync.tsOptimistic mixer sync — local lock, throttled broadcast, inbound apply
rust/crates/songbird-state/src/collab.rsRust collab core — state sync for push/pull
rust/crates/songbird-state/src/collab_state_sync.rsCollab state synchronization
rust/crates/songbird-state/src/collab_mixer_sync.rsOptimistic mixer sync for collaboration
utils/collab-server.shServer launch script

Design Decisions

  • WebSocket from React — JavaScript has native WebSocket support. Running the WebSocket client in React reuses the existing Tauri IPC bridge for file I/O.
  • File-level sync, not git-object sync — Sending full file contents (not git objects/patches) is simpler and the files are small text. Each client maintains its own local git history for undo/redo independently.
  • Server-side merge — The server runs the structured merge engine. This is simpler than client-side merge and ensures a single source of truth. The tradeoff is offline editing doesn’t auto-merge (but local git undo/redo still works fine).
  • Last-write-wins — Simple, predictable, and users rarely conflict at the track/section level in practice. No complex conflict resolution UI needed.
  • Echo suppression on apply — When applying remote files, the undo_redo_in_progress guard flag is set (same mechanism as undo/redo) to prevent echo-pushing the same changes back to the server.