Report an issue

guideReal-time collaboration feature integration

Welcome to the quick start guide for the real time-collaboration integration. This tutorial was designed to provide you with the most crucial information. This will let you set up real-time collaboration inside your editor, including basic integration with other collaboration features.

After reading this article, we recommend visiting the “Learn more” sections in our documentation pages for the comments, track changes and revision history features to get more in-depth knowledge about these features.

Complementary to this guide, we provide ready-to-use samples available for download. We prepared samples for all editor types (multi-root included) as well as for the React, Angular and Vue.js integrations. You may use them as an example or as a starting point for your own integration.

# Sign up to the collaboration service

The real-time collaboration feature needs a server to synchronize content between the clients, so in order to use it, you need to sign up to the collaboration service first. Refer to the CKEditor Cloud Services Collaboration – Quick Start guide for more details. There is also Premium features free trial available for testing purposes, that will provide the necessary backend.

# Prepare editor build

Once you have the server ready, the next step is to create a custom CKEditor 5 build. It needs to include the real-time collaboration feature, because it is not present in any of the predefined builds. The following instructions describe this process step-by-step.

Real-time collaboration is a complex topic and despite having over 10.000 tests we cannot guarantee that no error will show during a long collaboration session. This is why we recommend to use the watchdog feature. It is a helpful utility which ensures that an editor instance is running and, in case of an error, tries to restore the editor to a working state. We highly recommend it using watchdog, even though using collaboration features without it is still possible.

First, clone the editor build that you want to integrate.

git clone -b stable https://github.com/ckeditor/ckeditor5

This example will use the classic build, however, real-time collaboration, like every other official CKEditor 5 plugin, will work with any of the rich text editor predefined builds.

Change your directory to the ckeditor5-build-classic package. All files and commands are relative to this directory.

Install the package:

cd ckeditor5/packages/ckeditor5-build-classic

npm install

To add the real-time collaborative features to your editor, install the @ckeditor/ckeditor5-real-time-collaboration and @ckeditor/ckeditor5-watchdog packages:

npm install --save-dev \
    @ckeditor/ckeditor5-real-time-collaboration \
    @ckeditor/ckeditor5-watchdog

Then, import and enable the plugins you want to use and export both the editor and the watchdog. The updated src/ckeditor.js should look like this:

import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog';
import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices';
import EasyImageUpload from '@ckeditor/ckeditor5-easy-image/src/easyimage';
import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices';

import RealTimeCollaborativeEditing from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativeediting';


// The following plugins enable real-time collaborative comments.
// You do not need to import them if you do not want this integration.
import Comments from '@ckeditor/ckeditor5-comments/src/comments';
import RealTimeCollaborativeComments from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativecomments';

// The following plugins enable real-time collaborative track changes and are optional.
// They depend on the `Comments` and `RealTimeCollaborativeComments` from above, so make sure to include
// them in the editor plugins if you want to integrate the real-time collaborative track changes.
// You do not need to import them if you do not want this integration.
import TrackChanges from '@ckeditor/ckeditor5-track-changes/src/trackchanges';
import RealTimeCollaborativeTrackChanges from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativetrackchanges';

// The following plugins enable revision history for real-time collaboration.
// You do not need to import them if you do not want this integration.
import RevisionHistory from '@ckeditor/ckeditor5-revision-history/src/revisionhistory';
import RealTimeCollaborativeRevisionHistory from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativerevisionhistory';

// The following plugin enables the users presence list and is optional.
// You do not need to import it if you do not want to integrate the user list.
import PresenceList from '@ckeditor/ckeditor5-real-time-collaboration/src/presencelist';

class ClassicEditor extends ClassicEditorBase {}

// Plugins to include in the build.
ClassicEditor.builtinPlugins = [
    Essentials, Paragraph, Bold, Italic, Image, ImageUpload, ImageToolbar, EasyImageUpload, CloudServices,
    // Remove plugins from here if you have not imported them.
    RealTimeCollaborativeEditing,
    Comments, RealTimeCollaborativeComments,
    TrackChanges, RealTimeCollaborativeTrackChanges,
    RevisionHistory, RealTimeCollaborativeRevisionHistory
    PresenceList
];

