Introduction to Real-Time Collaboration (RTC)

TinyMCE’s Real-time Collaboration (RTC) plugin will be retired and deactivated on December 31, 2023, and is no longer available for purchase.

The Real-Time Collaboration (RTC) plugin for TinyMCE allows 2 or more users to collaborate on the same content in TinyMCE at the same time.

The RTC plugin is designed to use the Tiny Cloud for communicating between the collaborator’s editors, with all data encrypted by the user’s web browser using a local encryption key. This ensures that no-one, including Tiny, can read the user’s content. The content in the editor can still be retrieved as HTML so TinyMCE can still be used for web forms, creating and editing content in Content Management Systems (CMS) and Learning Management Systems (LMS), or any application requiring a rich-text editor.

Interactive example

The following example shows two editors that are collaborating using the TinyMCE Real-Time Collaboration plugin. All network requests made by these editors, real or simulated, are being logged to the browser console. To view the network requests, open the browser console using the F12 keyboard key and navigate to the Console tab.

  • TinyMCE

  • HTML

  • JS

Current user:
Collaborator (user in the other editor):

Current user:
Collaborator (user in the other editor):

<div style="min-height: 470px;">
  <p style="margin-top: 20px;"><strong>Current user:</strong> <span id=fakedemouser1></span><br><strong>Collaborator
      (user in the other editor):</strong> <span id=otherfakeuser1></span></p>
  <div id="editor1" style="display: none;" data-rtc-editor-parent>
    <textarea id="rtc-editor-1" data-rtc-editor>
      <h2 style="text-align: center;">Welcome to the TinyMCE editor demo!</h2>
    </textarea>
  </div>
</div>
<div style="min-height: 470px;">
  <p style="margin-top: 20px;"><strong>Current user:</strong> <span id=fakedemouser2></span><br><strong>Collaborator
      (user in the other editor):</strong> <span id=otherfakeuser2></span></p>
  <div id="editor2" style="display: none;" data-rtc-editor-parent>
    <textarea id="rtc-editor-2" data-rtc-editor>
      <h2 style="text-align: center;">Welcome to the TinyMCE editor demo!</h2>
    </textarea>
  </div>
</div>
/*
 * Initial content for the editor, to be loaded into the editor using the
 * optional `rtc_initial_content_provider` option. This could pulled from
 * a database when using the editor in production.
 */
const initialEditorContent = '<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p><h2 style="text-align: center;">Welcome to the TinyMCE Real-Time Collaboration demo!</h2><p>This editor is collaborating with the other editor on the page. Try editing the content by adding images, lists, or any other currently supported content, it should appear in the other editor too!</p><p>All network requests made by this demo, fake or real, are logged in the browser console using <a href="https://netflix.github.io/pollyjs" target="_blank" rel="noopener">Polly.js</a> (the browser console is typically accessed using the F12 key).</p><h2>Got questions or need help?</h2><ul><li>Our <a class="mceNonEditable" href="https://www.tiny.cloud/docs/tinymce/6/">documentation</a> is a great resource for learning how to configure TinyMCE.</li><li>Have a specific question? Try the <a href="https://stackoverflow.com/questions/tagged/tinymce" target="_blank" rel="noopener"><code>tinymce</code> tag at Stack Overflow</a>.</li></ul><h2>Found a bug?</h2><p>If you think you have found a bug please create an issue on the <a href="https://github.com/tinymce/tinymce/issues">GitHub repo</a> to report it to the developers.</p><h2>Finally,</h2><p>Thanks for supporting TinyMCE! We hope it helps you and your users create great content.<br>All the best from the TinyMCE team.</p>';

/* Set up a connected users object for maintaining a list of connected users */
const connectedUsers = {};

