Repository navigation
馃搶 Tracking: Presence API path to Core聽#550
Description
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
- Release 0.8.0 (chore(main): release 0.8.0聽#547)
- Move post locks out of post meta in the plugin (An open editor makes every cached post query stale on each Heartbeat tick聽#548)
- Release 0.11.0 (chore(main): release 0.11.0聽#635)
- Make the Presence API gutenberg-sync-engines' awareness store (feat: require the Presence API plugin聽gutenberg-sync-engines#120)
- Label agent rows once Agent Users lands (An agent can edit a post you have open, and neither of you knows聽#549)
- Release 0.15.0 (chore(main): release 0.15.0聽#718)
- Release 0.18.0 (chore(main): release 0.18.0聽#803)
- List it in the plugin directory's Beta Testing tab, raised at devchat on 2026-10-14; Make/Core's features page lists it already
- Firefox and WebKit end-to-end runs for the tab coordination code (End-to-end tests run in Chromium only聽#808, test: run the cross-tab, visibility and admin bar contrast specs in Firefox and WebKit聽#838)
- Step each piece aside once core's patch exists (The whole plugin stops loading if core registers the presence table聽#859); when core adopts a feature, a release removes its registration and files, as Gutenberg does
- Review the Trac proposal draft with Deepak, then publish once readiness gate is met
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
- A plugin exists in the official WordPress plugin repository, is updated regularly, and is used/tested by the community. presence-api on WordPress.org, 0.18.2 shipped 2026-10-16.
- Weekly chats are taking place on Slack, and the feature lead is attending the weekly core dev chat. The meeting in #feature-presence-api runs every other Monday at 15:00 UTC.
- A kickoff post and regular update posts are published publicly, tracking the progress and major decisions. Kickoff and What's new on Make/Core.
- The feature works in all of the browsers that WordPress officially supports. The specs for code that can differ between engines also run in Firefox and WebKit, and a manual pass covers Safari, Chrome, Firefox and Edge (test: run the cross-tab, visibility and admin bar contrast specs in Firefox and WebKit聽#838).
- Touch devices can use the entire feature with no hindrance, with visual records for major flows. The admin bar shows on phones and spells out names that were hover-only (fix: show the admin bar presence node on phones and spell out hover-only names on touch聽#830), and every other screen works by tap on phones and tablets, with visual records (Screens haven't been checked on touch devices and small screens聽#833).
- Visual records comparing old flow with new flow are provided for any feature that changes or replaces existing interfaces. Nothing replaces an existing interface; post locks look the same and only move where they are stored (An open editor makes every cached post query stale on each Heartbeat tick聽#548).
- The code conforms to the WordPress coding standards. PHPCS (the full
WordPressruleset, with the rules that don't fit tests turned off, as core does), Stylelint and ESLint cover the source and tests. - The code is well-tested, and has unit tests. PHPUnit on single site, multisite and SQLite, Playwright end-to-end tests, and about 97% coverage.
- The code is well-documented. The README covers the PHP API, REST, WP-CLI and every hook, and each function has a docblock.
- The code follows the plugin security best practices, and has undergone a security audit. No audit yet (The plugin hasn't had a security audit聽#834).
- The user interface/experience has been tested through user testing, and appropriate feedback was incorporated. Not yet (The plugin hasn't been through user testing聽#835).
- The design is fully responsive, displaying properly on any mobile device, and using graphics that are ready for hi-dpi/retina displays. Avatars are fetched at twice their display size, and no screen scrolls sideways on a phone or tablet (fix: show the admin bar presence node on phones and spell out hover-only names on touch聽#830, Screens haven't been checked on touch devices and small screens聽#833).
- HTML validates to the proper doctype. The plugin's fragments pass as the server sends them, and the stale-screen notice, which JavaScript adds, as the browser builds it (test: validate the plugin's markup in e2e聽#852, The markup check validates the browser's repaired markup, not the HTML the plugin sends聽#855).
- The code has all of the proper hooks in place for localization. Strings use the
presence-apitext domain, which PHPCS enforces, and each release regenerates the translation template. - WordPress continues to function, and the feature is still usable, with JavaScript turned off. Every screen works, since each admin page load records presence on the server (test: check the plugin's screens with JavaScript turned off聽#857).
- The feature can be used with just a keyboard. Fixed for the admin bar and widgets in Admin bar flyout: non-link items unreachable by keyboard聽#264 and Heartbeat swap drops focus to <body> in dashboard widgets聽#265, and for the screens added since in fix: make the presence screens readable with only a keyboard聽#841.
- Any required accessibility support has been added, including (but not limited to) off-screen text, ARIA, and JS-assisted. Accessibility audit for dashboard widgets and admin bar node聽#138, Missing alt text on Who's Online widget avatar聽#142, Admin bar flyout: non-link items unreachable by keyboard聽#264, Heartbeat swap drops focus to <body> in dashboard widgets聽#265 and Admin bar flyout text fails contrast in the Light color scheme聽#266.
- A merge proposal has been published on the Make/Core blog once the plugin is ready. After the Trac ticket.
Merge checklist
- Make sure the feature plugin noops once the feature is merged. This means the plugin won't break with a Fatal Error after code is merged from the plugin to core. If core registers the presence table, the plugin shows a notice and loads nothing, and feat: let core's copy of a presence function or class win when it already exists聽#805 and Six files would fatal with "cannot redeclare" if core ships their functions聽#860 let core's copy of each function win. The whole plugin stops loading if core registers the presence table聽#859 holds the rest for core's patch.
- If code from another plugin is used, make sure there are no clashes with that plugin once the feature merges. gutenberg-sync-engines calls the presence functions but declares none of its own.
Readiness gate
The proposal goes to core when all of these hold, since core weighs real-world use of the API over install counts.
- Three plugins outside this repository build on the API. One so far: gutenberg-sync-engines, as its awareness store.
- A host runs it on production sites, reaching persistent object caches and multisite at scale.
- Two or more large sites measure the post query cache before and after post locks moved out of post meta (An open editor makes every cached post query stale on each Heartbeat tick聽#548). One small site and one synthetic large site have run it so far (The post query cache hasn't been measured before and after post locks left post meta聽#836).
- Eight weeks with no breaking change to the API and no high-severity bug, counted from the release that ships the
$argssignatures (feat: take an $args array in the presence functions聽#789): 0.17.0 on 2026-10-08, so 2026-12-03 at the earliest. - 1,000+ active installs on WordPress.org. 10 today.
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
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
Detecting a second editor. The client must poll the endpoint to learn whether it needed to poll it.
- Gutenberg sets
hasCollaboratorsfrom the poll response (polling-manager.ts#L731) and selects the interval from it (#L778, #L804). - Presence fires
wp_presence_collaboration_startedand_endedon 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.collaborationStartedandpresence-api.collaborationEnded, alongsidepresence-api.watchingRoom, so the editor can listen instead of polling.
Cross-tab coalescing. The other half of section 3, on the client side.
- Gutenberg has none. No
BroadcastChannelornavigator.locksanywhere inpackages/sync/src. - Presence elects one poller per room with Web Locks and distributes the result over BroadcastChannel, with a per-tab fallback (presence-poll-coordinator.js), from #300. Six tests in presence-poll-coordinator.test.js.
Idle backoff. Visibility is not idleness, so an open untouched editor keeps paying the fast rate.
- Gutenberg keys off
document.visibilityStateonly. A focused tab left untouched keeps polling at 4s. - Presence backs off on inactivity, widening to
idleIntervalafteridleTicksunchanged 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-userget_userdata()andget_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
[-]馃搶 Tracking: Presence API path to core[/-][+]馃搶 Tracking: Presence API path to Core[/+]