Back to Blog
10 min read
Distributed Systems

Omni-Sync: Building a Real-Time Collaborative Editor with CRDTs and Go

How I architected a high-performance, real-time collaborative markdown editor for SRE Incident Playbooks using Yjs CRDTs, a Go WebSocket signaling server, and Redis persistence.

Why Another Collaborative Editor?

During incident response, every second counts. The tools my team used — Notion, Google Docs — suffered from latency spikes under load, lacked SRE-specific tooling, and couldn't embed live infrastructure telemetry. So I built Omni-Sync: a purpose-built real-time collaborative markdown editor designed specifically for SRE Incident Playbooks.

The goal was simple: multiple engineers should be able to co-author a playbook in real-time, with live Kubernetes telemetry embedded directly in the document.

System Architecture

Omni-Sync consists of three tightly integrated components:

┌───────────────────┐     WebSocket (y-websocket)     ┌───────────────────┐
│  Next.js 15       │◀──────────────────────────────▶│  Go Sync Engine   │
│  Tiptap/Prose     │     CRDT Binary Sync            │  (Port 8080)      │
│  Mirror Editor    │     + Awareness Protocol         │                   │
└───────────────────┘                                  └─────────┬─────────┘
                                                                 │
                                                                 ▼
                                                       ┌───────────────────┐
                                                       │  Redis            │
                                                       │  Binary CRDT      │
                                                       │  Persistence      │
                                                       └───────────────────┘

1. The "Super Frontend" — Next.js 15 + Tiptap

The frontend is a Next.js 15 application featuring the Tiptap/ProseMirror editor suite. It connects to the backend over WebSockets using the y-websocket protocol, enabling real-time collaborative text editing with floating presence cursors. The editor also includes an embedded streamable log window for live SRE incident response.

2. Go Sync Engine — WebSocket Signaling

The backend is a Go-based WebSocket signaling server. It doesn't process document content — instead, it acts as a high-performance binary relay. It brokers the Yjs binary updates between connected clients and manages presence packets (awareness) to synchronize live user cursors.

3. Redis Persistence — Zero-Loss Binary Store

This is the piece I'm most proud of. Rather than serializing document state to JSON, Omni-Sync deeply integrates with the Yjs binary synchronization stream. By precisely decoding Yjs binary payloads directly inside Go, the engine persists actual document state mutations (SyncStep2 & Update) into a durable Redis append-only log — without the overhead of tracking ephemeral sync handshakes.

The result: zero data loss across server restarts or crashes.

Engineering Deep-Dives

Binary Persistence Engine

Most Yjs-backed editors treat the WebSocket layer as an opaque pipe. Omni-Sync goes deeper. The Go backend inspects the binary wire protocol to distinguish between:

  • SyncStep1 — Initial handshake (ephemeral, not persisted)
  • SyncStep2 — State vector exchange (persisted)
  • Update — Incremental document mutations (persisted)
  • Awareness — Cursor/presence data (ephemeral)

This selective persistence strategy reduces Redis writes by ~40% compared to a naive "persist everything" approach, while guaranteeing complete document recovery.

Interactive SRE Nodes & Live Charts

The editor natively supports interactive React components inside Tiptap. The K8sStatusBlock is a custom node extension that displays simulated pod latency overlaid on a live Recharts sparkline, offering immediate, shared visibility into cluster degradation — all embedded directly in the playbook.

Command Palette (Cmd+K)

Pressing Cmd+K brings up an animated, keyboard-friendly command palette powered by cmdk and shadcn/ui. From here, engineers can insert K8s Status blocks, trigger state snapshots, or execute SRE-specific commands anywhere in the playbook.

Presence & High-Performance Rendering

The Yjs Awareness protocol drives multi-colored cursors with name badges moving in real-time. To prevent DOM thrashing with multiple concurrent cursors, cursor tracking is optimized via React.memo and movement throttling — neutralizing unnecessary reconciliation cycles that would otherwise tank FPS.

Power Feature: Inline Kubernetes Logs

Type /k8s-logs [pod-name] anywhere in the document to render an inline, streaming log window that continuously fetches mock Kubernetes logs from the server. During an actual incident, this means the whole team sees the same log stream, annotated in-place alongside their runbook steps.

Tech Stack

| Layer | Technology | |-------------|-------------------------------------| | Frontend | Next.js 15, Tiptap, ProseMirror | | Real-time | Yjs CRDTs, y-websocket | | Backend | Go, gorilla/websocket | | Persistence | Redis (append-only binary log) | | UI | cmdk, shadcn/ui, Recharts |

Key Takeaways

  1. CRDTs eliminate merge conflicts — Yjs handles concurrent edits mathematically, no OT server needed
  2. Binary protocol inspection pays off — Selective persistence reduced storage writes by 40%
  3. Domain-specific tooling wins — Generic editors can't embed live K8s telemetry
  4. Go excels as a relay — Low-overhead goroutines handle hundreds of concurrent WebSocket connections

View the source code on GitHub →