Collaborative editing for Confluence Server

Collaborative editing allows multiple people to concurrently edit a single Confluence page or blog post (we'll just call them pages from here on). When using collaborative editing, a page editor will see the avatar(s) of others editing the page, and the changes they make will appear in the editor in real time.

This page provides an overview of the various components that make up collaborative editing in Confluence Server. We'll go through how each component works, and how you can build plugins to integrate with them.

If you're developing for Confluence Cloud head to Collaborative editing for Confluence Cloud.

Shared drafts

The shared draft vs personal draft model

We're changing the way drafts behave, and moving to a model that's more consistent with how content should be stored for collaborative editing. We'll call the outgoing model, used prior to collaborative editing, 'personal drafts', and the new model 'shared drafts'. The notable differences are:

  • When editing a page prior to collaborative editing a new personal draft was created for each user, and the changes couldn't be seen by other users as personal drafts are private. Using collaborative editing a single shared draft is created for each page, and anyone editing the page will see the same draft.
     
  • Personal drafts were represented by a Draft class, which extends ContentEntityObject and contains a draftType field indicating whether the draft was a "page" or "blogpost". We're deprecating the Draft class in favour of using the existing Page and Blogpost classes, where a contentStatus field of "draft" will indicate the object is a draft.

Comparing the personal draft and shared draft models:

Personal drafts (before collaborative editing) Shared drafts (collaborative editing)

A draft page object in its different representations:

Personal draft Shared draft

Class: Draft

ContentStatus: N/A

DraftType: "page" or "blogpost"

Class: Page or Blogpost

ContentStatus: "draft"

DraftType: N/A

Shared drafts API

REST API

We provide a REST API that allows external interaction with shared drafts. The currently-supported operations are:

  • Creating a shared draft
  • Fetching a single or a list of shared drafts
  • Publishing a draft

The Drafts API extends Confluence's existing REST API. In order to reference the draft, you'll need to add the following query parameter to REST requests:

?status=draft

For example, if the request to fetch an existing page is:

GET /rest/api/content/{contentId}

The request to fetch the corresponding draft will be:

GET /rest/api/content/{contentId}?status=draft

Similarly, publishing a draft will be:

PUT /rest/api/content/{contentId}?status=draft

where the request payload will contain:

{
   ...
   "status": "current"
   ...
}

indicating that the status should be updated from "draft" to "current".

When publishing drafts, it's also possible to specify a "conflictPolicy" query parameter. Currently only the "abort" policy is supported. This means that the publish operation will be aborted (and an error code returned) if a conflict is encountered during the publishing of a draft.

You can find more detailed documentation for the Confluence Server REST API at https://docs.atlassian.com/atlassian-confluence/REST/latest-server/

Java API

Draft operations are also exposed as a Java interface via the ContentService and ContentDraftService. These services are made available to plugins as Spring Beans with apiContentService and apiContentDraftService IDs.

You can create a shared draft by calling:

contentService
    .create(Content
        .builder()
        .status(ContentStatus.DRAFT)
        ...
        .build());

Note that the API currently only supports creating new shared drafts – it doesn't support creating a shared draft for an existing page.

To fetch a shared draft:

contentService
    .find()
    .withStatus(ContentStatus.DRAFT)
    ...
    .fetchOne();

Finally, to publish a draft:

contentDraftService
    .publishEditDraft(Content
        .builder()
        .status(ContentStatus.CURRENT)
        ...
        .build(), ConflictPolicy.ABORT);

Event list

Event Type Purpose Example
SharedDraftCreatedEvent Java Triggered when a new shared draft gets created. Event Listener Module
SharedDraftUpdatedEvent Java Triggered when a shared draft is updated. This normally happens at regular intervals in an editing session when a draft is auto-saved. Event Listener Module
rte-draft-saved Javascript Triggered when a draft is saved. This is similar to SharedDraftUpdatedEvent, except that it's fired from the client side and is triggered for both personal and shared drafts. AJS.bind('rte-draft-saved', <handler function>);

Extending the XWork actions

Shared draft support is provided to actions that extend the AbstractCreateAndEditPageAction.

Prior to shared drafts, actions could access drafts by calling getDraft(). This method – to be deprecated – only returns the old Draft class, which is incompatible with shared drafts. In order to support shared drafts you need to replace calls to getDraft with getDraftAsCEO, which will return a shared draft on instances that support them or a personal draft on other instances. This allows actions to maintain compatibility with instances that haven't been upgraded to collaborative editing.

You should ensure that future calls are made to getContentDraft, which will only return shared drafts and doesn't provide support for personal drafts.

Checking for shared drafts using Javascript

To find out if shared drafts are supported using client-side Javascript:

AJS.Meta.getBoolean('shared-drafts')

What happens to personal drafts once shared drafts have been rolled out?

Personal drafts will still be accessible from users' drafts list. They won't, however, be able to resume editing the draft in the editor. Opening a personal draft will display it in a read-only dialog, from which the user can copy the content into a new page.

How do shared drafts behave in the drafts list?

A user's drafts list will be populated with the following drafts:

  • Personal drafts, created by the user
  • Shared drafts created by the user but never published

Drafts that have a corresponding published page won't appear in the drafts list – users can access those drafts by editing the corresponding page.

Restoring historical versions

When a page is reverted, the corresponding shared draft is also updated to contain the new page content. This will also trigger an 'external changes' request (see our explanation of external changes).

Location persistence

Shared drafts will now implement the hierarchical interface (inherited from the Page and Blogpost classes), which means it's now possible to persist location onto drafts.

Drafts created using the AbstractCreateAndEditPageAction or DraftsTransitionHelper will:

  • Have its parent automatically set as the page from which user clicks the Create button
  • Default to a space's home page if the parent page isn't specific (for example, when clicking Create from the dashboard)

We've also updated the move page dialog so that it persists the draft's location by calling MovePageAction rather than temporarily storing the location on the client. This means the draft location will persist between different editing sessions.

Permissions and labels on shared drafts

There are two scenarios for draft metadata:

Scenario Metadata persistence
A shared draft has never been published
  • All permissions applied to the draft are saved with the draft
  • All labels added are saved with the draft
A shared draft has been published and as such links with a corresponding page
  • Permissions are added to the page. The draft object has no permissions saved with it. This means, to determine if a user can view a draft, you must check whether the user is allowed to view and edit the corresponding page.
  • Labels are added to the page. The draft object has no labels saved with it.

Sharing a shared draft

To collaborate on a shared draft that's linked to a published page, users only need to edit the published page.

To access a shared draft that's never been published, you need two pieces of information:

  1. The contentId (this will also be the draftId for unpublished drafts)
  2. The draftShareId of the draft

At the time a draft is created, a draftShareId is generated and stored against the draft's content properties. This content and draftShareId are displayed in the URL while the draft is being edited. It'll look like this:

<your_confluence_URL>/pages/resumedraft.action?draftId=1234&draftShareId=e4d05ef0-3cef-3c63-9f44-15e899469ad2

Sharing this link with other users will allow them to edit the same draft, assuming the view and edit restrictions on the draft allow it.

Once a user accesses a draft with the correct draftShareId they'll be granted a ContentPermission#SHARED_PERMISSION, which allows them to make future edits to the draft without providing the draftShareId. This shared permission won't be visible in the UI, and it'll be removed once the draft is published.

Draft Versioning

Stricter rules now apply to draft versioning. Whereas it was possible to save drafts as new versions prior to collaborative editing, this action is no longer permitted. Drafts must have a version of 1. If a version bump of a draft is attempted, Confluence will throw an exception.

When saving a draft, don't call PageManager#saveNewVerion

Draft Indexing

Shared drafts, like personal drafts, aren't indexed in Confluence. Since they're not added to the content index, it means drafts won't appear in features that access the context index, including:

  • Search
  • Recently updated
  • Space activity

Page updates triggered by draft publishes will appear in the content index as usual.

Collaborative editing modes in Confluence Server

In Confluence Server we provide administrators with a choice of three collaborative editing modes:

  • On - use this mode to bring collaborative editing goodness and shared drafts to your teams. Collaborative editing is on by default when you install or upgrade Confluence. 
  • Limited - use this mode if you need to troubleshoot Synchrony problems. As the name suggests, editing functionality is limited, but users' shared drafts are protected.  When a site is in limited mode, only one person can edit a shared draft at one time, people can't revert to an earlier version of the page in the page history, can't move pages and can't make inline comments on pages.
  • Off - use this mode if you decide that collaborative editing is not right for your site (for example, there are strict auditing requirements that the collaborative editing experience can't meet yet). In this mode, people can only edit their own personal draft of a page. Confluence will attempt to merge any conflicts on save (similar to the Confluence 5 editing experience).  Any existing Synchrony data contained in shared drafts is lost when the mode is changed to off.

Changing the mode is always initiated by the administrator.  We do not automatically fall back to a particular mode at any time. You may want to test your add-on in all three modes.  

Things to test

Make sure:

  • The correct type of draft is created in the database – all drafts created after the the introduction of collaborative editing should be shared drafts.
  • The correct type of draft is retrieved.

Synchrony

What's Synchrony?

Synchrony is a service that allows the synchronisation of arbitrary data models in real time. It supports special synchronisation for HTML WYSIWYG editors, including telepointers (remote selections).

How does Synchrony work?

  1. Confluence is configured to communicate with the Synchrony service using an appId and appSecret
  2. A JSON Web Token (JWT) is provided to clients containing the connection details
  3. Synchrony Javascript is loaded into the browser when the Confluence editor is initialised
  4. A WebSocket session is opened to Synchrony using the provided JWT and the contentId of the page being edited
  5. The WebSocket connection allows multiple clients to be kept in sync

This means that content data will be stored on the Synchrony service and the service will act as the source of truth for page content. While this is the case, Confluence will continue storing a snapshot of the most recent state of the document being edited at regular intervals. This content is stored as the body content in the shared draft but shouldn't be referenced as the most up-to-date content, as changes stored in Synchrony may not have been saved to the draft yet.

How the Synchrony Service runs in Confluence Server

Synchrony is executed as a separate process by Confluence Server. This process is managed by Confluence automatically, and through out normal operations, is not intended to be managed manually by an administrator. Administrators can choose to change the editing mode (on, limited, off). The affect of this change is site-wide. Collaborative editing cannot be turned on or off for specific spaces or pages.  

The Synchrony Service executable is extracted from Confluence into the home directory. It will be named 'synchrony-standalone.jar', and a second java process will be spawned with the above mentioned jar as the entry point. Confluence passes all required parameters for Synchrony via environmental variables - these are not considered api, and may be subject to change.

Synchrony requires a JDBC connection to the database that Confluence Server runs under. The three database tables used by Synchrony are 'Events', 'Secrets' and 'Snapshots'. Modifying the contents of these tables is not recommended, and they are subject to change without notice.

The Synchrony API

Synchrony doesn't currently provide an API for third party plugins. A client-side implementation is already provided by the Confluence Collaborative Editor Plugin, which will:

  • Automatically open a session to Synchrony when the Confluence editor is opened
  • Propagate changes to Synchrony when page content is changed outside of the editor (see our explanation of external changes)

The Synchrony Whitelist

When a session is opened to Synchrony, a whitelist of styles, elements and attributes is provided. The whitelist prevents malicious content from being synchronised from one client to another. Any items not in the whitelist will be ignored and won't be synchronised to other editing sessions.
 

  The current Synchrony whitelist:
'styles': $.extend({}, Synchrony.whitelists.tinymce.styles, {
    // Syncing the padding-top on the body element will
    // cause an infinite sync loop (WD-170). Other
    // padding-* styles are blacklisted for
    // completeness.
    'padding-top': false,
    'padding-right': false,
    'padding-bottom': false,
    'padding-left': false,
    'padding': false,
    'display': cssQuiteSafeValueRx,
    'list-style-type': cssQuiteSafeValueRx,
    'background-image': true
}),
'attributes': $.extend({}, Synchrony.whitelists.tinymce.attributes, {
    // ** date lozenge support
    // TODO this is only a workaround, the
    // contenteditable atribute should not be synced
    'contenteditable': true,
    // -- end date lozenge support
    // ** inline comments support
    'data-ref': true,
    // -- end inline comments support
    //Extra HTML attributes CONF uses
    'accesskey' : true,
    'datetime': true,
    'data-highlight-class':true,
    'data-highlight-colour':true,
    'data-space-key':true,
    'data-username': true,
    // ** space-list macro
    'data-entity-id': true,
    'data-entity-type': true,
    'data-favourites-bound': true,
    // -- end space-list macro
    // ** macro placeholder support
    'data-macro-id': true,
    'data-macro-name': true,
    'data-macro-schema-version': true,
    'data-macro-body-type': true,
    'data-macro-parameters': true,
    'data-macro-default-parameter': true,
    // -- end macro placeholder support
    // ** page layout support
    'data-atlassian-layout': true,
    'data-placeholder-type': true,
    'data-layout': true,
    'data-title': true,
    'data-type': true,
    // -- end page layout support
    // ** inline task support
    'data-inline-task-id': true,
    // -- end inline task support
    // ** WD-323
    'data-base-url': true,
    'data-linked-resource-id': true,
    'data-linked-resource-type': true,
    'data-linked-resource-version': true,
    'data-linked-resource-default-alias': true,
    'data-linked-resource-container-version': true,
    // -- end WD-323
    'data-unresolved-comment-count': true,
    'data-location': true,
    'data-image-height': true,
    'data-image-width': true,
    'data-attachment-copy': true
    // -- end WD-323
}),
'elements': $.extend({}, Synchrony.whitelists.tinymce.elements, {
    // ** date lozenge support
    'time': true,
    // -- end date lozenge support
    'label': true,
    'form': true
})

What happens if Synchrony is disabled?

Depending on how Synchrony is disabled:

  • If Synchrony is temporarily offline, users can still edit their content but are prevented from publishing. All changes are buffered; once Synchrony comes back online, buffered changes will be merged and synchronised between clients.
  • If Synchrony is unavailable for an extended time, the administrator can change the collaborative editing mode to limited, which prevents more than one person editing a shared draft at a time. This prevents any merge conflicts and data loss while Synchrony is unavailable. 

Debugging Synchrony

As Confluence opens a WebSocket connection to Synchrony, all Synchrony traffic can be monitored in the WebSocket request via the developer tools' Network tab:


Things to test

  • Make sure changes are properly persisted to Synchrony. For example, plugins that display a preview in the editor should make sure that this is updated accordingly.
  • Make sure style and attribute changes aren't filtered out by the whitelist.

External Changes

What are external changes?

External changes refer to updates made to page content outside of the Confluence editor, like inline comments and tasks. When external changes are made, the changes need to be propagated to Synchrony to ensure that Confluence and Synchrony are in sync.

External changes can be triggered in a number of ways. A plugin can:

  1. Make a request to the Confluence REST API
  2. Call the ContentService
  3. Call the PageManager

It's possible to externally modify page content using other means, but it's important to stick with the methods listed above and only update published pages to ensure the changes are correctly propagated to Synchrony. Things to be aware of:

  • Don't use ContentEntityManager to update page content – this'll bypass the external changes call to Synchrony.
  • Don't make external changes to the draft object, as these changes will be lost when the published page is synchronised with Synchrony.

How do external changes work?

Synchrony provides an API that allows external applications to inform Synchrony of external changes. Confluence automatically makes a call to the Synchrony API whenever page content is updated, so plugins should avoid calling the Synchrony API directly.

If a plugin needs to modify page content, it just needs to make the appropriate updates in Confluence and the updates are pushed to Synchrony.

How are external changes triggered?

The external changes call to the Synchrony API will be made from PageManager#saveContentEntity.

If page updates are made which bypass the PageManager, then updates won't be synchronised to Synchrony.

What external changes will be pushed to Synchrony?

Changes to the following will be propagated to Synchrony:

  • Page title
  • Body contents

Event List

Event Type Purpose Example
editor.external.change Javascript

Triggered after an external change has occurred and the changes have been synchronised to the editor

Also triggered once after the content in the editor is initialised by Synchrony.

AJS.bind('editor.external.change', <handler function>);

Things to test

  • Any changes to page contents without interacting with the editor. Make sure the change is still present when the page is edited then published.
  • Any changes to page content made programmatically, like direct modification of the DOM using Javascript. Make sure the change is still there after an external change is made and the page is published.

Things you shouldn't do

Don't:

  • Cache a DOM reference in the plugin JS – Synchrony will perform a replacement and render the reference obsolete. Use a selector instead.
  • Use the ContentEntityManager to directly modify page content – the external changes call will be bypassed. Always use the PageManager.

Test Scenario Checklist

  • Test plugins with two open tabs, as this forces the synchronised changes to be filtered by the whitelist.
  • Test that external changes are correctly propagated by triggering another publish from the shared draft after the external change is made. This is to check that the change is still there after the publish.


Was this page helpful?

Have a question about this article?

See questions about this article

Powered by Confluence and Scroll Viewport