tinymce.init({
  selector: 'textarea#rtc',
  plugins:
    'rtc advlist charmap emoticons help image insertdatetime link ' +
    'lists powerpaste save visualblocks wordcount',
  menubar: 'file edit insert view format table tools help',
  toolbar:
    'undo redo | blocks | bold italic underline | ' +
    'alignleft aligncenter alignright | bullist numlist | insert | help',
  height: 400,
  toolbar_groups: {
    insert: {
      icon: 'plus',
      tooltip: 'Insert',
      items: 'link | charmap emoticons | image | insertdatetime',
    },
  },
  rtc_document_id: documentID,
  rtc_encryption_provider: ({ documentId, keyHint }) =>
    fetch('https://api.example/getEncryptionKey/', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ documentId, keyId: keyHint }),
    })
      .then((response) => response.json())
      .catch((error) =>
        console.log('Failed to return encryption key\n' + error)
      ),
  rtc_token_provider: () =>
    fetch('https://api.example/getJwtToken/', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ userID }),
    })
      .then((response) => response.json())
      .catch((error) => console.log('Failed to return a JWT\n' + error)),
  rtc_user_details_provider: ({ userId }) =>
    fetch('https://api.example/getUserDetails/', {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ userId }),
    })
      .then((response) => response.json()),
  rtc_initial_content_provider: () =>
    Promise.resolve({ content: initialEditorContent }),
  rtc_client_connected: ({
    userDetails,
    userId,
    caretNumber,
    clientId,
  }) => {
    connectedUsers[clientId] = {
      caretNumber,
      userDetails,
      userId,
    };
    /* Adds the collaborator above the editor and logs details to the console */
    document.getElementById(collaboratorUsernameElem).innerText =
      userDetails.fullName;
    console.log(
      `Fake user ${userDetails.fullName} (${userDetails.email}) connected with caret number ${caretNumber}`
    );
  },
  rtc_client_disconnected: ({ clientId, userDetails }) => {
    delete connectedUsers[clientId];
    /* Removes collaborator from above the editor and logs to the console */
    document.getElementById(collaboratorUsernameElem).innerText =
      userDetails.fullName;
    console.log(
      `Fake user ${userDetails.fullName} (${userDetails.email}) disconnected`
    );
  },
});

Features of TinyMCE Real-Time Collaboration

End-to-end encryption

The Real-Time Collaboration (RTC) plugin encrypts all content sent between clients. Clients are assigned a random presence ID when they connect, which is used to transmit their cursor position, along with their JWT user ID. This means the TinyMCE cloud services can not read any data transferred or know who is editing. Content and user data is only available to the page running TinyMCE.

JSON Web Token based authentication

Some cloud services for TinyMCE require setting up JSON Web Token (JWT) authentication. JWTs are a common solution for communicating user authorization with web services. JWTs are used to communicate to TinyMCE that the user has been authorized to access Tiny Cloud services.

For general information on JWTs, visit: https://jwt.io/.

For information on using JWT authentication with the Real-Time Collaboration (RTC) plugin, see: JWT authentication.

User Presence API

The Real-Time Collaboration (RTC) plugin exports a presence API to enable tracking when users enter and leave the collaboration session. The only user information shared through the RTC server is the user id stored in the JWT sub claim. Other details such as the user’s full name are resolved locally so the Tiny Cloud will never see who is actually connecting. User resolution is performed through the rtc_user_details_provider option. Presence events can be received through either configuration callbacks or editor events.

Overview of how TinyMCE Real-Time Collaboration works

Conceptual diagram showing how TinyMCE RTC works

When a new document is created

  1. The initial content is set using the HTML within the element replaced by the editor, or using the initial content option.

  2. The editor requests and receives the following on behalf of the user:

    • A JSON Web Token (JWT) from your server.

    • The encryption details from your server.

The JWT and encryption details are stored in the browser until required.

When the editor content is changed by a user

  1. The editor encrypts the content using the encryption details.

  2. The encrypted content and the JWT (but not the encryption details) are sent to the RTC server.

  3. The RTC server verifies that the JWT was signed by the same private key as the public key stored on the RTC server.

  4. Once verified, the content is sent to collaborating editors where the editor will decrypt the content using the encryption details provided when the user opened the editor.

  5. Once decrypted, the plugin will merge the local content and the content from the server.

  6. When the content is submitted, it will be sent to your server. If snapshotting option is configured, no submission is needed as snapshots of the content will be sent to your server from the editors automatically.

Getting started with Real-Time Collaboration

For instructions for getting started with TinyMCE Real-Time Collaboration, see: Getting started with Real-Time Collaboration (RTC).