Real-time collaboration architecture from frontend perspective: WebSocket gateway with sticky sessions by docId, stateful Yjs doc actor tier handling CRDT merge, separate Redis pub-sub for ephemeral awareness/cursors, IndexedDB-backed offline ops queue, snapshot persistence to Postgres+S3. Four scenarios: presence cursor broadcast (15 Hz throttle + interpolation), CRDT concurrent merge (commutative ops, no transform), offline reconnect with vector-clock catchup, multi-cursor with server-side ACL denial and client rollback. Includes ADR comparing Liveblocks managed vs Yjs+Hocuspocus self-host vs PartyKit edge.
Совместное редактирование стало базовым ожиданием для современных продуктов: документы, доски, дизайн-файлы, таблицы, заметки, IDE, roadmap, incident rooms. Пользователь ожидает видеть чужие курсоры, печатать одновременно, продолжать работу при кратком disconnect, не терять изменения и получать понятный rollback, если нет прав. За этим UX стоит сложная архитектура: persistent transport, локальная модель, operation log, conflict resolution, presence, permissions, persistence и reconnect.
Обычный realtime chat значительно проще: сообщения append-only, порядок почти линейный, конфликтов мало. Collab editor сложнее, потому что два пользователя могут изменить одну позицию текста, один блок, один объект canvas или одну ячейку таблицы одновременно. Если просто отправлять весь document state на каждое изменение, система быстро ломается по bandwidth, latency и lost updates. Поэтому реальные системы обмениваются операциями или CRDT updates, а не полным состоянием.
Знать эту тему важно и frontend, и backend инженерам. Frontend должен делать optimistic local edits и не ждать сети. Backend должен валидировать каждую операцию, обеспечивать ordering или convergence, сохранять snapshots и не хранить ephemeral presence в основной БД.
Каждый клиент держит локальную копию документа. Пользовательское действие превращается в операцию: insert text, delete range, move object, set property, add block. Операция применяется локально сразу, записывается в local queue, отправляется через WebSocket, проходит server validation, merge и broadcast. Другие клиенты применяют операцию к своим локальным копиям. После доставки всех операций все реплики должны сойтись к одному состоянию.
Есть две главные семьи алгоритмов. OT, Operational Transform, трансформирует операции относительно concurrent операций и обычно требует центральный сервер для total ordering. CRDT, Conflict-free Replicated Data Types, проектирует структуры так, чтобы операции коммутировали и реплики сходились даже при разном порядке доставки. OT исторически силен в текстовых редакторах вроде Google Docs. CRDT популярен в JS-экосистеме через Yjs, Automerge, Loro и похожие библиотеки.
Presence - отдельный слой. Курсоры, selections, avatars и "user is typing" ephemeral: их не надо сохранять в Postgres как документ. Они идут через awareness channel, Redis pub-sub или managed presence service, throttled и с interpolation на клиенте.
Диаграмма показывает два браузера и серверную середину. В каждом браузере есть editor с Yjs Y.Doc, IndexedDB offline ops queue и awareness state. Оба клиента соединяются с WebSocket gateway. Gateway делает auth и ACL checks, маршрутизирует операции по docId к stateful doc actor. Doc actor держит authoritative Y.Doc, op log и vector clock. Persistence слой сохраняет snapshots в Postgres/S3. Awareness fan-out идет отдельно через Redis pub-sub.
Ключевая идея: физические соединения не равны бизнес-направлениям ответа. Клиент подключается к gateway, gateway к doc actor, doc actor к storage и Redis. Broadcast remote operations и presence frames идут по тем же соединениям в обратном направлении. Не нужно моделировать отдельные reverse edges для ответов; важен устойчивый канал и ownership документа.
Диаграмма также показывает sticky routing by docId. Если операции одного документа попадают на разные stateless workers без общей ordering/merge модели, появляются race conditions. Stateful actor на документ упрощает merge и snapshots. Альтернатива - CRDT gossip/mesh или distributed log, но для большинства web products actor per document проще понять и эксплуатировать.
Первый сценарий: presence cursor broadcast. User A двигает курсор. Клиент обновляет local awareness на 60 fps, но по сети отправляет throttled frames примерно 10-15 Hz. Gateway публикует их в Redis channel, другие gateway pods получают fan-out, User B рисует чужой cursor с interpolation. Урок: presence должен быть быстрым, дешевым и ephemeral; сохранять каждый cursor move в БД бессмысленно.
Второй сценарий: concurrent CRDT merge. User A и User B одновременно вставляют текст в одну логическую позицию. Оба клиента применяют свои edits локально без ожидания. Операции приходят на doc actor в произвольном порядке. Yjs применяет CRDT update, результат сходится у всех клиентов. Урок: perceived latency нулевая, а consistency обеспечивается структурой данных, а не надеждой на быстрый сервер.
Третий сценарий: offline reconnect. User A теряет WebSocket, продолжает печатать, операции копятся в IndexedDB. После reconnect клиент отправляет state vector: какие ops у него уже есть. Server/doc actor вычисляет diff, отправляет missing remote ops, затем принимает buffered local ops. Урок: reconnect - это протокол catchup, а не просто "открыть сокет заново".
Четвертый сценарий: permission denial. User B optimistic удаляет read-only block. Клиент обязан показать мгновенный UI, но сервер обязан валидировать каждую операцию по ACL. Doc actor отклоняет op, клиент делает rollback только своей локальной операции и показывает понятное сообщение. Урок: collaborative UI не отменяет server authority. Клиенту нельзя доверять ordering, permissions или shape операции.
Liveblocks vs Yjs/Hocuspocus self-host vs PartyKit. Managed Liveblocks быстро дает presence, storage, auth hooks, Yjs adapter и низкую операционную сложность. Это хороший выбор для раннего продукта и малой команды. Минусы: vendor lock-in, цена по MAU/usage, ограниченный контроль над persistence и data residency. Yjs плюс Hocuspocus self-host дешевле на масштабе и дает полный контроль, но команда владеет WebSocket scaling, sticky sessions, snapshots, Redis, monitoring и on-call. PartyKit/Cloudflare Durable Objects естественно моделирует actor per document на edge, снижает latency и упрощает stickiness, но привязывает архитектуру к Cloudflare limits и programming model.
OT vs CRDT. OT может быть компактнее для текста и хорошо работает с central ordering, но реализовать его корректно трудно. Edge cases с concurrent insert/delete, undo, composition и rich text сложны. CRDT проще использовать через готовые библиотеки и лучше для offline/peer-to-peer, но имеет overhead: tombstones, metadata, compaction, larger document updates. Практичный совет: не писать OT или CRDT с нуля для production. Используйте Yjs, Automerge, Loro, ShareDB или managed stack.
Stateful actor vs stateless gateway. Stateless gateway легко масштабировать горизонтально, но кто-то должен владеть состоянием документа и порядком операций. Actor per docId упрощает mental model: один процесс применяет ops и snapshots. Минус - hot documents, memory pressure, resharding, actor recovery. Для популярных документов нужны limits, backpressure, snapshot compaction и возможно split по subdocuments.
Snapshot plus op log vs full state writes. Полный state на каждое изменение прост, но дорог и теряет granular history. Op log сохраняет историю и позволяет catchup, но растет бесконечно. Поэтому production обычно делает snapshots every N ops or seconds, хранит recent op log, compact старые операции и умеет восстановить actor после crash.
Awareness persistence trade-off прост: не сохраняйте ephemeral presence в durable storage. Но presence должен иметь TTL, heartbeat и cleanup, иначе пользователи будут видеть "висящие" курсоры. Network frequency тоже trade-off: 60 Hz по сети плавно, но дорого; 10-15 Hz с interpolation обычно достаточно.
Google Docs исторически ассоциируется с OT и central coordination. Figma описывала свой multiplayer подход как custom LiveGraph/CRDT-like модель для design objects, где важны свойства объектов, parent pointers и LWW для некоторых полей. Notion мыслит документ как tree of blocks и решает sync на уровне блоков и операций. Linear использует sync engine и server reconciliation ближе к Replicache-модели: это не чистый CRDT, но дает мгновенный UX и strong server logic.
Excalidraw и tldraw показывают разные варианты canvas collaboration. Yjs часто используется в rich text editors через ProseMirror/Tiptap integrations. Liveblocks популярен как быстрый путь к presence and collaborative storage. VS Code Live Share показывает, что real-time collaboration может быть не только документом, но и средой разработки.
Для CloudArch эта тема связана с ::concept{slug="offline-first"}: offline queue и reconnect становятся частью collaborative protocol. Связана с ::concept{slug="websocket"}: транспорт должен иметь heartbeat, backoff, auth refresh и backpressure. Связана с ::concept{slug="crdts"}: именно там объясняется математическая гарантия convergence.
Передавать полный document state на каждую клавишу - не масштабируется и создает lost update race. Доверять клиенту permissions - критическая уязвимость: каждый op должен валидироваться сервером. Хранить cursors в Postgres - лишняя нагрузка и stale presence. Делать global undo - пользователь откатывает чужие изменения; undo должен быть per-user или origin-aware.
WebSocket без heartbeat и reconnect backoff приводит к "мертвым" соединениям. Reconnect без state vector заставляет скачивать весь документ. Отсутствие backpressure позволяет одному hot document положить gateway. Отсутствие snapshot compaction раздувает op log. Нет idempotency/op ids - retry дублирует операции.
Frontend ошибки: ждать server ack перед локальным изменением, из-за чего typing lag заметен; не показывать offline state; rollback чужих операций вместе со своими; auto-resolve опасные ACL errors без сообщения; presence updates слишком частые и рвут CPU; не throttling selection events; не тестировать IME/composition input для CJK языков.
Процессная ошибка - писать свой CRDT "за пару недель". Рабочий demo с двумя пользователями не покрывает undo, delete ranges, rich text marks, nested objects, reconnect после месяца offline, schema migrations, malicious clients и partial permissions.
Не каждую live-фичу надо строить как full collaboration engine. Chat, notifications, stock ticker и live dashboard часто append-only или read-only; им достаточно WebSocket/SSE plus server state. Если пользователи не редактируют один и тот же объект одновременно, CRDT может быть лишним.
Не используйте managed realtime storage для данных с жесткими compliance или data residency требованиями, если провайдер не закрывает контракт. Не используйте peer-to-peer WebRTC как единственный путь для enterprise collab: NAT, firewalls, moderation, persistence и permissions часто требуют server relay/authority.
Не включайте offline collaborative editing для операций, которые нельзя безопасно смержить: финансовое утверждение, юридическая подпись, destructive admin action. Можно дать draft offline, но commit должен пройти серверную проверку.
Начните с работ Мартина Клеппмана по local-first software и CRDT, документации Yjs и Automerge, статьи Figma "How Figma's multiplayer technology works", материалов Linear про sync engine, ShareDB для OT и Hocuspocus для Yjs server. Для практики сравните Liveblocks, PartyKit, Yjs, Automerge, Loro и Replicache. Внутри курса продолжайте через ::concept{slug="offline-first"}, ::concept{slug="crdts"}, ::concept{slug="websocket"} и кейсы ::case{slug="figma-multiplayer"}, ::case{slug="google-docs"}, ::case{slug="notion-blocks"}, если они доступны.