Case: Real-Time Collab App
Problem
Design a collaborative document editor like Google Docs / Notion. Multiple users edit same doc concurrently. Presence (who's online, cursors). Changes propagate within ~100ms. Persistent history.
Walkthrough
Clarify
- Up to 50 concurrent editors per doc.
- Up to 100K active docs.
- Tens of thousands of concurrent users globally.
- Conflict resolution: ops merge automatically (CRDT or OT).
- Offline editing → sync on reconnect.
- Persistent history: undo/redo + audit.
High-level architecture
[Client (Browser SPA)]
│ WebSocket (SignalR or Y.js native)
▼
[Collab Hub — ASP.NET Core / SignalR]
│
├──→ [Redis backplane / Azure SignalR for fan-out across hub instances]
├──→ [Doc state store: Cosmos DB or Postgres + JSONB]
└──→ [Operations log / event store: Cosmos change feed or Kafka]
[Background sync: persist debounced state]
CRDT vs OT
- OT (Operational Transformation): ops transformed against concurrent ops; central server typical. Google Docs uses.
- CRDT (Conflict-free Replicated Data Type): math-based; ops merge regardless of order. Y.js is the standard CRDT lib for collab.
For 2026 greenfield: CRDT (Y.js / Automerge) — simpler; offline-friendly; no central server logic.
Y.js + .NET backend
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('wss://collab/ws', 'doc-123', ydoc);
const ytext = ydoc.getText('content');
Backend (Y.js doesn't dictate language; you can stand up a relay):
app.MapHub<CollabHub>("/collab");
public class CollabHub : Hub
{
public Task UpdateDoc(string docId, byte[] update)
{
// Broadcast to all clients in this doc; persist update event
return Clients.OthersInGroup(docId).SendAsync("Update", update);
}
}
Or use existing Y.js relay servers. .NET hub primarily handles auth + persistence.
Presence
public class CollabHub(IPresenceTracker p) : Hub
{
public async Task JoinDoc(string docId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, docId);
await p.AddAsync(docId, Context.UserIdentifier!);
await Clients.Group(docId).SendAsync("Presence", await p.ListAsync(docId));
}
public override async Task OnDisconnectedAsync(Exception? ex)
{
// remove from all docs
}
}
Cursor positions: ephemeral; broadcast at high frequency; not persisted.
Persistence
Strategy:
- Stream operations to Kafka/Cosmos changefeed.
- Background worker debounces (1s) → persists snapshot to DB.
- Client connects → reads snapshot + replays recent ops since snapshot.
Snapshots avoid replaying thousands of ops on cold start.
Scaling SignalR
[Client] → [Front Door / LB] → [SignalR pod 1, 2, 3, ...]
│
▼
[Azure SignalR Service or Redis backplane]
Azure SignalR offloads connections — your app emits messages; Azure handles the per-user connections.
Auth
- JWT via query string on WebSocket upgrade.
- Per-doc authorization: claim or per-doc ACL check on Join.
- Optimistic local edits; server enforces auth on persist.
Offline editing
Y.js stores changes locally; reconnects sync. CRDT merges seamlessly.
Scalability math
50 concurrent / doc * 100K active docs = 5M connections (not all simultaneous).
Realistic: 50K concurrent connections.
SignalR Standard: 1K connections per server. Need 50 servers.
Or Azure SignalR Service: scales automatically.
Cost
- Azure SignalR Service: ~$1.50 per unit per day; one unit = 1K concurrent.
- Cosmos DB for state: RUs depend on edit rate.
- Background workers cheap.
Latency
Client applies optimistically; rollback on conflict (rare with CRDT).
Failure modes
- Server crash: clients reconnect; SignalR auto-reconnect; state from DB + recent ops.
- Network partition: clients edit locally; sync on reconnect via CRDT merge.
- DB outage: in-flight edits buffered in Redis / queue; persist when DB recovers.
History / undo
Op log persisted. Undo: client sends inverse op (CRDT).
Audit
Trade-offs
| Choice | Why | Trade-off |
|---|---|---|
| CRDT (Y.js) | Offline; merge-friendly | Larger state per doc |
| Azure SignalR | Managed scale | Cost per connection |
| Cosmos changefeed | Operations log | Eventual consistency |
| Snapshots | Fast cold start | Coordinate snapshot+ops |
What we'd skip
- Heavy OT machinery: CRDT is simpler.
- Strong consistency: not needed for edits.
What we'd add for higher scale
- Multi-region replication of doc state (Cosmos multi-master).
- Smarter snapshot scheduling (LRU; warm-cache).
- CDN for read-only docs.