Collaborative Editing Architecture

Written by Backbone Tutorials Team

Last updated: June 2026 · 11 min read

Building a text editor that one person uses is a solved problem. Building one where ten people edit the same document simultaneously is an architecture problem. The algorithms, OT and CRDTs, handle the conflict resolution; the architecture handles everything around them: the document model that the algorithms operate on, the sync layer that moves operations over the network, the awareness layer that shows who is present, the undo stack that works per user rather than globally, and the permission layer that prevents one user from overwriting another's protected sections. Getting the layers right is what separates a demo from a production system.

This guide assembles the techniques from OT, CRDTs, presence and awareness, and data synchronization into a complete architectural picture.

Collaborative editor layer stack Four layers from bottom to top: transport, sync, awareness, and UI. Each layer has a clear responsibility boundary. UI Layer - editor view, toolbar, cursors, selection Sync Layer - OT / CRDT + operations Awareness Layer - cursors, presence Transport Layer - WebSocket / SSE Server - auth, permission, document store, event log
A collaborative editor is four client layers over a server: transport, sync and awareness in parallel, then the UI on top.

The document model

Every other layer depends on the document model. It defines the shape of data that operations transform and the UI renders.

Schema-first design

// A simple document schema for a collaborative rich-text editor
var doc = {
  id: 'doc-abc',
  version: 42,
  content: [
    { type: 'heading', level: 1, text: 'Introduction' },
    { type: 'paragraph', text: 'Hello world.' },
    { type: 'paragraph', text: '' }
  ],
  meta: {
    title:     'My Document',
    createdAt: '2026-06-12T04:30:00Z',
    updatedAt: '2026-06-12T10:00:00Z'
  }
};

The schema defines what node types exist, what attributes they carry, and what nesting is valid. Operations are defined against the schema: insert a paragraph after node N, set the text of node N, change the level of heading N. This is important because OT and CRDT implementations must know the structure to transform operations correctly. A flat string model is simple; a rich tree model is more powerful but requires more complex operations. Choose the simplest model that covers your requirements, then extend it deliberately.

The sync layer

The sync layer is the bridge between the document model and the network. It translates local edits into operations, applies remote operations, and ensures convergence.

Connecting the algorithm to the transport

// Sync layer: local edit -> operation -> network; network -> operation -> apply
var syncLayer = {
  doc: null,
  revision: 0,
  pending: [],

  localEdit: function (change) {
    var op = toOperation(change, this.doc);
    this.pending.push(op);
    this.doc = applyOp(this.doc, op);        // optimistic local apply
    transport.send({ type: 'op', op, revision: this.revision });
  },

  onRemoteOp: function (msg) {
    var transformed = msg.op;
    // Transform against any pending local ops not yet acknowledged
    this.pending.forEach(function (local) {
      transformed = transform(transformed, local);
    });
    this.doc = applyOp(this.doc, transformed);
    this.revision = msg.revision;
    render(this.doc);
  },

  onAck: function (msg) {
    this.pending = this.pending.filter(function (op) { return op.id !== msg.opId; });
    this.revision = msg.revision;
  }
};

The pending queue holds local operations that have been applied optimistically but not yet acknowledged by the server. Each incoming remote operation must be transformed against all pending local operations before application, because those pending operations changed the document state the remote operation was generated against. The acknowledgement clears the pending entry and updates the local revision. This is the client-side of the OT architecture from the previous guide, assembled into a working layer.

The awareness layer

Awareness is ephemeral and runs parallel to the sync layer on the same WebSocket connection but in a separate message namespace.

Cursor positions mapped to document nodes

// Awareness update: cursor mapped to doc node ID, not character index
var awarenessUpdate = {
  userId:   'user-42',
  color:    '#3b82f6',
  cursor: {
    nodeId: 'node-paragraph-3',
    offset: 14
  },
  selection: {
    anchor: { nodeId: 'node-paragraph-3', offset: 10 },
    focus:  { nodeId: 'node-paragraph-3', offset: 14 }
  }
};

Cursors referenced by node ID rather than absolute character index survive the insertions and deletions that the sync layer applies. If Alice's cursor is at character 50 and Bob inserts 10 characters before it, a character-index cursor silently moves to position 60 and is now wrong. A node-relative cursor stays correctly anchored to the same paragraph. The awareness layer throttles position updates to about twenty per second, as covered in the presence guide, and the UI renders remote cursors as overlays on top of the editor canvas.

