Skip to content

Real-time collaboration: Add new REST endpoints, setting, and registered post meta#10894

Open
chriszarate wants to merge 29 commits intoWordPress:trunkfrom
chriszarate:add/collaboration-functionality
Open

Real-time collaboration: Add new REST endpoints, setting, and registered post meta#10894
chriszarate wants to merge 29 commits intoWordPress:trunkfrom
chriszarate:add/collaboration-functionality

Conversation

@chriszarate
Copy link

@chriszarate chriszarate commented Feb 10, 2026

Description

Trac ticket: https://core.trac.wordpress.org/ticket/64622

In Gutenberg, we have added support for real-time collaboration using CRDT documents (via the Yjs library). This work has suggested the following additions to WordPress:

  1. A default "sync provider" based on HTTP polling that allows collaborators to share updates with each other. Previously, we relied on WebRTC connections between collaborators for this purpose, but it proved unreliable under many network conditions.

    • Our solution is designed to work on any WordPress installation.
    • HTTP polling is the transport we identified as most likely to work universally.
    • Given the isolation and lifecycle of PHP processes, updates must be stored centrally in order to be shared among peers. We have chosen to store updates in post meta against a special post type, but alternate storage mechanisms are possible.
    • Collaborative editing can involve syncing multiple CRDT documents. To limit the number of connections consumed by this provider, requests are batched.
    • To prevent unbounded linear growth, updates are periodically compacted.
    • To avoid excessive load on lower-resourced hosts, this provider will benefit from usage limits (e.g., a maximum of three connected collaborators) enforced by the client (Gutenberg).
  2. A new registered post meta that allows Gutenberg to persist CRDT documents alongside posts.

    • This provides all collaborators with a "shared starting point" for the collaborative session, which avoids duplicate updates.
    • Content stored in the WordPress database always remains the source of truth. If the content differs from the persisted CRDT document, the CRDT document is updated to match the database.
  3. A new Writing setting that allows users to opt-in to real-time collaboration.

    • Enabling real-time collaboration disables post lock functionality and connects users to the sync provider.
  4. A behavior change to autosaves is needed. When the the original author is editing a draft post (post_status == 'draft' OR 'auto-draft') and they hold the post lock, the autosave targets the actual post instead of an autosave revision. This puts the post data and the persisted CRDT document out of sync and leads to duplicate updates. When real-time collaboration is enabled, all collaborators must autosave in the same way.

This PR provides a proposed implementation of the changes above. This corresponding Gutenberg PR moves the work from the experimental directory to lib/compat:

WordPress/gutenberg#75366

Cumulative work to add this functionality can be found using this label:

https://github.com/WordPress/gutenberg/issues?q=label%3A%22%5BFeature%5D%20Real-time%20Collaboration%22%20is%3Apr

Testing instructions

  1. Check out this branch.
  2. Update the Gutenberg ref in package.json to 131239e64048aa0f107512129e8b6771c54ba855
  3. Follow the setup instructions in the README.
  4. Settings > Writing > Enable real-time collaboration.
  5. Open a post for editing in two browsers / browser tabs.

@github-actions
Copy link

Hi @chriszarate! 👋

Thank you for your contribution to WordPress! 💖

It looks like this is your first pull request to wordpress-develop. Here are a few things to be aware of that may help you out!

No one monitors this repository for new pull requests. Pull requests must be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description.

Pull requests are never merged on GitHub. The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making.

More information about how GitHub pull requests can be used to contribute to WordPress can be found in the Core Handbook.

Please include automated tests. Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the Automated Testing page in the handbook.

If you have not had a chance, please review the Contribute with Code page in the WordPress Core Handbook.

The Developer Hub also documents the various coding standards that are followed:

Thank you,
The WordPress Project

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@pkevan
Copy link

pkevan commented Feb 10, 2026

Is there any tests we can add for the default provider and new functionality?

@pkevan
Copy link

pkevan commented Feb 10, 2026

Can we also provide some documentation on how to override the provider given it's known limitation listed above?

Copy link
Member

@TimothyBJacobs TimothyBJacobs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't each room a post?

