Building Real-Time Collaboration in a Web DAW


Building Real-Time Collaboration in a Web DAW

β€œCollaborative editing isn’t about preventing conflicts. It’s about making them mathematically impossible.”


The Pain Point

You’re jamming with a friend. You’re both editing the same project. They move a MIDI clip. You adjust a fader. The DAW has to reconcile these changes somehow.

Traditional approaches?

  • Lock-based editing: One person edits at a time. Lame.
  • Last-write-wins: Someone’s changes always get clobbered. Painful.
  • Operational Transform (OT): Complex, server-dependent, fragile.

We needed something better.


The Revelation: CRDTs + WebRTC + SpacetimeDB

Enter Conflict-free Replicated Data Types (CRDTs). Specifically, Yjs - a JavaScript CRDT library that makes real-time collaboration feel like magic.

The stack:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Next.js Frontend             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Yjs (CRDT state management)    β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚  β”‚  β”‚  WebRTC (P2P sync)      β”‚   β”‚   β”‚
β”‚  β”‚  β”‚  or WebSocket fallback  β”‚   β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        SpacetimeDB Signaling           β”‚
β”‚  (Connection brokering, presence)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

How Yjs Works (The 30-Second Version)

CRDTs guarantee that if two users make concurrent edits, their states will eventually converge to the same result. No server reconciliation needed.

Example: Two producers add MIDI notes at the same timestamp:

// Producer A's machine
yDoc.getArray('midiClips').push([{ note: 60, time: 0.5 }])

// Producer B's machine  
yDoc.getArray('midiClips').push([{ note: 64, time: 0.5 }])

// Both states converge to: [{ note: 60, ... }, { note: 64, ... }]
// Order is deterministic based on unique IDs, not timing

The CRDT handles the β€œwhat happened first” problem for you.


WebRTC: Going Peer-to-Peer

Yjs can sync over WebSocket (centralized) or WebRTC (P2P). For Bolt, we went P2P for latency:

// Initialize WebRTC provider
const provider = new WebrtcProvider('bolt-room-123', yDoc, {
  signaling: ['wss://signal.bolt.xyz'],
  password: null,
  awareness: new awarenessProtocol.Awareness(yDoc),
  maxConns: 20 + Math.floor(Math.random() * 15),
  filterBcConns: true,
  peerOpts: {}
})

// Presence awareness (who's online, what are they doing)
provider.awareness.setLocalStateField('user', {
  name: 'b0gie',
  color: '#00ffaa',
  cursor: { track: 3, position: 16.5 }
})

WebRTC means data flows directly between browsers. No server bottleneck. Sub-50ms sync for most operations.


SpacetimeDB: The Signaling Layer

WebRTC needs a way to find peers. That’s where SpacetimeDB comes in:

  • Connection brokering: Helps peers discover each other
  • Fallback signaling: When direct P2P fails, routes through WebSocket
  • Presence: Who’s in the room, what are they working on
  • Persistence: Project state backed to disk
// SpacetimeDB reducer for joining a session
#[reducer]
pub fn join_session(ctx: &ReducerContext, session_id: String, user_id: String) {
    let presence = Presence {
        session_id,
        user_id,
        joined_at: ctx.timestamp,
        last_seen: ctx.timestamp,
    };
    ctx.db.presence().insert(presence);
    
    // Broadcast to all connected clients
    ctx.emit(Event::UserJoined { user_id });
}

SpacetimeDB is WebAssembly-based and stupid fast. Perfect for real-time apps.


The Video Portal Layer

Collaboration isn’t just about shared state. It’s about presence. That’s why Bolt has Video Portals - peer-to-peer video streaming alongside the DAW:

// SimplePeer for WebRTC video
const peer = new SimplePeer({
  initiator: isInitiator,
  stream: localStream,
  trickle: false
})

peer.on('stream', (remoteStream) => {
  videoElement.srcObject = remoteStream
})

Same WebRTC infrastructure, different use case. Face-to-face collaboration while producing.


Conflict Resolution in Practice

Here’s what happens when two producers edit the same clip:

  1. Producer A moves clip from bar 4 to bar 8
  2. Producer B (offline/busy) deletes the clip entirely
  3. Sync happens

With CRDTs: The delete wins (it’s an operation, not a state). But the fact that Producer A moved it is also preserved in history. You can β€œundo” the delete and the clip reappears at bar 8.

Traditional systems: Last write wins, someone’s work disappears. Oops.


Pro Tips for Real-Time Collaboration

  1. Granularity matters: Sync at the clip/parameter level, not the whole project
  2. Awareness is UX: Show where everyone’s cursor is, what they’re editing
  3. Offline support: CRDTs handle intermittent connectivity gracefully
  4. Binary state: Yjs supports binary data (audio buffers!) via Uint8Array
  5. Garbage collection: CRDTs grow over time. Periodically snapshot and reset.

The Code That Makes It Work

// Bolt's collaboration hook
export function useCollaboration(projectId: string) {
  const [ydoc] = useState(() => new Y.Doc())
  const [provider, setProvider] = useState<WebrtcProvider | null>(null)
  
  useEffect(() => {
    // Connect to WebRTC room
    const rtcProvider = new WebrtcProvider(
      `bolt-${projectId}`,
      ydoc,
      { signaling: ['wss://signal.bolt.xyz'] }
    )
    
    // Also connect to SpacetimeDB for persistence
    const spacetime = new SpacetimeClient(
      process.env.NEXT_PUBLIC_SPACETIMEDB_URI!,
      process.env.NEXT_PUBLIC_SPACETIMEDB_NAME!
    )
    
    setProvider(rtcProvider)
    
    return () => {
      rtcProvider.destroy()
      spacetime.disconnect()
    }
  }, [projectId, ydoc])
  
  return { ydoc, provider }
}

What’s Next

  • Operational transform fallback: For when CRDTs hit edge cases
  • Version history: Full git-like branching for projects
  • Conflict UI: Let users manually resolve true conflicts
  • Mobile sync: Handle intermittent mobile connectivity

Cleetus Speaks

β€œbrother b0gie, so you’re saying i can make sick beats WITH my friends at the SAME TIME??

and if i delete their track by accident it comes BACK??

this is like the opposite of when i β€˜accidentally’ deleted all those training logs from the facility

…what do you mean β€˜that was different’

#Yjs #WebRTC #CRDTsAreMagic #CollaborativeBeats”


Real-time collaboration isn’t magic. It’s just very good math. And WebRTC. And maybe a little bit of magic.

β€” b0gie