How We Built Real-Time Collaboration

Q
Quixli Team
March 15, 20258 min read

How We Built Real-Time Collaboration

Building real-time collaboration for a document editor is one of the hardest problems in software engineering. Handling concurrent edits, network failures, and conflict resolution at scale requires careful architectural decisions. Here is how we did it.

The Challenge

Real-time collaboration presents several interconnected challenges:

  • Concurrent edits — Multiple users typing in the same paragraph simultaneously

  • Conflict resolution — Determining the correct state when edits overlap

  • Network resilience — Handling offline users who reconnect with stale data

  • Performance — Keeping the editor responsive with 10+ simultaneous editors

  • Persistence — Saving document state reliably without data loss

Our Technology Stack

After evaluating Operational Transform (OT) and CRDTs, we chose Yjs for several reasons:

  • CRDTs over OT — CRDTs guarantee eventual consistency without a central server, making them more resilient than OT.

  • Yjs — A mature, battle-tested CRDT implementation with excellent ProseMirror integration.

  • Hocuspocus — A WebSocket server purpose-built for Yjs that handles connection management, authentication, and persistence.

Architecture Overview

The collaboration system has three main components:

  1. Client (TipTap + y-prosemirror) — The editor uses y-prosemirror to bind Yjs documents to the ProseMirror state

  1. WebSocket Server (Hocuspocus) — Attached to the NestJS HTTP server, handles real-time sync and broadcasts changes to all connected clients

  1. Persistence Layer (PostgreSQL) — Yjs document state is stored as binary data in the Page model, ensuring no data is lost even if the server restarts

Authentication & Authorization

Security was non-negotiable. Every WebSocket connection goes through:

  1. JWT token validation on WebSocket upgrade

  1. Page ownership verification

  1. Share-based access control checking the full hierarchy: page shares, folder shares, and collection shares

  1. Permission level enforcement (viewer vs editor)

// Simplified Hocuspocus authentication hook
onAuthenticate({ token, documentName }) {
const user = verifyJwtToken(token);
const pageId = extractPageId(documentName);
const access = await checkAccess(user.id, pageId);

if (!access.canEdit) {
throw new Error('Unauthorized');
}

return { user, access };
}

Auto-Versioning

During active collaboration, we automatically create document versions every 5 minutes. This provides a safety net — if something goes wrong, users can browse and restore previous states.

Lessons Learned

  • CRDTs are not magic — While they handle concurrent edits gracefully, you still need to think carefully about how document state is structured.

  • Test with real latency — Development on localhost hides timing issues. Always test collaboration with simulated network latency.

  • Binary state is efficient — Storing Yjs state as binary (rather than JSON) reduces storage by 60-80% and speeds up sync.

  • Graceful degradation matters — When the WebSocket disconnects, the editor should continue working offline and sync when reconnected.




Building real-time collaboration was one of the most rewarding engineering challenges we have tackled. The result is an editing experience that feels seamless — multiple people working together as if they are sitting at the same desk.