Beyond the WebSocket: Scaling Real-Time Collaboration in Expo with CRDTs
Stop relying on 'Last Write Wins' and fragile socket events. I explore how CRDTs enable robust local-first, multi-user experiences in React Native and Expo.

For years, my go-to for real-time collaboration was simple: spin up a Socket.io server, emit an event when a user types, and pray that the database transaction finishes before the next user hits 'Save'.
But as we started building more complex, multi-user canvases and editors in Expo, the cracks in the 'Centralized Truth' model became impossible to ignore. Race conditions, the dreaded 'Last Write Wins' data loss, and the nightmare of handling offline sync made me realize we were doing it wrong.
If you're building collaborative software today, you need to go beyond the WebSocket. You need CRDTs (Conflict-free Replicated Data Types).
The Problem with the 'Socket-Only' Approach
When you rely purely on WebSockets to broadcast state, you're essentially building a thin wrapper over a synchronous system. If User A and User B edit the same paragraph simultaneously, the server has to play referee.
In a mobile environment like Expo, this is even more dangerous. High-latency 5G connections and intermittent tunnel-fade mean users are 'offline' for 2-3 seconds at a time. A standard WebSocket approach fails here because it assumes constant connectivity. When the user reconnects, how do you merge their 5-second-old changes with the current state without nuking someone else's work?
Enter CRDTs: Local-First by Design
CRDTs are data structures that can be updated independently and concurrently without a central coordinator. They guarantee that if two nodes have received the same set of updates (even in a different order), they will arrive at the same state.
In my experience, moving to a CRDT-based architecture shifted our mindset from 'Sending updates to a server' to 'Syncing a local database'. This is the core of Local-First software.
The Tech Stack: Expo + Yjs + Hocuspocus
For a recent project, we integrated Yjs (a high-performance CRDT library) into an Expo app. Here is the architecture we landed on:
- Frontend (Expo/React Native): Yjs manages the shared document state.
- Persistence:
expo-sqliteto store the binary 'update blocks' locally so the app works instantly on cold start. - Sync: A WebSocket provider (we used Hocuspocus) that simply pipes binary updates between peers.
Implementing a Collaborative Hook
Here’s a simplified version of how I structured the shared state hook. Instead of useState, we use the CRDT as the source of truth.
The Breakthrough: Handling Binary Updates in Expo
One hurdle I encountered was performance. CRDTs like Yjs communicate via Uint8Array binary updates. Standard JSON-over-WebSocket is too heavy for this. By using the Hocuspocus provider, we were able to keep the payload size tiny, which is critical for mobile battery life and data usage.
Furthermore, we utilized y-protocols/awareness to handle 'who is currently in the doc' states (cursors, presence) without ever touching a database. This 'ephemeral state' lives only in memory and syncs across peers automatically.
Why This Matters for Your Career
As senior engineers, we shouldn't just be building apps that work; we should be building apps that are resilient. Moving to CRDTs in our Expo apps allowed us to:
- Eliminate 'Save' buttons. Everything is reactive and collaborative.
- Native Offline Support. Users can edit on a plane, and when they land, the merge happens mathematically—no conflict dialogs needed.
- Reduced Server Load. The server becomes a 'dumb' relay rather than an expensive compute engine for state reconciliation.
Wrapping Up
WebSockets are a transport layer, not a concurrency strategy. If you're building the next Figma for mobile or a collaborative task manager in Expo, stop fighting the 'Last Write Wins' battle. Embrace the math of CRDTs, leverage the performance of Yjs, and give your users an experience that feels truly native and instantaneous.
In my next post, I'll dive into optimizing expo-sqlite for CRDT persistence—because keeping that data on-device is the final piece of the local-first puzzle.