// Editor configuration.
ClassicEditor.defaultConfig = {
    language: 'en'
};

export default { ClassicEditor, EditorWatchdog };

After the above steps are done, your custom build needs to be bundled using webpack:

npm run build

Read more about installing plugins in the dedicated Installing plugins guide.

# Prepare the HTML structure

Having configured the plugins, you need to place your bundle in an HTML document and create the editor.

For the sake of completeness, the examples below implement the wide sidebar display mode for comments and suggestions annotations. If you want to use the inline display mode, remove parts of the snippets that set up the wide sidebar.

Your HTML structure will also differ depending on whether you would like to integrate the revision history feature or not.

Below, you will find an example of an HTML structure for an integration with sidebar and without the revision history feature:

<div id="presence-list-container"></div>

<div class="container">
    <div id="editor"><p>Let's edit this together!</p></div>
    <div class="sidebar" id="sidebar"></div>
</div>

The revision history feature requires a more complex HTML structure:

<div id="presence-list-container"></div>

<div id="editor-container">
    <div class="container">
        <div id="editor"><p>Let's edit this together!</p></div>
        <div class="sidebar" id="sidebar"></div>
    </div>
</div>

<div id="revision-viewer-container">
    <div class="container">
        <div id="revision-viewer-editor"></div>
        <div class="sidebar" id="revision-viewer-sidebar"></div>
    </div>
</div>

# Configure and initialize the editor

To finalize the editor configuration, you need to define the CKEditor Cloud Services connection data, set the channelId, and set up the containers for the features and add buttons to the toolbar.

If you do not have your CKEditor Cloud Services URLs ready, read more about these first in the CKEditor Cloud Services Collaboration – Quick Start guide.

Below, you will find an updated HTML file (sample/index.html) with a minimal editor configuration and the CSS+HTML structure prepared for integration with the revision history feature.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>CKEditor 5 Collaboration – Hello World!</title>

        <style>
            body {
                font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
            }

            .container {
                display: flex; /* Create a column layout. */
                position: relative;
                width: 1000px; /* Set some size for the whole editor + sidebar (can be any size). */
            }

            .container > .ck.ck-editor {
                width: 100%;  /* Stretch the editable area. */
            }

            .sidebar {
                border: 1px solid #c4c4c4;
                margin-left: -1px;
                background: #fafafa;
            }

            #sidebar {
                /* Set some size and spacing for the sidebar (can be any size). */
                min-width: 300px;
                padding: 0 10px;
                overflow: hidden; /* Hide annotations moved above top sidebar border. */
            }

            #revision-viewer-container {
                display: none; /* Revision history viewer is initially hidden. */
            }

            #revision-viewer-sidebar {
                min-width: 320px; /* 20px more than #sidebar due to paddings. */
            }
        </style>
    </head>

    <body>
        <!-- Use simpler CSS and HTML structure if you do not want to integrate revision history feature. !-->
        <div id="presence-list-container"></div>

        <div id="editor-container">
            <div class="container">
                <div id="editor"><p>Let's edit this together!</p></div>
                <div class="sidebar" id="sidebar"></div>
            </div>
        </div>

        <div id="revision-viewer-container">
            <div class="container">
                <div id="revision-viewer-editor"></div>
                <div class="sidebar" id="revision-viewer-sidebar"></div>
            </div>
        </div>

        <script src="../build/ckeditor.js"></script>
        <script>
            const { ClassicEditor: Editor, EditorWatchdog } = ClassicEditor;

            const watchdog = new EditorWatchdog( Editor );

            watchdog.create( document.querySelector( '#editor' ), {
                initialData: '<p>Let\'s edit this together!</p>',
                // Do not include buttons for the features that you do not want to integrate.
                // If you use the image toolbar, you might want to add the `'comment'` button to the image toolbar, too.
                toolbar: [ 'bold', 'italic', 'uploadImage', '|', 'comment', 'trackChanges', '|', 'revisionHistory' ],
                cloudServices: {
                    // PROVIDE CORRECT VALUES HERE:
                    tokenUrl: 'https://example.com/cs-token-endpoint',
                    uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/',
                    webSocketUrl: 'your-organization-id.cke-cs.com/ws/'
                },
                collaboration: {
                    channelId: 'document-id'
                },
                sidebar: {
                    container: document.querySelector( '#sidebar' )
                },
                presenceList: {
                    container: document.querySelector( '#presence-list-container' )
                },
                // Add configuration for the Comments if the Comments plugin is added.
                comments: {
                    editorConfig: {
                        extraPlugins: ClassicEditor.builtinPlugins.filter( plugin => {
                            return [ 'Bold', 'Italic', 'Underline', 'List', 'Autoformat' ].includes( plugin.pluginName );
                        } )
                    }
                }
                // Do not include revision history configuration if you do not want to integrate it.
                revisionHistory: {
                    editorContainer: document.querySelector( '#editor-container' ),
                    viewerContainer: document.querySelector( '#revision-viewer-container' ),
                    viewerEditorElement: document.querySelector( '#revision-viewer-editor' ),
                    viewerSidebarContainer: document.querySelector( '#revision-viewer-sidebar' )
                }
            } );
        </script>
    </body>
