---
title: "Collaboration on the client"
description: "Collaboration is a feature: mount a surface with a Yjs session, per-client undo, authors and comments, positions that survive concurrent edits, and matching schemas."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/collaboration"
---
# Collaboration on the client

> Collaboration is a feature: mount a surface with a Yjs session, per-client undo, authors and comments, positions that survive concurrent edits, and matching schemas.

## Mounting a session

Collaboration is a feature, not a mode: the preset contains `collabFeature()`, and the surface is mounted with a session.

```vue
<script setup lang="ts">
import { HocuspocusProvider } from '@hocuspocus/provider'
import { collabFeature } from '@skaile/forge-editor/collab/vue'
import { coreContentFeature, coreUiFeature, defineEditor } from '@skaile/forge-editor/core'
import { EditorSurface } from '@skaile/forge-editor/vue'
import * as Y from 'yjs'

const preset = defineEditor({ features: [coreContentFeature(), coreUiFeature(), collabFeature()] })
const ydoc = new Y.Doc()
const provider = new HocuspocusProvider({
  url: `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/collab/${id}`,
  name: id,
  document: ydoc,
})
onBeforeUnmount(() => provider.destroy())
</script>

<template>
  <EditorSurface :preset="preset" :collaboration="{ ydoc, provider, user: { id: 'u1', name: 'Ada', color: '#3b82f6' } }" />
</template>
```

React is the same with `@skaile/forge-editor/collab/react` and the React `EditorSurface`; the Y.Doc and provider are created once per document id and destroyed in an effect cleanup:

```tsx
import { HocuspocusProvider } from '@hocuspocus/provider'
import { collabFeature } from '@skaile/forge-editor/collab/react'
import { coreContentFeature, coreUiFeature, defineEditor } from '@skaile/forge-editor/core'
import { EditorSurface } from '@skaile/forge-editor/react'
import { useEffect, useMemo, useState } from 'react'
import * as Y from 'yjs'

const preset = defineEditor({ features: [coreContentFeature(), coreUiFeature(), collabFeature()] })

function CollaborativeEditor({ id }: { id: string }) {
  const [session, setSession] = useState<{ ydoc: Y.Doc, provider: HocuspocusProvider } | null>(null)
  useEffect(() => {
    const ydoc = new Y.Doc()
    const provider = new HocuspocusProvider({ url: `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/collab/${id}`, name: id, document: ydoc })
    setSession({ ydoc, provider })
    return () => { provider.destroy(); setSession(null) }
  }, [id])
  const collaboration = useMemo(() => session && { ...session, user: { id: 'u1', name: 'Ada', color: '#3b82f6' } }, [session])
  return collaboration ? <EditorSurface key={id} preset={preset} collaboration={collaboration} /> : null
}
```

