Document CRUD
POST /v1/documents
Body: {"title": "Untitled, "owner_id": "user123"}
Response: {"doc_id": "doc_abc", "created_at": timestamp}
GET /v1/documents/{doc_id}
Response: {"doc_id", "title", "content":
Real-time editing (WebSocket)
WS /v1/documents/{doc_id}/collaborate
Comments
POST /v1/documents/{doc_id}/comments
Body: {"anchor": {"start": 10, "end": 25}, "body": "Rephrase this?", "author_id": "user456"}
POST /v1/documents/{doc_id}/comments/{comment_id}/replies
Body: {"body": "Done!", "author_id": "user123"}
Sharing & Permissions
PUT /v1/documents/{doc_id}/permissions
Body: {"grants":[{"user_id": "user789", "role": "editor"}]}
POST /v1/documents/{doc_id}/share-link
Body: {"role": "viewer", "expires_at": timestamp}
Response: {"link": "https://docs.example.com/s/abc123"}
Revision History
GET /v1/documents/{doc_id}/revisions?limit=50&cursor={cursor}
Response: {"revisions": [{"version": 42, "author_id", "timestamp", "summary"}]}
POST /v1/documents/{doc_id}/restore
Body: {"version": 42}
The core challenge is conflict resolution - multiple users editing the same position simultaneously. The industry standard solutions are:
We choose OT with a central coordination server per document because:
Components:
The core algorithm:
Each document has a linear version number. Every operation (insert, delete, format) is submitted against a base_version.
Client A (version 5): insert("X", pos=3) -> sends to server
Client B (version 5): insert("Y", pos=1) -> sends to server
Server receives A's op first, applies it -> version 6. When B's op arrives (base_vresion=5, but server is now at 6), the server transforms B's op against A's op:
Server state machine per document:
Client state machine:
Scaling: Each document is assigned to one collaboration server instance (consistent hashing on doc_id). If a document gets too hot (1000+ concurrent editors), the server can shard by section/paragraph - but this is rare.
Persistence: Every operation is appended to an operation log (append only, Kafka or DDB stream). Periodically (every N ops or T seconds), a snapshot is written. Recovery = load latest snapshot + replay ops since.
2. Offline Sync & Conflict Resolultion
Problem: User edits offline for hours, then reconnects. Their operations are based on a stale version.
Design:
Guarantee: OT's transformation properties ensure convergence regardless of how long the client was offline. No manual conflict resolution needed for text - the math handles it.
Edge case - large divergence: If the client has been offline for days and the op log is huge, the server sends a full snapshot instead of the op stream. Client rebases local ops against the snapshot diff.
3. Revision history and snapshots
Problem: Storing every keystroke forever is expensive. Users want meaningful "versions" they can browse and restore.
Design - Two-level storage:
Op log - every individual operation - 30 day retention - PurposeL real-time sync, recent undo
Snapshots - Full document state - forever - revision history, restore points
Snapshot strategy:
Viewing history: The revision timeline shows snapshots with metadata (author, timestamp, change summary). Summary is generated by diffing consecutive snapshots.
Restoring: Creates a new operation that sets the document content to the snapshot state. This is itself an operation in the log - so "restore" is undoable and doesn't destroy subsequent history.
Storage:
Session cache (Redis)
Purpose: Hold ephemeral, hot state that doesn't belong in durable storage
What lives here:
On a cache miss: Fall thru to dynamodb for the op log an latest snapshot. Rebuild in-memory state.
Scaling: Redis cluster sharded by doc_id. Each doc's keys land on the same shard.
Metadata Store (PostgreSQL)
Purpose: Durable source of truth for everything about documents that isn't the document content itself.
Schema:
documents (
doc id
title
ownerid
created_at
updated_at
current_version
storage_region
)
permissions (
doc_id
grantee
role
granted_by
granted_at
PRIMARY_KEY (doc_id, grantee)
)
share_links
comments
Comment anchor tracking: When the document changes, comment anchors drift. Two approaches:
We use lazy rebase
Why Single Owner is Required for OT
OT relies on a total ordering of operations. The server assigns sequential version numbers. Without the lock, the document corrupts
What the lock actually enforces
Server A holds lock for doc_123 (lease: 30s, renewed every 10s)
Server B receives a WS connection for doc_123
Server B checks Redis -> lock held by A -> routes client to A (or rejects + tells client to reconnect to A)
If Server A dies:
Sticky Routing Layer
The WebSocket Gateway needs to route clients to the correct collaboration server. This is done via:
So the routing is:
Client connects for doc_123
Could you avoid the single owner constraint?
Yes - by switching from OT to CRDTs. Google chose OT because the single-owner constraint is acceptable -failover in 30s is fine for a document editor, and the simpler version model makes history and snapshots trivial. If you needed sub-second failover (e.g. multiplayer gaming) CRDTs would be the better choice.