Per-user undo

Global undo, where pressing Ctrl+Z reverses the most recent operation regardless of who made it, is unacceptable in a collaborative context. Undoing a colleague's work without warning them is a collaboration failure.

Inverting and transforming the local operation

var undoStack = [];

// On local edit: push an invertible record
function pushUndo(op, docStateBeforeOp) {
  undoStack.push({ op, inverse: invertOp(op), atRevision: syncLayer.revision });
}

// On undo
function undo() {
  var entry = undoStack.pop();
  if (!entry) return;

  // Transform the inverse past all remote ops that arrived since
  var inverse = entry.inverse;
  var remoteOps = getRemoteOpsSince(entry.atRevision);
  remoteOps.forEach(function (remote) {
    inverse = transform(inverse, remote);
  });

  syncLayer.localEdit(inverse);
}

Each undo entry stores the inverse of the local operation and the revision at which it was made. On undo, the inverse is transformed past all remote operations that have arrived since that revision. The result is an operation that, when applied to the current document, undoes exactly Alice's change without disturbing Bob's interleaved work. This is correct but requires keeping a list of remote operations since each undo entry's revision; the event log from the event streaming guide is the natural place to store them.

Permission enforcement

In a multi-user document, not everyone should be able to edit everything. Permission enforcement must live on the server.

Server-side operation validation

// Server: validate before applying
function handleClientOp(userId, op, revision) {
  var role = getRole(userId, op.docId);

  if (role === 'viewer') {
    return { error: 'read-only' };
  }
  if (role === 'commenter' && op.type !== 'addComment') {
    return { error: 'comments-only' };
  }
  if (op.nodeId && isNodeLocked(op.nodeId, userId)) {
    return { error: 'node-locked' };
  }

  return applyClientOp(userId, op, revision);
}

Client-side permission checks are only UX conveniences. A determined user can modify JavaScript and send any operation they want. The server must re-check every incoming operation against the user's current role and the current document state before applying it. Rejected operations return an error code; the client rolls back the optimistic apply and shows appropriate feedback. Locking individual nodes, useful for template sections that only admins can edit, works the same way: the lock check runs on the server, not in the editor.

Assembling the layers

Four independent layers, each with a clear responsibility, compose into a full collaborative editing system with manageable complexity at each boundary.

Layer boundaries and data flow

The transport layer carries two message streams: sync messages that carry operations and revision numbers, and awareness messages that carry ephemeral state. The sync layer consumes sync messages, maintains the document, and exposes a change event that the UI subscribes to. The awareness layer consumes awareness messages and exposes a participant map that the UI renders as cursor overlays and avatar lists. The UI layer translates user input into local edits, passes them to the sync layer, and renders both the document and the awareness overlays. The server enforces permissions, applies and broadcasts operations in canonical order, and serves as the recovery point for catch-up. Each layer is testable in isolation: the sync layer with mock transports, the awareness layer with mock messages, the permission layer with unit tests against role tables. The separation is what makes this architecture manageable at production scale.

Frequently Asked Questions

What layers does a collaborative editor need?

A collaborative editor typically has four layers: the document model that defines the data structure, the sync layer that handles conflict resolution and transport, the awareness layer for presence and cursor positions, and the UI layer that renders the document and exposes editing controls.

Why is per-user undo hard in collaborative editors?

In a single-user editor, undo reverses the most recent operation. In a collaborative editor, Alice and Bob interleave operations. If Alice undoes her last operation, it may sit between several of Bob's, so a simple stack revert would corrupt Bob's work. Per-user undo must invert Alice's operation and transform the inverse past all intervening operations from other users.

Where should permission checks happen in a collaborative editor?

Permission checks must happen on the server, not only in the client. A client can be modified to bypass UI restrictions. Every operation sent to the server must be validated against the user's role before being applied and broadcast. The server is the authority; the client UI is a convenience.

What is the difference between the sync layer and the awareness layer?

The sync layer manages durable document state: operations, conflict resolution, and the persistent record of all changes. The awareness layer manages ephemeral presence data: who is connected, where their cursor is, and what they are doing right now. Awareness state is never persisted and is safe to lose on server restart.

Read next: the Interactive Web hub, or continue to multiplayer state synchronization for real-time shared state beyond documents.

Want views that update cleanly as collaborative edits arrive?

The Backbone guide shows exactly how views respond to model changes from a live sync layer.

Explore the Backbone Guide →