- The Y.Doc is the document: there is no `v-model`. Core turns its own undo history off; the Collaboration extension brings a **per-client** undo/redo (`Mod-z`, `Mod-y`, `Shift-Mod-z`), so undo never reverts another user's edits.
- `provider` adds remote carets (`CollaborationCaret`, labelled with `user`); without it the Y.Doc is edited locally only. `user` is an `AuthorIdentity` (`{ id, name, color }`, the app's stable user id): the optional `authors` and `comments` features attribute text and comments to it, see [Authors and comments](#authors-and-comments).
- Without a `collaboration` prop `collabFeature()` contributes no extension: the editor behaves like a standalone one with JSON `v-model`.
- Supplying `collaboration` to a preset **without** `collabFeature()` is a setup error: the surface throws „`collaboration` needs a preset with the collaboration feature“.
- The mode is fixed at mount. Remount (change `key`) when the document changes, so Y.Doc, provider and editor never bleed between documents.
- Include `@skaile/forge-editor/styles/collab.css` (the Nuxt module adds it) for the caret styles.

## Authors and comments

Two optional features build on the session's `user`; the advanced editor contains both.

- `authorsFeature()` marks every text run the user inserts with an `author` mark (`{ id, name, color }`; one author per run, remote changes and Yjs undo/redo keep the original author's mark). Without `collaboration.user` it attributes nothing, so standalone editors write unmarked text. Colouring is a per-user preference (`editing.authorColors`, `editing.authorColorStyle`).
- `commentsFeature()` (requires `collab`) stores comment threads in the Y.Doc, in a `Y.Map` named `comments`; the commented range is a `comment` mark. Threads therefore sync live and persist with the document, but they are **not** part of the JSON/Markdown adapter content. Without a Y.Doc and a `collaboration.user` the UI says „Kommentare gibt es nur in gemeinsam bearbeiteten Dokumenten.“ and nothing can be written.
- Both marks are in `advancedSchemaExtensions()`, so the server's Yjs↔JSON conversion keeps them. They have no Markdown form: Markdown export, imports and the AI context ignore them (`DocumentMarkdown`).
- Surface contract: `v-model:comments-open` (Vue `EditorSurface`/`AdvancedEditor`) or `commentsOpen`/`onCommentsOpenChange` (React) is the open state of the margin column, which the host renders with `CommentsMargin` inside the editor's scroll container; `useEditorUi().toggleComments()` and `Mod-Shift-m` toggle it, and it excludes the AI sidebar. The column lays its cards out with the framework-free `layoutMargin` / `createMarginController` of `@skaile/forge-editor/comments`: the active thread sits exactly at its text's line, the others are one-line cards out of its way, resolved threads sit behind „Erledigt (n)“, and a thread whose text was deleted stays greyed out at the deletion point („Text gelöscht“) until undo re-attaches it ([Authors and comments guide](https://skaile-ai.github.io/forge-editor/guides/authors-and-comments)).

## Positions that survive concurrent edits

`collabFeature()` supplies `DocAnchors` with `yjsAnchorResolver`: ranges captured through `captureRange` (AI reviews, ghost text, chat ops, the selection menu) are stored as Yjs relative positions and re-resolved after every remote change and undo/redo. Without the collab feature anchors map through local transactions only.

The resolver binds to the Yjs types of the editor's own Y.Doc. The package pins one `yjs` instance as a peer dependency: install a single copy in the host, or Yjs warns „already imported“ and `instanceof` checks fail.

## Matching schemas: who owns the document

The Yjs document is only valid if every participant uses the same schema.

- The **server** builds `collabSchema()` from `advancedSchemaExtensions()` plus the host's `schema` option of `createCollabServer`/`installEditorServer` (the package's collab route) and migrates and validates seeded content with it.
- The **client** must mount a preset with the same schema: `advancedEditor()`/`AdvancedEditor`, or your own preset whose features contribute the same nodes and marks (media, transcript, Typed blocks, mentions, authors and comments marks, block IDs). Custom `blockTypes` are storage, not schema, so they may differ per client.
- The host owns the *document lifecycle*: create the `Y.Doc` and the `HocuspocusProvider` when the page mounts, destroy the provider on unmount (`onBeforeUnmount(() => provider.destroy())`), and call `forgetDocument(id)` of the `EditorServer` handle on the server before deleting a document (see [Deleting a document](https://skaile-ai.github.io/forge-editor/guides/editor-ports#deleting-a-document)). The library never creates a provider for you.
- A plain-Vue or React app brings its own Hocuspocus server, builds its schema from `/schema/advanced` and sets the Collaboration `field` to `default`. A host block of your own (see [Backend and collaboration](https://skaile-ai.github.io/forge-editor/tutorial/backend-and-collaboration)) belongs to that schema too: pass its framework-free node in the server's `schema` option (`schema: () => [...advancedSchemaExtensions(), YourNode]`); a custom server builds `getSchema([...advancedSchemaExtensions(), YourNode])` itself.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
