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:
- Producer A moves clip from bar 4 to bar 8
- Producer B (offline/busy) deletes the clip entirely
- 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
- Granularity matters: Sync at the clip/parameter level, not the whole project
- Awareness is UX: Show where everyoneβs cursor is, what theyβre editing
- Offline support: CRDTs handle intermittent connectivity gracefully
- Binary state: Yjs supports binary data (audio buffers!) via Uint8Array
- 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