</html>

Voilà! All users who open this page should be able to collaborate, working on the same rich text document at the same time.

# The channelId configuration property

The config.collaboration.channelId configuration property is an important property that controls which editor instances collaborate with one another. All clients created with the same channelId will be collaborating on the same content.

Each document must have a different channelId. This ID is usually the primary key of the document in the database or a unique identifier for a given form field. However, you are free to provide whatever identifier fits your scenario.

Note that to start the collaboration on a new document, you need the document ID since the very beginning, when the editor is created for the first time. If your application creates document IDs later, for instance when the form is submitted, you may need to provide another method to generate document IDs (for example, some random unique IDs generator).

The channelId needs to be unique in a given environment, so if you are using, for instance, staging and production environments, you may use the same channel ID in these. Since the environments are separated, these ID will not interfere with one another.

Since editor content is linked with and only with the channel ID, it is possible to create an application where different views (forms, subpages, etc.) provide various sets of rich-text editable fields and have users collaborate with each other even though they have opened a different form, etc.

# Handling document data

To make the integration work out of the box, all comments, suggestions and revisions data is saved in CKEditor Cloud Services.

In this section, it is assumed that Cloud Services document storage is not enabled.

Document storage has numerous advantages and we recommend enabling it if available in your case.

Note that CKEditor Cloud Services does not save or load the document content. Instead, Cloud Services acts as a medium to handle the collaboration process. Content management should be handled by the application integration.

We understand that due to legislation or your company’s policy, you may be required to store all data in your own data center or private cloud. This is why we also provide the on-premises version of CKEditor Cloud Services. Contact us to learn more about it.

# Data initialization

When the first user opens the rich text editor for a certain document ID, their content is sent to CKEditor Cloud Services. In case of the example above it is:

<p>Let's edit this together!</p>

If you cannot or do not want to set the initial data straight in the HTML, use the initialData configuration option instead. Do not use editor.setData() (see below).

After the first user initializes the document and starts the editing session, every other user that connects to the same document will receive the content from CKEditor Cloud Services (while their local initial data is discarded).

From that moment on, every single change to the editor content is passed between Cloud Services and clients and the document for each client is updated. Any conflicting updates are resolved on-the-fly.

This is the reason why you should not use the editor.setData() or editor.data.set() methods when using the collaboration plugin. These simply overwrite the whole content of the editor (even if the data to set is the same as the current editor data). In real-time collaboration, this behavior can result in overwriting other clients’ local updates. In most cases, using editor.setData() and editor.data.set() in real-time collaboration is incorrect.

If you are sure that you understand and accept the behavior and side effects of setting the editor data this way, you can call editor.data.set() with the flag suppressErrorInCollaboration set to true:

editor.data.set( '<p>Your data</p>', { suppressErrorInCollaboration: true } );

This will let you overwrite the editor data without throwing an error.

# Editor initial data and revision data

When the revision history feature is used, it is crucial to keep the revisions state in synchronization with the document data. For this reason, if there are any revisions saved for that document, the editor initial data will be discarded and data saved with the most recent revision will be used instead.

Since the editor initial data is discarded, you can limit the data load by not sending the document data to the browser (it applies to non-new documents, i.e. documents which were already initialized and for which revisions were created).

If the editor initial data is set (by passing a non-empty DOM element or a string to Editor.create(), or by setting the initialData configuration value), and it is different than the revision data, a warning will be logged in the console.

# Saving data

Although CKEditor Cloud Services handles passing data between clients, you are still responsible for saving the final data in your database. The document is stored in the cloud by CKEditor Cloud Services only temporarily, as long as there are connected users. It means that the editor content should be saved in the database on your server before the last user disconnects — otherwise it will be lost. It is recommended to save the data automatically whenever it changes.

CKEditor 5 provides two utilities to make your integration simpler.

# The cloudDocumentVersion property

When your collaborative users are saving the same document at the same time, there might be a conflict. It might happen that one user will try to save an older document version, overwriting a newer one.

To prevent such race conditions, use the cloudDocumentVersion property.

editor.plugins.get( 'RealTimeCollaborationClient' ).cloudDocumentVersion

This property is simply a number, and a bigger value means a newer document version. This number should be stored in the database together with the document content. When a client wants to save the content, document versions should be compared. The document should only be saved when the version is higher. Otherwise, it means that the incoming data is an older version of the document and should be discarded.

CKEditor Cloud Services keeps the value of cloudDocumentVersion and makes sure that it is properly set whenever there is a new editing session for a given document.

# The Autosave plugin

Using the autosave feature with large documents may result in a lot of data being sent to your server and can have a negative impact on the editor user experience.

We recommend using the document storage feature instead.

The second helper is the Autosave plugin.

The autosave plugin triggers the save callback whenever the user changes the content. It takes care of throttling the callback execution to limit the number of calls to the database. It also automatically secures the user from leaving the page before the content is saved.

The autosave plugin is by default not included in any of the predefined builds nor is it automatically required by the real-time collaboration plugins, so you need to install it separately. To learn more about this plugin, refer to the Getting and saving data guide.

# Autosave for revision history

If you are using the revision history feature, your autosave callback should take care of updating the recent revision data. See the example below:

autosave: {
    save: async editor => {
        const revisionTracker = editor.plugins.get( 'RevisionTracker' );
        const currentRevision = revisionTracker.currentRevision;
        const oldRevisionVersion = currentRevision.toVersion;

        // Update the current revision with the newest document changes.
        await revisionTracker.update();

        // Check if the revision was updated.
        // If not, do not make an unnecessary call.
        if ( oldRevisionVersion === currentRevision.toVersion ) {
            return true;
        }

        // Use the document data saved with the revision instead of the editor data.
        // Revision data may slightly differ from the editor data when
        // real-time collaboration is involved.
        const documentData = await revisionTracker.getRevisionDocumentData( revisionTracker.currentRevision );

        // Use revision version instead of `cloudDocumentVersion`.
        const documentVersion = currentRevision.toVersion;

        // `saveData()` should save the document in your database.
        // `documentData` contains data for all roots.
        // You can save just `documentData.main` if you are using a single root,
        // or the whole object if you are using multiple roots.
        return saveData( documentData.main, documentVersion );
    }
}

You can read more about saving revisions in the Revision history guide.

# Reconnection process

During the real-time collaboration, it may happen that the internet connection will go down for some time. When this happens, the editor will switch into read-only mode. After the connection is back, the real-time collaboration plugin will try to reconnect the editor back to the editing session. This reconnection process may involve re-creating the editing session (if the reconnection happens after a longer period of time).

While one client is offline, other clients may perform changes to the document. When the offline client reconnects, the real-time editing plugin will make a decision if the reconnection is possible.

# Users selection

The real-time collaborative editing feature not only synchronizes the document content between all participants, but it also shows each user the list of all other users in real time. It works out of the box and does not require any additional configuration. This is the only part of the real-time collaborative editing plugin that provides a UI. Refer to the Users in real-time collaboration guide to learn more.