How AFFiNE Implements Awareness and Presence Features for Real-Time Collaboration

AFFiNE implements awareness and presence features using Yjs's CRDT-based awareness protocol, wrapping it in an AwarenessStore that synchronizes user metadata, cursor positions, and selections across peers via WebSocket or WebRTC.

AFFiNE's real-time collaboration system enables multiple users to see each other's cursors, selections, and identity while editing shared workspace documents. The implementation leverages Yjs's conflict-free replicated data type (CRDT) awareness protocol to manage ephemeral user state without document lock conflicts.

Core Architecture of AFFiNE's Awareness System

AwarenessStore and Yjs Integration

At the heart of AFFiNE's presence system is the AwarenessStore class defined in blocksuite/framework/store/src/yjs/awareness.ts. This store wraps a Y-protocols Awareness instance and manages both local state (current user's metadata) and global state (metadata of all connected peers).

When a workspace document initializes, the system creates a new Y-Doc for the space and binds an Awareness instance to it:

// From packages/frontend/core/src/modules/workspace/impls/doc.ts
constructor(...) {
  this._ySpaceDoc = new Y.Doc();
  this.awarenessStore = new AwarenessStore(
    new Awareness(this._ySpaceDoc)
  );
}

Local and Global State Management

The AwarenessStore maintains two distinct state layers. The local state contains fields like user (display name), color (cursor highlight), and selectionV2 (current text selection). The global state aggregates these fields from all connected clients into a single observable map.

Initializing Awareness in Workspace Documents

Awareness initialization occurs during DocImpl construction in packages/frontend/core/src/modules/workspace/impls/doc.ts. The constructor instantiates the AwarenessStore with a fresh Awareness object bound to the document's Y-Doc space.

The system pre-populates the selectionV2 field during initialization to ensure every client can broadcast its selection state immediately upon connection. This happens within the AwarenessStore constructor logic, establishing the schema for ephemeral data exchange.

Broadcasting User Presence and Metadata

Setting Local State Fields

Individual features update specific awareness fields using setLocalStateField. For example, the RemoteColorManager in blocksuite/affine/widgets/remote-selection/src/manager/remote-color-manager.ts assigns unique colors to users for cursor rendering:

// Simplified from RemoteColorManager
setLocalColor(color: string) {
  this.store.setLocalStateField('color', color);
}

setLocalUser(user: UserInfo) {
  this.store.setLocalStateField('user', user);
}

Publishing Presence on Document Load

When a document finishes loading via DocImpl.load, the workspace exposes the raw Awareness instance through the onLoadAwareness callback. This hook allows higher-level services to attach listeners for cursor positions and selection changes.

The callback is defined in packages/frontend/core/src/modules/workspace/impls/workspace.ts and invoked during the document loading sequence, bridging the low-level Yjs awareness with AFFiNE's UI layers.

Synchronizing Awareness Across the Network

AwarenessSyncImpl and Network Transport

The AwarenessSyncImpl class in packages/common/nbstore/src/sync/awareness/index.ts handles broadcasting awareness updates to both local storage and remote peers. When local state changes, the implementation forwards updates through configured network adapters (WebSocket or WebRTC).

// From AwarenessSyncImpl
update(awareness: Awareness, changedClients: number[]) {
  // Forward to local storage
  this.localStorage.update(awareness, changedClients);
  // Broadcast to remote peers
  this.remoteStorages.forEach(storage => 
    storage.update(awareness, changedClients)
  );
}

Remote changes propagate back to the local AwarenessStore via subscribeUpdate, which merges incoming peer states into the global awareness map without conflicts.

Rendering Remote Selections and Cursors

SelectionExtension and Global State Reading

The SelectionExtension in blocksuite/framework/store/src/extension/selection/selection-extension.ts renders remote cursors and selection highlights by reading the global awareness state. It filters out the local client ID and iterates over peer entries to generate visual indicators.

// Simplified from SelectionExtension
renderRemoteSelections() {
  const states = this.store.awarenessStore.getStates();
  const localId = this.store.awarenessStore.clientID;
  
  states.forEach((state, clientId) => {
    if (clientId !== localId && state.selectionV2) {
      this.renderCursor(state.user, state.color, state.selectionV2);
    }
  });
}

Data Model for Awareness State

The raw awareness state structure is defined by RawAwarenessState in blocksuite/framework/store/src/yjs/awareness.ts. This interface contains optional fields for user identity, cursor color, and selection data:

interface RawAwarenessState {
  user?: { name: string; id: string };
  color?: string;
  selectionV2?: Map<string, Selection>;
}

The selectionV2 field uses a map structure keyed by selection manager ID, allowing multiple selection contexts per user without collisions.

Summary

  • AFFiNE implements awareness and presence features using Yjs's CRDT-based awareness protocol wrapped in an AwarenessStore abstraction.
  • Each workspace document initializes awareness in DocImpl by binding a new Awareness instance to the document's Y-Doc.
  • Local state fields (user, color, selectionV2) are set via setLocalStateField and broadcast through AwarenessSyncImpl to remote peers.
  • The SelectionExtension renders remote cursors by reading global awareness states and filtering out the local client ID.
  • Network synchronization occurs through AwarenessSyncImpl, which forwards updates to both local storage and remote transports (WebSocket/WebRTC).

Frequently Asked Questions

How does AFFiNE handle conflicting awareness updates from multiple users?

AFFiNE relies on Yjs's CRDT-based awareness protocol, which automatically merges concurrent updates without conflicts. The Awareness class from y-protocols handles the convergence of local and remote states, ensuring that each peer eventually sees the same global awareness map regardless of network latency or simultaneous edits.

What information is shared through AFFiNE's awareness system?

The awareness system shares ephemeral user metadata defined in RawAwarenessState, including the user's display name and ID, a unique color for cursor rendering, and the selectionV2 map containing current text selections and cursor positions. This data is distinct from the document content and is not persisted to the document itself.

How are awareness updates synchronized across the network?

Updates are synchronized through AwarenessSyncImpl in packages/common/nbstore/src/sync/awareness/index.ts. When local awareness changes, this implementation forwards the update to both local storage and configured remote storages (typically WebSocket or WebRTC connections). Remote updates propagate back via subscribeUpdate, which merges peer states into the local AwarenessStore.

Where does AFFiNE render remote user cursors and selections?

Remote cursors and selections are rendered by the SelectionExtension in blocksuite/framework/store/src/extension/selection/selection-extension.ts. This extension reads the global awareness states via store.awarenessStore.getStates(), filters out the local client ID, and generates visual indicators for each peer's cursor position and text selection using the peer's assigned color and user info.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →