Skip to content

馃搶 Tracking: Presence API path to Core聽#550

Open

Description

@josephfusco

Presence API is headed for a core proposal, with post locks as its first consumer. This page says what just happened and what comes next.

Next

Feature plugin merge criteria

Each item from the Core Handbook's Features as Plugins, in its order and wording, with where this plugin stands.

Merge criteria

Merge checklist

Readiness gate

The proposal goes to core when all of these hold, since core weighs real-world use of the API over install counts.

Recently

What happened
0.8.0 Shipped wp_presence_exchange() and wp_presence_leave() (#546), and a Site Health check for when Heartbeat cannot keep presence current (#544)
#548 Found that every post lock refresh makes every cached post query stale on sites with a persistent object cache, which gives core a reason to adopt presence
#549 WordPress/ai is giving agents their own user accounts (#961); presence can show them in the room
0.9.0 Moved post locks into the presence table (#551), put where people are behind the view_presence_location cap (#571) and retired the site Who's Online widget (#591)
0.10.0 Kept the Editors column and Online users list current on each heartbeat (#604, #606) and fixed presence writes on SQLite (#623)
0.11.0 Presence on every post type edited in the admin (#631), including what the Site Editor has open (#638), and post locks kept in the presence table with recording off (#644)
#558, #667 Split the core-bound code so each piece lifts out as its own Trac patch, then moved the plugin into plugins/presence-api so tools such as presence-scenes sit beside it, and #710 gave each piece its own switch
0.12.0 Replaced the debugger's Dashboard widget with an admin bar debugger that plugins can extend (#652, #666), and labelled agent rows across the admin UI (#653)
0.12.2 Read each presence query once per request until the table changes (#678), and elected one Heartbeat ping leader per site among visible tabs (#675)
Scenes Presence Scenes, a GitHub-only plugin beside this one that plays real users through probable situations from WP-CLI (#655, #661)
0.13.0 An agent's save writes its presence row (#697)
0.14.0 Breaking: presence colors are no longer stored or served; REST entries drop color, and wp_presence_get_user_color() returns the block editor's color for the user ID (#707)
Hosts The hosting team asked hosts to test the three gutenberg-sync-engines candidates
0.15.0 Post locks can be switched off (#724), and a row being set or removed fires an action (#756)
0.16.0 Switches for the admin bar and Editors column, the debugger moved into its own plugin (#760), and usePresenceUsers registered as wp-presence (#767)
0.17.0 The $args signatures (#789), presence on synced patterns (#779), and a switch for every remaining piece, the network's included (#788)
0.18.0 A room tells a second browser from a second tab (#815), core's copy of each API function wins once it exists (#805), and the tests are linted to WordPress standards (#817)
0.18.1 The admin bar presence node shows on phones, and hover-only names are spelled out on touch (#830)
0.18.2 The presence screens can be read with only a keyboard (#841)
#836 On a synthetic large site with a Redis object cache, post locks in post meta doubled the post query cache miss rate, from about 5% to about 10%; two real large sites still need to measure it

How it fits

flowchart LR
  E[Block editor] --> G[gutenberg-sync-engines<br/>edits]
  G -->|who is here| P[Presence API<br/>wp_presence]
  H[Heartbeat in wp-admin] --> P
  L[Post locks] --> P
  A[AI agents] --> P
Loading

Each of these asks who is here right now, and today each answers it separately: post meta for locks, a serialized array for editor awareness, nothing for agents. One row per client per room answers it once. RTC ships as a feature plugin on two primitives, gutenberg-sync-engines for edits and this plugin for who is in the room, with core adopting what holds up. The cost evidence is in #444 and the work left outside this repo in #647, and what Gutenberg needs to host an RTC plugin is in WordPress/gutenberg#83165. Discussed at the Presence API meeting, every other Monday at 15:00 UTC in #feature-presence-api.

Readiness for the core proposal

All 37 issues in the Core Merge Proposal milestone are closed.

Area Resolved in
Public API contract #140, #415, #417, #518
Authorization #128, #246
Database portability #133, #206, #207, #208, #209
Scale #132, #205, #285
Multisite #137
Accessibility #138, #142, #264, #265, #266
Privacy and site control #393, #400, #441
No persistent object cache required #204, #512, #514
A consumer outside this repository gutenberg-sync-engines#102

The Presence API runs on its own table and Heartbeat. On a single site a Heartbeat tick writes only to wp_presence; options are written on content saves and cron runs, and on multisite when the set of online users changes.

Where Gutenberg sync and presence overlap Venn diagram. Gutenberg sync only: CRDT update log, compaction, cursor geometry, join and leave notices, disconnect modal, client limit per room, per-connection identity. Both: room membership, identity, expiry, idle backoff, post lock, room naming, transport. Presence API only: cross-tab read coalescing, collaboration edge, unchanged-payload skip, TTL sweep cron, user-keyed identity, with no equivalent in packages/sync.
Detecting a second editor. The client must poll the endpoint to learn whether it needed to poll it.
  • Gutenberg sets hasCollaborators from the poll response (polling-manager.ts#L731) and selects the interval from it (#L778, #L804).
  • Presence fires wp_presence_collaboration_started and _ended on the 1 to 2+ and 2+ to 1 transitions (heartbeat.php#L414-L457). Shipped in #290; the edge was made to hold across requests in #380. Ten tests in test-heartbeat.php. Since #503 and #511 the same two edges fire JavaScript events, presence-api.collaborationStarted and presence-api.collaborationEnded, alongside presence-api.watchingRoom, so the editor can listen instead of polling.
Cross-tab coalescing. The other half of section 3, on the client side.
Idle backoff. Visibility is not idleness, so an open untouched editor keeps paying the fast rate.
  • Gutenberg keys off document.visibilityState only. A focused tab left untouched keeps polling at 4s.
  • Presence backs off on inactivity, widening to idleInterval after idleTicks unchanged ticks, from #307, closing #267. The widened interval is capped below the TTL, so an idle-but-open tab cannot drop out of its own room (presence-ping.js#L49-L50).

Both Presence values are filterable with no clamp. Gutenberg's sync.pollingManager.pollingInterval runs through Math.min( filteredInterval, defaultInterval ) (config.ts#L62), so a host can lower the interval but not raise it. That is intentional, from WordPress/gutenberg#78811: a host slowing the interval far enough makes active collaboration feel broken. The clamp protects the two-editor case at the cost of the solo one.

Unchanged payloads. N writes per poll for state that usually has not changed.
  • Gutenberg calls set_awareness_state() on every poll without comparing state.
  • Presence hashes room state and has the client echo its last hash; on a match the response omits the payload, skipping cache_users(), per-user get_userdata() and get_avatar_url(), label building and most of the JSON (#288).
Identity hydration. Fields that almost never change ride every request.
  • Gutenberg does it client-side. Each client puts its own name, slug, avatar_urls and browserType into awareness state and sends it on every poll (utils.ts#L146).
  • Presence does it server-side and batched, through wp_presence_hydrate_room_users() (functions.php#L1218), paginated in #297.
Already aligned. Room naming, post lock and room membership need no work.

Room identifiers match (postType/{type}:{id}). Both read _edit_lock, for different purposes: Gutenberg suppresses the lock modal and adjusts post-list UI, Presence keeps the lock itself in the room's _lock row and flags the holder's editor- entry. Room membership lands in the same wp_presence table under the same room string. sync-storage did it with a sync- client id prefix; gutenberg-sync-engines does it with gse-, through an awareness backend of its own rather than __unstable_wp_sync_storage (WordPress/gutenberg-sync-engines#102). It needs no configuring: where the Presence API is active, recording and has its table, a collaborator in the editor is a row in wp_presence and shows up in the admin bar and the post list. Where it is not, the plugin keeps its own store.

What gutenberg-sync-engines already answers

The Presence API items that came out of #444 are checked off in #444 搂6.

gutenberg-sync-engines

sync-storage is archived. Its README names gutenberg-sync-engines as the successor, and the direction is set out in Moving to a server-aware approach for collaboration.

It is a comparison harness rather than one design. A site picks an engine, how the server merges edits, from intent-log, yjs-server or de-rtc, and separately picks how updates travel. Transport is the less interesting axis: http-polling is the default and the only one every host can run, so it is the floor #444 describes, and server-sent events, WebSocket and the advisory channel only take requests off it.

Neither axis reaches awareness. Every awareness read and write in that plugin goes through WP_Sync_Awareness, so whatever a site picks, the wp_sync_awareness_backend filter and the Presence API behind it serve all of them. Engine choice changes the update log, and who is in the room is the same question in every combination. That is the case for presence being a primitive rather than part of a sync design.

The built-in awareness store keeps a room's whole array under one storage key, so two clients writing in the same instant can lose one of the writes, and on a host with a persistent object cache the array never reaches the database. Presence rows are one per client with a unique key on (room, client_id), so a concurrent write to the same room touches a different row.

Already answered there:

Concern How
Collaboration data off post meta Own tables, wp_sync_updates and wp_sync_room_meta
Poll cost when nothing is happening Advisory channel, so a quiet room defers its poll rather than running one every 5s
Rewriting an unchanged payload Awareness timestamps are bucketed, so an idle client repeating its state writes nothing inside the bucket
Room grammar validate_access() delegates to WP_Sync_Config::parse_room() and can_user_sync_entity_type() rather than matching with its own regex
Awareness in the shared table WP_Sync_Awareness behind a wp_sync_awareness_backend filter, using the Presence API wherever it is available (#102)

Activity

  1. pinned this issue on Sep 24, 2026
  2. added this to the Future Release milestone on Sep 24, 2026
  3. unpinned this issue on Oct 5, 2026
  4. pinned this issue on Oct 8, 2026
  5. changed the title [-]馃搶 Tracking: Presence API path to core[/-] [+]馃搶 Tracking: Presence API path to Core[/+] on Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

[Area] InfrastructureIssues for the repository infrastructure[Type] EnhancementA suggestion for improvement of an existing feature

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions