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

> Discover how AFFiNE implements real-time collaboration awareness and presence features using Yjs CRDTs and an AwarenessStore for seamless peer synchronization via WebSocket or WebRTC.

- Repository: [Toeverything/AFFiNE](https://github.com/toeverything/AFFiNE)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/toeverything/AFFiNE/blob/main/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:

```typescript
// 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`](https://github.com/toeverything/AFFiNE/blob/main/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`](https://github.com/toeverything/AFFiNE/blob/main/blocksuite/affine/widgets/remote-selection/src/manager/remote-color-manager.ts) assigns unique colors to users for cursor rendering:

```typescript
// 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`](https://github.com/toeverything/AFFiNE/blob/main/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`](https://github.com/toeverything/AFFiNE/blob/main/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).

```typescript
// 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`](https://github.com/toeverything/AFFiNE/blob/main/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.

```typescript
// 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`](https://github.com/toeverything/AFFiNE/blob/main/blocksuite/framework/store/src/yjs/awareness.ts). This interface contains optional fields for user identity, cursor color, and selection data:

```typescript
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`](https://github.com/toeverything/AFFiNE/blob/main/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`](https://github.com/toeverything/AFFiNE/blob/main/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.