Return

OpenOT: A Case Study in Type-Agnostic Operational Transformation

The headless OT engine that doesn't assume your deployment.

Executive Summary

OpenOT is a production-ready Operational Transformation (OT) framework that solves a critical problem in modern web development: building real-time collaborative applications without vendor lock-in. Unlike monolithic solutions that bundle the editor, database, and network layer into a black box, OpenOT provides the raw synchronization primitives developers need while letting them choose their own stack.

This case study examines OpenOT's architecture, real-world applications, technical implementation, and the design decisions that make it uniquely flexible in the collaborative editing space.

The Problem Space

Why Building Real-Time Collaboration is Hard

Real-time collaborative applications face four fundamental challenges:

  1. Concurrency: When two users edit the same document simultaneously, their changes can conflict
  2. Latency: Network delays can make UIs feel unresponsive if not handled properly
  3. Offline Support: Users expect applications to work without internet connectivity
  4. Consistency: All clients must eventually converge to the exact same state

Traditional approaches fall into two camps:

Monolithic SaaS Solutions (e.g., Yjs, ShareDB with fixed backends):

Low-Level CRDT/OT Libraries:

OpenOT occupies the sweet spot: production-ready synchronization with architectural freedom.

Solution Architecture

The Three-Layer Design

OpenOT is architected as three decoupled packages, allowing developers to use only what they need:

1. Core (@open-ot/core)

The heart of the framework, defining the OTType interface:

1export interface OTType<Snapshot, Op> {
2  name: string;
3  create(): Snapshot;
4  apply(snapshot: Snapshot, op: Op): Snapshot;
5  transform(opA: Op, opB: Op, side: "left" | "right"): Op;
6  compose(opA: Op, opB: Op): Op;
7}

Key Insight: If you can implement these four functions for your data structure, OpenOT can synchronize it. This abstraction enables collaboration on:

2. Client (@open-ot/client)

Implements the standard OT state machine with three states:

The Magic: Optimistic UI updates happen instantly while background synchronization handles network delays transparently.

1client.applyLocal(op); // Updates local state immediately
2// ... background magic handles sending, buffering, and rebasing ...

3. Server (@open-ot/server)

A lightweight coordinator that doesn't care about your database. It uses an IBackendAdapter interface for persistence:

1interface IBackendAdapter {
2  getRecord(docId: string): Promise<DocumentRecord>;
3  saveOperation(docId: string, op: Op, revision: number): Promise<void>;
4  getHistory(docId: string, from: number, to: number): Promise<Op[]>;
5}

Bring Your Own Database: Implement this simple interface to use Redis, Postgres, MongoDB, DynamoDB, Cloudflare Durable Objects, or any other storage.

Technical Deep Dive

How Operational Transformation Works

OT solves the "diamond problem" of concurrent edits. When two users modify the same document simultaneously, their operations must be transformed to apply correctly regardless of arrival order.

The Diamond Problem Visualized:

The Transformation Property:

S ∘ OpA ∘ T(OpB, OpA) ≡ S ∘ OpB ∘ T(OpA, OpB)

This mathematical property ensures convergence: no matter which order operations arrive, all clients end up with identical state.

Text Operations: The Built-in Type

OpenOT's TextType uses the industry-standard Retain/Insert/Delete format:

1type TextOperation = Array<
2  | { r: number } // Retain n characters
3  | { i: string } // Insert string
4  | { d: number } // Delete n characters
5>;

Example Transformation:

1import { TextType } from "@open-ot/core";
2
3const doc = "Hello World";
4
5// User A: Insert "Beautiful " after "Hello "
6const opA = [{ r: 6 }, { i: "Beautiful " }, { r: 5 }];
7
8// User B: Replace "World" with "Universe"
9const opB = [{ r: 6 }, { d: 5 }, { i: "Universe" }];
10
11// Transform A to apply after B
12const opA_prime = TextType.transform(opA, opB, "left");
13
14// Result: "Hello Beautiful Universe"

Key Features:

Storage Model: Log + Snapshot

OpenOT uses a hybrid storage approach inspired by database write-ahead logs (WAL):

Operation Log (Source of Truth):

[Op₀, Op₁, Op₂, ..., Opₙ]

Every operation is appended to an ordered log, enabling:

Snapshots (Performance Optimization):

Snapshot @ Rev 0
Snapshot @ Rev 100
Snapshot @ Rev 200

Replaying 10,000 operations is slow, so OpenOT periodically saves full document state:

Example with Redis Adapter:

1import { RedisAdapter } from "@open-ot/adapter-redis";
2
3const adapter = new RedisAdapter("redis://localhost:6379");
4
5// Fetches snapshot at Rev 500 + ops 501-505
6const doc = await adapter.getRecord("doc-id");

Real-World Use Cases

Case Study 1: Next.js Serverless Collaboration

Challenge: Build a collaborative document editor deployable to Vercel (serverless) without persistent WebSocket connections.

Solution: OpenOT's HybridTransport with Redis Pub/Sub

1// Client automatically switches between SSE and polling
2const transport = new HybridTransport({
3  docId: "demo-doc",
4  baseUrl: "/api/ot",
5  inactivityTimeout: 2 * 60 * 1000, // Switch to polling after 2min
6  pollingInterval: 5000,
7});
8
9const client = new OTClient({
10  type: TextType,
11  transport: transport,
12});

How It Works:

  1. Active Users: Real-time updates via Server-Sent Events
  2. Connection Timeouts: Automatic fallback to polling
  3. Inactive Users: Switches to polling after 2 minutes to save resources
  4. Redis Pub/Sub: Broadcasts operations across all serverless instances

Results:

Case Study 2: React Native Mobile Collaboration

Challenge: Build a mobile note-taking app with offline-first editing and sync.

Solution: OpenOT client with no transport during offline mode

1import { useOTClient } from "@open-ot/react";
2import { TextType } from "@open-ot/core";
3import NetInfo from "@react-native-community/netinfo";
4
5function NotesEditor() {
6  const transport = useMemo(() => {
7    // Only connect when online
8    return isOnline ? new WebSocketTransport("wss://api.app.com") : null;
9  }, [isOnline]);
10
11  const { client, snapshot } = useOTClient({
12    type: TextType,
13    initialSnapshot: cachedNote,
14    transport: transport, // null when offline
15  });
16
17  // Works seamlessly offline!
18  const handleEdit = (newText) => {
19    client.applyLocal(generateOp(snapshot, newText));
20  };
21}

Behavior:

Case Study 3: Cloudflare Durable Objects at the Edge

Challenge: Deploy collaborative editing at the edge with minimal latency.

Solution: OpenOT server inside a Durable Object

1export class CollaborativeDoc {
2  constructor(state, env) {
3    this.storage = state.storage;
4    this.server = new Server(new DurableObjectAdapter(this.storage));
5    this.server.registerType(TextType);
6  }
7
8  async fetch(request) {
9    const { pathname } = new URL(request.url);
10
11    if (pathname === "/ws") {
12      // WebSocket connection for real-time sync
13      const [client, server] = Object.values(new WebSocketPair());
14      this.handleWebSocket(server);
15      return new Response(null, { status: 101, webSocket: client });
16    }
17  }
18}

Benefits:

Integration Patterns

Pattern 1: Hybrid Transport for Universal Deployment

1import { HybridTransport } from "@open-ot/transport-http-sse";
2
3const transport = new HybridTransport({
4  docId: "shared-doc",
5  baseUrl: "/api/ot",
6  // Adapts to environment automatically
7});

When to Use:

Pattern 2: Pure WebSocket for Real-Time Apps

1import { WebSocketTransport } from "@open-ot/transport-websocket";
2
3const transport = new WebSocketTransport("wss://api.app.com/ot");

When to Use:

Pattern 3: Custom Transport for Unique Requirements

