# Collaborative Editing
URL: https://pro.rv-grid.com/guides/data-manage/collaborative-editing/
Source: src/content/docs/guides/data-manage/collaborative-editing.mdx
Description: Learn how to add shared cell editing, presence, pending review, permissions, server commits, conflicts, and audit attribution to RevoGrid Pro.
`CollaborativeEditingPlugin` lets several users work on the same grid data at the same time. One user edits a cell, the edit is written into a shared Yjs document, and the other connected grids can receive the same change.

You do not need to know Yjs before using the plugin. Think of Yjs as the shared notebook where the browser stores collaborative changes. RevoGrid still renders the grid, your app still owns users and business rules, and your backend still decides what is finally saved.

Install collaborative editing as a separate package so Yjs is only loaded by applications that need realtime editing:

```sh
pnpm add @revolist/revogrid-collaborative-editing @revolist/revogrid-pro
```

## Live Demo: Split Collaborative Editing

The demo catalog includes a complete split-view example with two users editing the same shared document. Open it when you want to see the moving parts together instead of reading isolated snippets.

[Open the Collaborative Editing demo](https://demo.rv-grid.com/collaborative-editing/ts)

The demo shows:

- two grids on the same page using one collaborative document;
- Avery and Grace rendered with matching presence colors and avatars;
- live remote focus and selection markers through `CollaborativePresencePlugin`;
- the first grid with the `Task` column pinned while the second grid keeps it unpinned;
- `EventManagerPlugin` with `applyEventsToSource: false`;
- an in-memory presence transport that stands in for a realtime provider;
- a mock `commitAdapter` that logs server commits so the backend boundary is visible.

## The Mental Model

Collaborative editing has a few moving parts. The names are technical, but the ideas are simple.

    A user changes a cell. `EventManagerPlugin` normalizes that edit so collaborative editing sees cell edits, paste, range edits, undo, and redo through one pipeline.

    Presence means "where are the other users looking or editing?" Add `CollaborativePresencePlugin` when you want remote cursors, ranges, colors, and labels.

    In review mode, edits stay pending until your UI commits or discards them. This is useful when a manager, workflow, or server must approve changes.

    A commit is the point where your app asks the backend to accept changes. The plugin calls your `commitAdapter`; your backend can accept, rewrite, or reject each edit.

    A conflict happens when users edit the same row and column concurrently. The default policy keeps the change pending so your UI can show a decision.

    Audit records explain who changed what and when. Add `AuditHistoryPlugin` when committed collaborative edits should be logged.

The plugin coordinates grid edits, shared Yjs state, pending changes, conflicts, presence bridging, and audit integration. Your app still owns authentication, user identity, backend persistence, authorization, and the review UI.

## Beginner Setup

Start with the smallest local setup, then add the pieces your product needs.

### 1. Minimal Local Grid

This example needs no server and no transport. It is useful for learning the plugin and verifying that local cell edits enter the collaborative pipeline.

1. Import the base plugins.

   ```ts
   import {
     EventManagerPlugin,
   } from '@revolist/revogrid-pro';

   import {
     CollaborativeEditingPlugin,
     type CollaborativeEditingConfig,
   } from '@revolist/revogrid-collaborative-editing';
   ```

2. Give every row a stable id.

   ```ts
   const rows = [
     { id: 'task-1', task: 'Revenue forecast', owner: 'Avery', status: 'Draft' },
     { id: 'task-2', task: 'Vendor renewal', owner: 'Mina', status: 'Review' },
     { id: 'task-3', task: 'Headcount plan', owner: 'Jon', status: 'Approved' },
   ];

   const columns = [
     { prop: 'task', name: 'Task' },
     { prop: 'owner', name: 'Owner' },
     { prop: 'status', name: 'Status' },
   ];
   ```

3. Register plugins and bind configuration.

   ```ts
   const grid = document.querySelector('revo-grid')!;

   grid.columns = columns;
   grid.source = rows;
   grid.plugins = [
     EventManagerPlugin,
     CollaborativeEditingPlugin,
   ];

   grid.eventManager = { applyEventsToSource: false };

   const collaborativeEditing: CollaborativeEditingConfig = {
     documentId: 'local-learning-session',
     user: {
       id: 'avery',
       name: 'Avery Stone',
       color: '#2563eb',
       permissionLevel: 'editor',
     },
     rowIdProp: 'id',
   };

   grid.collaborativeEditing = collaborativeEditing;
   ```

`grid.eventManager = { applyEventsToSource: false }` is important. It lets collaborative editing own how edits are applied, reviewed, committed, or rolled back instead of letting each raw grid edit immediately mutate source data twice.

### 2. Two Grids On The Same Page

Use a shared Yjs document when two grids on one page should represent the same collaborative session. This is the easiest way to understand how two users share edits before connecting a real transport.

```ts
import * as Y from 'yjs';
import {
  CollaborativePresencePlugin,
  EventManagerPlugin,
} from '@revolist/revogrid-pro';
import {
  CollaborativeEditingPlugin,
} from '@revolist/revogrid-collaborative-editing';

const doc = new Y.Doc();
const plugins = [
  EventManagerPlugin,
  CollaborativePresencePlugin,
  CollaborativeEditingPlugin,
];

function configureGrid(grid: HTMLRevoGridElement, user: { id: string; name: string; color: string }) {
  grid.columns = columns;
  grid.source = rows.map(row => ({ ...row }));
  grid.plugins = plugins;
  grid.eventManager = { applyEventsToSource: false };
  grid.collaborativeEditing = {
    documentId: 'same-page-demo',
    user,
    rowIdProp: 'id',
    doc,
    presence: {
      enabled: true,
      showLabels: true,
      staleAfterMs: 15_000,
    },
  };
}

configureGrid(gridA, { id: 'avery', name: 'Avery Stone', color: '#2563eb' });
configureGrid(gridB, { id: 'grace', name: 'Grace Hopper', color: '#16a34a' });
```

Both grids use the same `documentId` and the same `doc`, so they are intentionally part of the same collaborative document.

`CollaborativeEditingPlugin` maps awareness state into presence users. `CollaborativePresencePlugin` renders those users as colored focus and range markers. For deeper cursor and range behavior, read [Collaborative Presence](/guides/data-manage/collaborative-presence/).

### 3. Review Mode With Pending Changes

Use review mode when users should make changes locally, inspect the pending list, then commit or discard those changes from your UI.

```ts
grid.collaborativeEditing = {
  documentId: `budget:${budgetId}`,
  user: currentUser,
  rowIdProp: 'id',
  mode: 'review',
  conflictPolicy: 'manual',
};
```

Read the plugin instance when your UI needs pending changes or review actions.

```ts
const plugins = await grid.getPlugins();
const collaborative = plugins.find(
  plugin => plugin instanceof CollaborativeEditingPlugin,
) as CollaborativeEditingPlugin | undefined;

const pending = collaborative?.getPendingChanges() ?? [];

await collaborative?.commitPendingChanges(pending.map(change => change.id));
collaborative?.discardPendingChanges();
```

Review mode is a good default for finance, procurement, admin, support, planning, and any workflow where users should not silently overwrite each other.

### 4. Commit Changes To A Server

Use `commitAdapter` when your backend must validate, persist, approve, reject, version, or rewrite edits.

```ts
grid.collaborativeEditing = {
  documentId: `forecast:${forecastId}`,
  user: currentUser,
  rowIdProp: 'id',
  mode: 'autoCommit',
  commitAdapter: {
    async commit(changes, context) {
      const response = await fetch(`/api/forecasts/${forecastId}/changes`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          documentId: context.documentId,
          user: context.user,
          changes,
        }),
      });

      return response.json();
    },
  },
};
```

The response should describe which changes were committed and which were rejected. Rejected changes are rolled back to their previous local value.

```ts
[
  { id: 'change-1', status: 'committed' },
  { id: 'change-2', status: 'rejected', reason: 'Budget is locked' },
]
```

### 5. Add Business Permissions

Use `permissions.canEditCell` for user, role, row, tenant, or workflow-specific edit rules.

```ts
grid.collaborativeEditing = {
  documentId: `pricing:${priceBookId}`,
  user: {
    id: currentUser.id,
    name: currentUser.name,
    roles: currentUser.roles,
    permissionLevel: currentUser.permissionLevel,
  },
  rowIdProp: 'sku',
  permissions: {
    canEditCell({ user, row, prop, newValue }) {
      if (row?.locked) return false;
      if (prop === 'approvedPrice') return user.roles?.includes('pricing-admin') ?? false;
      if (prop === 'discount' && Number(newValue) > 0.2) return user.permissionLevel === 'manager';
      return user.permissionLevel === 'editor';
    },
  },
};
```

Client permissions improve UX and prevent obvious invalid edits early. They are not a security boundary; repeat important checks in your backend through `commitAdapter`.

## Connect More Than One Browser

A shared local `Y.Doc` is enough for demos and same-page tests. For real users in different browsers, add a transport.

```ts
import {
  createYjsWebSocketTransport,
} from '@revolist/revogrid-collaborative-editing';

grid.collaborativeEditing = {
  documentId: `project:${projectId}:tasks`,
  user: currentUser,
  rowIdProp: 'id',
  transport: createYjsWebSocketTransport({
    url: 'wss://collaboration.example.com',
    params: { token: sessionToken },
  }),
};
```

Use WebSocket when your product needs a controlled server, authentication, observability, and enterprise networking. Use WebRTC for peer-to-peer collaboration where your deployment can support signaling and peer connectivity. Use a custom transport when your application already wraps Yjs providers.

## Listen For Conflicts

The default `conflictPolicy` is `manual`. When two users edit the same row and column concurrently, the plugin emits `collaborativeconflict` and keeps the local change pending.

```ts
grid.addEventListener('collaborativeconflict', (event) => {
  const conflict = event.detail;

  showConflictDialog({
    rowId: conflict.local.rowId,
    column: conflict.local.prop,
    localValue: conflict.local.newValue,
    remoteValue: conflict.remoteValue,
    user: conflict.remoteUser,
  });
});
```

Set `conflictPolicy: 'lastWriteWins'` only for low-risk cells such as notes, draft comments, or transient planning values where silent overwrite is acceptable.

## Configuration Reference

### Business-Critical Properties

| Property | Type | Default | First-time guidance |
|:--|:--|:--|:--|
| `documentId` | `string` | `'revogrid-collaborative-document'` | The shared session name. Use the same value for every user editing the same business object, such as `budget:2026` or `project:alpha:tasks`. |
| `user` | `CollaborativeEditingUser` | `undefined` | The current editor. Provide at least `id` and `name`; without a user, local edits are ignored because edits must be attributable. |
| `rowIdProp` | `string` | `'id'` | The row field that uniquely identifies the business record. Use your real primary key, such as `invoiceId`, `sku`, `taskId`, or `employeeId`. |
| `commitAdapter` | `CollaborativeEditingCommitAdapter` | `undefined` | Your backend boundary. Add it when changes must be saved, validated, approved, rejected, versioned, or audited by a server. |
| `permissions` | `CollaborativeEditingPermissions` | `undefined` | Client-side business rules. Use this for role checks, locked rows, approval status, tenant rules, or field-level permissions. |
| `audit` | `CollaborativeEditingAuditConfig \| boolean` | enabled | Audit integration. Keep it enabled for operational, financial, admin, support, or regulated workflows. |
| `conflictPolicy` | `'manual' \| 'lastWriteWins'` | `'manual'` | How same-cell concurrent edits are handled. Keep `manual` for business data. |

### Runtime And Collaboration Properties

| Property | Type | Default | First-time guidance |
|:--|:--|:--|:--|
| `enabled` | `boolean` | `true` | Turns collaboration on or off without changing the plugin list. Useful for feature flags or read-only screens. |
| `doc` | `Y.Doc` | new isolated document | Pass this when your app owns the Yjs document or when several grids should share one local document. |
| `mode` | `'autoCommit' \| 'review'` | `'autoCommit'` | Use `autoCommit` for immediate server reconciliation. Use `review` when users need commit and discard buttons. |
| `transport` | `CollaborativeEditingTransport` | `undefined` | Connects different browsers. Without transport, collaboration is local to the current page or shared `doc`. |
| `presence` | `{ enabled, showLabels, staleAfterMs }` | enabled, labels shown | Controls the bridge into `CollaborativePresencePlugin`. Use it when users should see who is focused or editing. |

### `user`

`user` tells the plugin who is editing. It is used for presence labels, permission checks, commit requests, audit records, and conflict metadata.

```ts
const user = {
  id: 'u-123',
  name: 'Avery Stone',
  email: 'avery@example.com',
  initials: 'AS',
  color: '#2563eb',
  roles: ['planner', 'budget-editor'],
  permissionLevel: 'editor',
};
```

Use a stable application user id. Avoid session ids or display names as `id`, because audit history and server reconciliation need durable identity.

### `documentId`

`documentId` means "which shared document is this grid editing?"

```ts
documentId: `forecast:${forecastId}`
documentId: `project:${projectId}:schedule`
documentId: `tenant:${tenantId}:price-book:${priceBookId}`
```

Use the same `documentId` for every client editing the same dataset. Use a different `documentId` when two grids should not share edits or presence.

### `rowIdProp`

Collaborative editing stores cell changes by stable row identity. If `rowIdProp` is wrong, edits may apply to the wrong row after sorting or filtering.

```ts
const rows = [
  { invoiceId: 'INV-1001', status: 'Draft', total: 1200 },
];

grid.collaborativeEditing = {
  documentId: 'invoices:open',
  user,
  rowIdProp: 'invoiceId',
};
```

Use `id` only when every row really has a durable `id`. For generated, aggregated, or temporary rows, provide a stable key before enabling collaboration.

### `commitAdapter`

`commitAdapter` means "ask my backend whether these edits are accepted."

```ts
commitAdapter: {
  async commit(changes, context) {
    const response = await fetch('/api/reconcile-grid-changes', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        documentId: context.documentId,
        user: context.user,
        changes,
      }),
    });

    return response.json() as Promise<Array<{
      id: string;
      status: 'committed' | 'rejected';
      value?: unknown;
      reason?: string;
      metadata?: Record<string, unknown>;
    }>>;
  },
}
```

Return `committed` when the server accepts a change. Return `rejected` when validation, permissions, workflow status, version checks, or business rules fail.

### `permissions`

`permissions.canEditCell` runs before a local edit is accepted. It complements RevoGrid's `readonly` support:

- use column `readonly` for static UI-level rules
- use `permissions.canEditCell` for user, row, tenant, or workflow-specific rules

```ts
permissions: {
  canEditCell({ user, row, prop, newValue }) {
    if (row?.approvalStatus === 'Approved') return false;
    if (prop === 'budgetLimit') return user.roles?.includes('finance-admin') ?? false;
    if (prop === 'discount' && Number(newValue) > 0.2) return user.permissionLevel === 'manager';
    return user.permissionLevel === 'editor';
  },
}
```

### `audit`

When `AuditHistoryPlugin` is registered, collaboration commits are recorded as audit events by default.

```ts
audit: {
  enabled: true,
  actionType: 'collaborative-price-change',
  source: 'api',
}
```

Disable audit only for temporary or non-business data:

```ts
audit: false
```

## Commit Modes

Use `mode: 'autoCommit'` when the server should accept or reject each edit as soon as it is made.

Use `mode: 'review'` when users need an explicit commit/discard workflow. In review mode, edits are optimistic locally and remain in `getPendingChanges()` until the host commits or discards them through the plugin instance.

## Audit Trail

When `AuditHistoryPlugin` is registered and `audit.enabled !== false`, committed collaborative changes call `AuditHistoryPlugin.recordEvent()` with:

- `type: 'collaborative-edit'`
- current collaborative user
- transaction id
- row id, row index, column, old value, and new value
- collaborative metadata and document id

Use the Audit History storage hooks to persist those records to your backend.

## Common First-Time Mistakes

| Mistake | What happens | Fix |
|:--|:--|:--|
| Missing `EventManagerPlugin` | Edits do not enter the normalized collaborative edit pipeline. | Register `EventManagerPlugin` before `CollaborativeEditingPlugin`. |
| Missing `user` | Local edits are ignored because the plugin cannot attribute them. | Pass a stable `user` with at least `id` and `name`. |
| Missing or unstable `rowIdProp` | Edits can target the wrong row after sorting, filtering, or reordering. | Use a durable business id field. |
| Different `documentId` values | Users do not share the same collaborative document. | Use one stable `documentId` for every client in the same session. |
| Expecting automatic persistence | Local/Yjs state changes, but your backend is not updated. | Add a `commitAdapter`. |
| Expecting visible labels without presence plugin | Editing works, but remote cursors and ranges do not render. | Add `CollaborativePresencePlugin` and enable `presence`. |

## What To Read Next

- [Collaborative Presence](/guides/data-manage/collaborative-presence/) for remote cursors, selections, labels, stale users, and pinned-area behavior.
- [Audit Trail History](/guides/data-manage/audit-history/) for persisted change logs and restore workflows.
- [Event Manager](/guides/data-manage/event-manager-explanation/) for the edit event pipeline collaborative editing uses.
