Skip to content

Real-time collaboration

ProseKit integrates with two CRDT libraries for real-time collaboration: Yjs and Loro. Each ships as its own extension that binds a CRDT document to the editor. Network transport, persistence, and presence are handled by the underlying CRDT, while ProseKit only wires it into ProseMirror.

See the live editors:

npm install yjs y-prosemirror

For WebSocket sync, also install a provider:

npm install y-websocket
import 'prosekit/extensions/yjs/style.css'

import { defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
} from 'prosekit/basic'
import { createEditorfunction createEditor<E extends Extension>(options: EditorOptions<E>): Editor<E>
@public
, unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
} from 'prosekit/core'
import { defineYjsfunction defineYjs(options: YjsOptions): YjsExtension
@public
} from 'prosekit/extensions/yjs'
import { WebsocketProviderclass WebsocketProvider
Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. The document name is attached to the provided url. I.e. the following example creates a websocket connection to http://localhost:1234/my-document-name
@example import * as Y from 'yjs' import { WebsocketProvider } from 'y-websocket' const doc = new Y.Doc() const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)@extendsObservableV2<{ 'connection-close': (event: CloseEvent | null, provider: WebsocketProvider) => any, 'status': (event: { status: 'connected' | 'disconnected' | 'connecting' }) => any, 'connection-error': (event: Event, provider: WebsocketProvider) => any, 'sync': (state: boolean) => any }>
} from 'y-websocket'
import * as Yimport Y from 'yjs' const ydocconst ydoc: Y.Doc = new Yimport Y.Doc
new Doc({ guid, collectionid, gc, gcFilter, meta, autoLoad, shouldLoad }?: DocOpts): Y.Doc
export Doc
@paramopts configuration
()
const providerconst provider: WebsocketProvider = new WebsocketProvider
new WebsocketProvider(serverUrl: string, roomname: string, doc: Y.Doc, { connect, awareness, params, protocols, WebSocketPolyfill, resyncInterval, maxBackoffTime, disableBc }?: {
    connect?: boolean | undefined;
    awareness?: Awareness | undefined;
    params?: {
        [x: string]: string;
    } | undefined;
    protocols?: string[] | undefined;
    WebSocketPolyfill?: {
        new (url: string | URL, protocols?: string | string[] | undefined): WebSocket;
        prototype: WebSocket;
        readonly CLOSED: number;
        readonly CLOSING: number;
        readonly CONNECTING: number;
        readonly OPEN: number;
    } | undefined;
    resyncInterval?: number | undefined;
    maxBackoffTime?: number | undefined;
    disableBc?: boolean | undefined;
}): WebsocketProvider
@paramserverUrl@paramroomname@paramdoc@paramopts@paramopts.connect@paramopts.awareness@paramopts.params specify url parameters@paramopts.protocols specify websocket protocols@paramopts.WebSocketPolyfill Optionall provide a WebSocket polyfill@paramopts.resyncInterval Request server state every `resyncInterval` milliseconds@paramopts.maxBackoffTime Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)@paramopts.disableBc Disable cross-tab BroadcastChannel communication
(
'wss://your-server.example/sync', 'my-room', ydocconst ydoc: Y.Doc, ) const editorconst editor: Editor<Union<readonly [BasicExtension, YjsExtension]>> = createEditorcreateEditor<Union<readonly [BasicExtension, YjsExtension]>>(options: EditorOptions<Union<readonly [BasicExtension, YjsExtension]>>): Editor<Union<readonly [BasicExtension, YjsExtension]>>
@public
({
extensionEditorOptions<Union<readonly [BasicExtension, YjsExtension]>>.extension: Union<readonly [BasicExtension, YjsExtension]>
The extension to use when creating the editor.
: unionunion<readonly [BasicExtension, YjsExtension]>(exts_0: BasicExtension, exts_1: YjsExtension): Union<readonly [BasicExtension, YjsExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
(
defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
(),
defineYjsfunction defineYjs(options: YjsOptions): YjsExtension
@public
({ docYjsOptions.doc: Y.Doc
The Yjs instance handles the state of shared data.
: ydocconst ydoc: Y.Doc, awarenessYjsOptions.awareness: Awareness
The Awareness instance.
: providerconst provider: WebsocketProvider.awarenessWebsocketProvider.awareness: Awareness }),
), })
provider.awareness.setLocalStateField('user', {
  name: 'Alice',
  color: '#2563eb',
})

The Yjs extension's CSS handles the visual rendering of remote cursors and selections.

For local persistence, pair the provider with y-indexeddb:

npm install y-indexeddb
import { IndexeddbPersistence } from 'y-indexeddb'

const persistence = new IndexeddbPersistence('my-room', ydoc)

Yjs handles offline edits automatically: changes queue locally and sync once the WebSocket reconnects.

npm install loro-crdt loro-prosemirror

defineLoro accepts the Loro document plus exactly one of awareness or presence (cursor / presence info). Pick whichever your app already uses to track collaborators.

import 'prosekit/extensions/loro/style.css'

import { LoroDoc
class LoroDoc<T extends Record<string, Container> = Record<string, Container>>
interface LoroDoc<T extends Record<string, Container> = Record<string, Container>>
The CRDTs document. Loro supports different CRDTs include [**List**](LoroList), [**RichText**](LoroText), [**Map**](LoroMap) and [**Movable Tree**](LoroTree), you could build all kind of applications by these. **Important:** Loro is a pure library and does not handle network protocols. It is the responsibility of the user to manage the storage, loading, and synchronization of the bytes exported by Loro in a manner suitable for their specific environment.
@example```ts import { LoroDoc } from "loro-crdt" const loro = new LoroDoc(); const text = loro.getText("text"); const list = loro.getList("list"); const map = loro.getMap("Map"); const tree = loro.getTree("tree"); ```
} from 'loro-crdt'
import { CursorAwarenessclass CursorAwareness, type LoroDocType
type LoroDocType = LoroDoc<{
    doc: LoroMap<LoroNodeContainerType>;
    data: LoroMap;
}>
} from 'loro-prosemirror'
import { defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
} from 'prosekit/basic'
import { createEditorfunction createEditor<E extends Extension>(options: EditorOptions<E>): Editor<E>
@public
, unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
} from 'prosekit/core'
import { defineLorofunction defineLoro(options: LoroOptions): LoroExtension
@public
} from 'prosekit/extensions/loro'
const docconst doc: LoroDocType: LoroDocType
type LoroDocType = LoroDoc<{
    doc: LoroMap<LoroNodeContainerType>;
    data: LoroMap;
}>
= new LoroDoc
new LoroDoc<{
    doc: LoroMap<LoroNodeContainerType>;
    data: LoroMap;
}>(): LoroDoc<{
    doc: LoroMap<LoroNodeContainerType>;
    data: LoroMap;
}>
Create a new loro document. New document will have a random peer id.
()
const awarenessconst awareness: CursorAwareness = new CursorAwarenessnew CursorAwareness(peer: PeerID, timeout?: number): CursorAwareness(docconst doc: LoroDocType.peerIdStrLoroDoc<{ doc: LoroMap<LoroNodeContainerType>; data: LoroMap; }>.peerIdStr: `${number}`
Get peer id in decimal string.
)
const editorconst editor: Editor<Union<readonly [BasicExtension, LoroExtension]>> = createEditorcreateEditor<Union<readonly [BasicExtension, LoroExtension]>>(options: EditorOptions<Union<readonly [BasicExtension, LoroExtension]>>): Editor<Union<readonly [BasicExtension, LoroExtension]>>
@public
({
extensionEditorOptions<Union<readonly [BasicExtension, LoroExtension]>>.extension: Union<readonly [BasicExtension, LoroExtension]>
The extension to use when creating the editor.
: unionunion<readonly [BasicExtension, LoroExtension]>(exts_0: BasicExtension, exts_1: LoroExtension): Union<readonly [BasicExtension, LoroExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
(
defineBasicExtensionfunction defineBasicExtension(): BasicExtension
Define a basic extension that includes some common functionality. You can copy this function and customize it to your needs. It's a combination of the following extension functions: - {@link defineDoc } - {@link defineText } - {@link defineParagraph } - {@link defineHeading } - {@link defineList } - {@link defineBlockquote } - {@link defineImage } - {@link defineHorizontalRule } - {@link defineHardBreak } - {@link defineTable } - {@link defineCodeBlock } - {@link defineItalic } - {@link defineBold } - {@link defineUnderline } - {@link defineStrike } - {@link defineCode } - {@link defineLink } - {@link defineBaseKeymap } - {@link defineBaseCommands } - {@link defineHistory } - {@link defineGapCursor } - {@link defineVirtualSelection } - {@link defineModClickPrevention }
@public
(),
defineLorofunction defineLoro(options: LoroOptions): LoroExtension
@public
({ docLoroOptions.doc: LoroDocType
The Loro instance handles the state of shared data.
, awarenessLoroOptions.awareness?: CursorAwareness | undefined
The (legacy) Awareness instance. One of `awareness` or `presence` must be provided.
}),
), })

Loro's transport is bring-your-own. The CRDT exposes binary updates you forward over any channel (WebSocket, libp2p, BroadcastChannel, etc):

const ws = new WebSocket('wss://your-server.example/loro')

doc.subscribe((event) => {
  if (event.by === 'local') {
    const update = doc.export({ mode: 'update', from: event.fromVersion })
    ws.send(update)
  }
})

ws.onmessage = (msg) => {
  doc.import(new Uint8Array(msg.data))
}
const snapshot = doc.export({ mode: 'snapshot' })
localStorage.setItem('doc.loro', JSON.stringify(Array.from(snapshot)))

// later:
const stored = localStorage.getItem('doc.loro')
if (stored) {
  doc.import(new Uint8Array(JSON.parse(stored)))
}