1class WebRTCTransport implements TransportAdapter {
2  async connect(onReceive) {
3    this.peer = new RTCPeerConnection();
4    this.channel = this.peer.createDataChannel("ot");
5    this.channel.onmessage = (e) => onReceive(JSON.parse(e.data));
6  }
7
8  async send(msg) {
9    this.channel.send(JSON.stringify(msg));
10  }
11}

When to Use:

Performance & Scalability

Benchmarks

Operation Throughput (TextType):

(Tested on M1 MacBook Pro with v8 engine)

Network Efficiency:

Horizontal Scaling with Redis

How It Works:

  1. User connects to any server instance
  2. Instance subscribes to document's Redis channel
  3. When operation arrives, it's saved to Redis and broadcast
  4. All connected instances receive the operation via Pub/Sub
  5. Instances push updates to their connected clients

Capacity: A single Redis instance can handle:

Comparison with Alternatives

Feature OpenOT Yjs (CRDT) ShareDB Firepad
Type System Pluggable (Text, JSON, Custom) Fixed (CRDT types) Pluggable (OT types) Text only
Database Choice Any (via adapter) IndexedDB/Memory MongoDB/Postgres Firebase only
Network Layer Pluggable WebSocket/WebRTC WebSocket Firebase Realtime DB
Serverless Support ✅ Yes (HybridTransport) ❌ Requires persistent connections ❌ Requires persistent connections ✅ Yes (Firebase)
Offline-First ✅ Yes ✅ Yes ✅ Yes ✅ Yes
React Integration @open-ot/react hooks Community packages Community packages None
TypeScript ✅ Native ✅ Native ⚠️ Types via DefinitelyTyped ❌ No
Bundle Size ~15KB (core + client) ~65KB ~40KB ~50KB
Learning Curve Medium High (CRDT concepts) Medium Low
Vendor Lock-in ❌ None ❌ None ❌ None ✅ Firebase required

Why Choose OpenOT?

Choose OpenOT if you:

Choose Yjs if you:

Choose ShareDB if you:

Production Deployment Guide

Recommended Architecture

Platform-Specific Guides

1. Railway (Recommended for Beginners)

1# 1. Create Railway project with Redis plugin
2railway init
3
4# 2. Add Redis plugin in Railway dashboard
5# 3. Deploy
6railway up

Environment Variables:

1REDIS_URL=redis://default:password@redis.railway.internal:6379

Cost: ~$10/month (includes Redis + app hosting)

2. Vercel + Redis Cloud (Serverless)

1# 1. Sign up for Redis Cloud (Free tier: 30MB)
2# 2. Deploy to Vercel
3vercel deploy

Important: Use HybridTransport for automatic polling fallback due to connection timeouts.

Cost: Free (Vercel Hobby + Redis Cloud free tier)

3. Cloudflare Workers + Durable Objects (Edge)

1export class OTDoc {
2  constructor(state, env) {
3    const adapter = new DurableObjectAdapter(state.storage);
4    this.server = new Server(adapter);
5  }
6}

Benefits:

Cost: $5/month + $0.15 per million requests

Key Takeaways

What Makes OpenOT Unique

  1. Architectural Freedom: Truly bring-your-own for database, network, and editor
  2. Type Agnostic: Synchronize any data structure, not just text
  3. Production-Ready: Battle-tested state machine handles edge cases
  4. Deployment Flexible: Works in serverless, edge, and traditional environments
  5. Developer Experience: TypeScript-first with React hooks

When to Use OpenOT

Good Fit:

Not the Best Fit:

Conclusion

OpenOT represents a new approach to building collaborative applications: providing the hard synchronization logic while respecting your architectural choices. By decoupling the OT engine from storage, transport, and data types, it enables developers to:

For teams building real-time collaboration into existing applications—or greenfield projects that need architectural flexibility—OpenOT offers a compelling middle ground between low-level primitives and inflexible SaaS solutions.

Resources

About This Case Study: This comprehensive analysis examines OpenOT's architecture, implementation patterns, and real-world applications. It serves as both a technical reference for developers evaluating collaborative editing solutions and a demonstration of OpenOT's unique position in the market.