* @param bool $is_compactor True if this client is nominated to perform compaction.
* @return array Response data for this room.
*/
private function get_updates_after( string $room, int $client_id, int $cursor, bool $is_compactor ): array {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it should be a responsibility of the storage layer. If I want to swap with something that can retrieve results ordered by time, we shouldn't have to load all updates always to do that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the storage interface and class to take this approach in 9e5cf96. It was a little trickier than I thought, because compaction is an external concern and shouldn't be implemented by storage.

@chriszarate
Copy link
Author

Why isn't each room a post?

No strong reason. Mostly to avoid creating a large number of posts. Each room corresponds to a synced CRDT document (representing a WordPress entity). Currently supported entity types:

  • posts (any post type)
  • taxonomy collections

In the future, this could be expanded to include all database entities, including individual taxonomy terms. We might also create rooms to share awareness data at the page or screen level.

We intend to enforce limits on this default provider, but probably applied to simultaneous collaborators and not on the overall number of synced entities. Sharing a single post felt prudent but happy to reconsider! I know wp_postmeta#meta_key is indexed but I don't have a grasp on the performance tradeoff at large scale.

@pkevan
Copy link

pkevan commented Feb 17, 2026

Fix one of the failing unit tests here: chriszarate#1 since I couldn't PR against this.

@chriszarate chriszarate force-pushed the add/collaboration-functionality branch from fb2d847 to 92152e8 Compare February 17, 2026 14:49
chriszarate and others added 9 commits February 17, 2026 21:19
…ted values

The test_get_items test in WP_Test_REST_Settings_Controller hardcodes
the expected list of settings exposed via the REST API. The new
enable_real_time_collaboration setting was registered with
show_in_rest but not added to this expected list, causing the test
to fail.
@chriszarate chriszarate force-pushed the add/collaboration-functionality branch from 92152e8 to f83467c Compare February 18, 2026 02:21
@chriszarate
Copy link
Author

Note that I've removed the commit that updates Gutenberg to the required version to allow unit tests to pass. The testing instructions have been updated accordingly.

@chriszarate chriszarate marked this pull request as ready for review February 18, 2026 03:47
@github-actions
Copy link

github-actions bot commented Feb 18, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @gclapps0612-cmd.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

Core Committers: Use this line as a base for the props when committing in SVN:

Props czarate, paulkevan, timothyblynjacobs, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@gclapps0612-cmd

This comment was marked as spam.


$entity_kind = $type_parts[0];
$entity_name = $object_parts[0];
$object_id = isset( $object_parts[1] ) ? (int) $object_parts[1] : null;
Copy link
Member

@westonruter westonruter Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the object ID is a string, right?

Suggested change
$object_id = isset( $object_parts[1] ) ? (int) $object_parts[1] : null;
$object_id = $object_parts[1] ?? null;

Comment on lines +225 to +227
if ( is_wp_error( $merged_awareness ) ) {
return $merged_awareness;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The process_awareness_update method never returns a WP_Error, yeah?

Suggested change
if ( is_wp_error( $merged_awareness ) ) {
return $merged_awareness;
}

* All updates are stored persistently.
*/
return $this->add_update( $room, $client_id, $type, $data );
break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
break;

* @param int $client_id Client identifier.
* @param string $type Update type (sync_step1, sync_step2, update, compaction).
* @param string $data Base64-encoded update data.
* @return bool|WP_Error True on success, WP_Error on storage failure.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return bool|WP_Error True on success, WP_Error on storage failure.
* @return true|WP_Error True on success, WP_Error on storage failure.

* @param int $cursor Return updates after this cursor.
* @param bool $is_compactor True if this client is nominated to perform compaction.
* @return array{
* compaction_request: array|null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This satisfies the following PHPStan issue:

phpstan: Method WP_HTTP_Polling_Sync_Server::get_updates() return type has no value type specified in iterable type array.

Suggested change
* compaction_request: array|null,
* compaction_request: mixed[]|null,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is slightly better:

Suggested change
* compaction_request: array|null,
* compaction_request: array<int, mixed>|null,

It is more explicit, and it satisfies PhpStorm's static analysis complaint as well:

Can be replaced with 'array'

*
* @param string $room Room identifier.
* @param int $cursor Return updates after this cursor.
* @return array Sync updates.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This satisfies a PHPStan issue:

phpstan: Method WP_Sync_Storage::get_updates_after_cursor() return type has no value type specified in iterable type array.

Suggested change
* @return array Sync updates.
* @return mixed[] Sync updates.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return array Sync updates.
* @return array<int, mixed> Sync updates.

*
* @param string $room Room identifier.
* @param int $cursor Return updates after this cursor.
* @return array Sync updates.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a PHPStan complaint:

phpstan: Method WP_Sync_Post_Meta_Storage::get_updates_after_cursor() return type has no value type specified in iterable type array.

Suggested change
* @return array Sync updates.
* @return mixed[] Sync updates.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return array Sync updates.
* @return array<int, mixed> Sync updates.

'wp_notes_notify' => 1,

// 7.0.0
'enable_real_time_collaboration' => 0,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'enable_real_time_collaboration' => 0,
'enable_real_time_collaboration' => 1,

switch on for beta1

'type' => 'boolean',
'description' => __( 'Enable Real-Time Collaboration' ),
'sanitize_callback' => 'rest_sanitize_boolean',
'default' => false,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'default' => false,
'default' => true,

"use_smilies": true,
"default_category": 1,
"default_post_format": "0",
"enable_real_time_collaboration": false,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"enable_real_time_collaboration": false,
"enable_real_time_collaboration": true,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants