wp-presence-api
Implement or audit integrations with the experimental WordPress Presence API feature plugin 0.1.23. Covers the seven public PHP functions, post and admin rooms, the per-site wp_presence table and TTL, Heartbeat transport, REST read/write/delete/rooms endpoints, per-room capabilit
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-presence-api
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WordPress Presence API
Integrate with Presence API 0.1.23 as an experimental feature plugin, not as a
WordPress 7.1 core API. It supplies awareness of active users and editors using
a dedicated per-site table, a 60-second TTL, Heartbeat, REST, admin surfaces,
and a small public PHP API. Pin and feature-detect the plugin; its 0.1.x
contract may still change.
When to use this skill
- Build who-is-online, active-editor, post-lock, or co-presence UI.
- Review
wp_get_presence(),wp_set_presence(),wp_presence_post_room(),presence-ping,wp_presence_editor_state, or/wp-presence/v1code. - Add presence support to a custom post type.
- Decide where to store high-frequency ephemeral state.
- Audit Heartbeat load, room authorization, presence privacy, cleanup, or multisite behavior.
Establish the runtime contract first
Feature-detect a public function and avoid loading plugin internals yourself:
if ( ! function_exists( 'wp_get_presence' ) ) {
return;
}
Presence API 0.1.23 requires WordPress 7.0+ and PHP 7.4+. WordPress 7.1 does
not provide these functions or the wp_presence table by itself. Do not test
only version_compare( get_bloginfo( 'version' ), '7.1', '>=' ).
Treat an experimental-plugin version constraint as a deliberate product
decision. Fail softly when it is absent, and verify the installed source again
before relying on signatures in a later 0.1.x release.
Use only the seven public PHP functions
The source explicitly marks these as its public contract:
| Function | Contract |
|---|---|
wp_get_presence( $room, $timeout ) |
Return active entry objects for one room. |
wp_set_presence( $room, $client_id, $state, $user_id ) |
Atomically upsert one (room, client_id) row. |
wp_remove_presence( $room, $client_id ) |
Remove one client entry. |
wp_remove_user_presence( $user_id ) |
Remove a user's entries across all rooms. |
wp_can_access_presence_room( $room, $user_id ) |
Check the plugin's room access policy. |
wp_presence_post_room( $post ) |
Return the canonical post room or false. |
wp_presence_admin_room() |
Return the canonical admin/online room. |
Everything after the public section in includes/functions.php is marked
private even when it has a global wp_* function name. Do not depend on
wp_get_active_rooms(), wp_get_presence_summary(), table/provisioning
helpers, or cleanup internals.
The direct PHP write/remove functions are trusted server-side primitives. They do not reproduce the REST controller's room-length, payload, ownership, entry limit, or capability checks. Validate and authorize before calling them from any request handler.
Model rooms and authorization together
Core post types post and page opt in automatically. Add support to a custom
post type during registration or afterwards:
register_post_type(
'book',
array(
'show_ui' => true,
'supports' => array( 'title', 'editor', 'presence' ),
)
);
$room = wp_presence_post_room( $book_id ); // postType/book:123 or false.
postType/{post_type}:{id} rooms require edit_post for that object. Other
room strings, including admin/online, require only edit_posts. Therefore a
custom room name is not a custom authorization boundary. Do not put data in a
generic room when every user with edit_posts must not see it; enforce the
narrower capability in your own server handler or use an object-backed room.
Presence is awareness, not authorization. Never grant locks, saves, or content access merely because a user has a presence entry.
Keep state ephemeral and bounded
$room = wp_presence_post_room( $post_id );
$client_id = 'my-plugin-' . get_current_user_id();
if ( $room && current_user_can( 'edit_post', $post_id ) ) {
wp_set_presence(
$room,
$client_id,
array( 'mode' => 'reviewing' ),
get_current_user_id()
);
}
Use stable, namespaced client IDs. Store only small UI state, never secrets, tokens, unpublished content bodies, or durable workflow state. Entries expire from reads after the TTL and are later removed in bounded cron batches. TTL is not a delivery guarantee, logout is not guaranteed to run, and a crashed tab can remain visible until expiry.
The table is the correct architectural pattern for high-frequency awareness:
it avoids repeatedly invalidating wp_options or object meta caches. It does
not make every custom ephemeral feature a reason to depend on this plugin;
use the public contract only when Presence API's room and capability model fit.
Use the REST contract safely
Authenticated endpoints are:
| Method | Route | Purpose |
|---|---|---|
GET |
/wp-presence/v1/presence |
Paginated entries for room. |
POST |
/wp-presence/v1/presence |
Upsert room, client_id, and data. |
DELETE |
/wp-presence/v1/presence |
Delete an owned entry; manage_options can delete any. |
GET |
/wp-presence/v1/presence/rooms |
Paginated, access-filtered active rooms. |
POST |
/wp-presence/v1/presence/screen-revisions/stale |
Bump an authorized screen revision. |
Use wp.apiFetch in WordPress admin/editor JavaScript so the REST nonce and
root middleware are applied. Respect pagination headers and request _fields
when only user identity is needed. Responses use Cache-Control: no-store.
The 0.1.23 controller bounds room/client IDs to 191 characters, REST state to
10 KiB and three nested array levels, list pages to 100 rows, and active
entries to 50 per user. It rejects an active client_id owned by a different
user and restricts delete-by-client ownership. Do not clone these values into
a competing endpoint; use the plugin route or implement an independently
reviewed contract.
Read references/api-and-runtime.md before adding a REST client, custom room,
screen-revision integration, or multisite dependency.
Integrate Heartbeat without multiplying traffic
The plugin enqueues WordPress Heartbeat and writes initial presence on eligible admin/front-end requests, then refreshes state through Heartbeat. The block editor can run Heartbeat faster than its normal 60-second interval for post locks. Do not add a second timer that posts the same state independently.
The shipped usePresenceUsers() React hook performs one REST read initially
and on Heartbeat ticks, deduplicates by user ID, supports _fields, and can
exclude the current user. In 0.1.23 it is shipped as source, not as a registered
WordPress package or script handle. Do not deep-import another installed
plugin's filesystem path at runtime. If a build deliberately vendors that
experimental source, pin the plugin release and review license/update drift;
otherwise implement a small wp.apiFetch consumer around the REST contract.
Treat collaboration hooks as advisory in 0.1.23
wp_presence_editor_state can enrich editor state. The plugin also declares
wp_presence_collaboration_started and wp_presence_collaboration_ended.
Do not use the latter actions for billing, durable workflow transitions, or
exact participant lifecycle: in 0.1.23 threshold memory is a request-local
static variable, so it does not persist a transition state across separate
Heartbeat requests. Recompute current membership from the room for decisions
and verify this implementation again after upgrading.
Test the whole lifecycle
- Absence of the feature plugin: integration fails softly.
- Supported and unsupported post types: room string versus
false. - Author can access own editable post but not a post they cannot edit.
- Generic custom room visibility for every
edit_postsuser. - REST create/read/delete, ownership conflict, 191-character keys, oversized
state, nested state, pagination,
_fields, andCache-Control: no-store. - Two tabs for one user and two users in one room; close/crash/logout/TTL.
- Heartbeat active, slowed, suspended, and unavailable.
- Table missing during a front-end request: reads return empty and writes
return
falseinstead of causing SQL errors. - Site activation, network activation, new-site creation, large network, cron cleanup, deactivation, and uninstall on a real multisite test network.
- Dynamic UI with keyboard focus, empty avatar alt text, live-region announcements, and reduced motion.
Critical rules
- Presence API 0.1.23 is an experimental plugin, not WordPress 7.1 core.
- Feature-detect it and use only the seven explicitly public PHP functions.
- Keep capability checks at every write/read boundary; presence grants nothing.
- Treat generic rooms as visible to all users with
edit_posts. - Keep state small, non-secret, ephemeral, and retry-safe.
- Reuse Heartbeat; do not add a competing polling loop.
- Do not depend on private global helpers or exact internal table queries.
- Do not treat collaboration threshold hooks as durable transition events.
Cross-references
- Use
wp-rest-apiwhen implementing a separate custom endpoint. - Use
wp-plugin-options-storagefor the custom-table decision. - Use
wp-plugin-cronfor cleanup reliability and multisite scheduling.
References
- Read
references/api-and-runtime.mdfor exact response fields, limits, provisioning, stale-screen, and hook details. - Active repository and source: https://github.com/WordPress/presence-api
- v0.1.23 release: https://github.com/WordPress/presence-api/releases/tag/v0.1.23
- Feature-plugin announcement: https://make.wordpress.org/core/2026/04/27/presence-api-feature-plugin/
- Verified source paths at tag
v0.1.23:presence-api.phpincludes/functions.phpincludes/class-wp-rest-presence-controller.phpincludes/heartbeat.phpincludes/screen-revisions.phpincludes/cron.phpsrc/hooks/use-presence-users.js
Files (wp-agent-skills)
-
agents
-
openai.yaml 295 B
interface: display_name: "WordPress Presence API" short_description: "Integrate the experimental Presence API safely" default_prompt: "Use $wp-presence-api to implement or audit this WordPress presence integration against feature plugin 0.1.23." policy: allow_implicit_invocation: true
-
-
references
-
api-and-runtime.md 5.7 KB
# Presence API 0.1.23 API and runtime contract Read this reference when implementing an integration rather than only deciding whether the Presence API is relevant. ## Storage and return shape Each site owns a `{prefix}presence` table with: - numeric primary key `id`; - `room` and `client_id`, each `varchar(191)` and unique as a pair; - `user_id`; - JSON state in `longtext`; - UTC refresh time in `date_gmt`; - indexes for time, user, and room/time reads. `wp_get_presence()` returns objects with `room`, `client_id`, `user_id`, decoded array `data`, and `date_gmt`. The direct PHP API does not hydrate user display names or avatars. The REST representation adds `display_name` and `avatar_url` and supports `_fields`. The default TTL is `WP_PRESENCE_DEFAULT_TTL` (60 seconds). Define the constant before plugin load or filter `wp_presence_default_ttl`. Keep it positive and test it against actual Heartbeat intervals and background-tab suspension. ## REST enforcement The controller applies these 0.1.23 boundaries: - `room` and `client_id`: non-empty string, maximum 191 characters; - `data`: JSON object, maximum encoded size 10,240 bytes; - nested arrays: sanitized recursively to three levels; - state values: strings, integers, floats, booleans, and arrays only; - entry listing: `per_page` 1-100, default 100; - room listing: `per_page` 1-100, default 50; - active entries per user: maximum 50; - same active `(room, client_id)` owned by someone else: HTTP 409; - unavailable table on a write: HTTP 503; - every collection response: `X-WP-Total`, `X-WP-TotalPages`, and `Cache-Control: no-store`. Sanitized REST strings are preserved as data and must still be escaped for the eventual HTML, attribute, URL, or JavaScript output context. `GET`, `POST`, and `DELETE` all call `wp_can_access_presence_room()`. A post room maps to `user_can( $user_id, 'edit_post', $post_id )`; other rooms map to `edit_posts`. Delete additionally checks database ownership, except users with `manage_options` may remove another user's entry. ## Heartbeat behavior The plugin uses these principal server hooks: - `heartbeat_received` priority 9: update `admin/online`; - priority 10: update the post editor entry; - priority 11: bridge a core post-lock refresh when the editor ping was absent; - priority 12: compare stale-screen revisions. The ping asset is enqueued only for logged-in users with `edit_posts`; on the front end the admin bar must also be showing. It writes an initial entry in the page request to avoid a gap until the first Heartbeat tick. Editor client IDs use `editor-{user_id}` and the admin room uses `user-{user_id}`. Do not assume one entry per user. A user can occupy multiple rooms and clients; REST consumers should deduplicate by `user_id` when displaying people rather than connections. ## Screen revisions Classic settings, post, user, term, and comment screens are covered. Custom admin screens need both sides: 1. filter `wp_presence_current_screen_key` to return a stable, non-empty key; 2. after a successful REST/AJAX save, call the plugin's browser-side `wp.presence.markScreenStale( key )` surface when available. The REST route accepts lowercase letters, digits, slash, underscore, and hyphen after normalization and caps the key at 191 characters. Its permission callback maps known screen-key families to relevant object capabilities. Custom fallback revisions share a bounded 200-entry option; known object screens use object metadata or dedicated state instead. `wp_presence_screen_revision_bumped` receives the screen key, revision, and actor ID after a successful bump. ## Editor and collaboration extension points `wp_presence_editor_state` receives current state, post ID, and user ID. Add only bounded, non-secret, JSON-compatible values: ```php add_filter( 'wp_presence_editor_state', static function ( array $state, int $post_id, int $user_id ): array { if ( current_user_can( 'edit_post', $post_id ) ) { $state['my_plugin_mode'] = 'review'; } return $state; }, 10, 3 ); ``` At 0.1.23, collaboration start/end threshold tracking uses a static variable inside `wp_presence_check_collaboration_threshold()`. That memory survives multiple calls only inside one PHP request. Normal Heartbeat transitions occur across requests, so do not interpret these actions as a durable or exactly-once session lifecycle. Query the room and make the downstream operation idempotent. ## Provisioning and cleanup Activation provisions the current site. Network activation iterates sites only when WordPress does not classify the network as large. New sites are provisioned after `wp_initialize_site`; missed/large-network sites reconcile on a real admin or CLI request. Front-end and AJAX request paths do not repeatedly run schema repair. Until provisioning succeeds, PHP reads return an empty array and writes return `false`. Cleanup runs through `wp_delete_expired_presence_data` on a custom one-minute WP-Cron interval. It selects and deletes bounded primary-key batches; the defaults are 1,000 rows per pass and 10 passes per invocation, filterable via `wp_presence_cleanup_batch_size` and `wp_presence_cleanup_max_passes`. Deactivation clears scheduled cleanup; uninstall drops each site's table and removes plugin revision/options/meta. Because WP-Cron is traffic-dependent, expiry filtering protects reads even when physical cleanup is delayed. ## Public documentation inconsistency At tag `v0.1.23`, `README.md` says “six public functions” and its code block lists six, while `includes/functions.php` explicitly says “Public API (7 functions)” and additionally lists `wp_presence_admin_room()`. The function's docblock is not marked private. Treat the tagged source as authoritative for this snapshot, but recheck the upstream contract on the next release.
-
-
SKILL.md 10.6 KB
--- name: wp-presence-api description: Implement or audit integrations with the experimental WordPress Presence API feature plugin 0.1.23. Covers the seven public PHP functions, post and admin rooms, the per-site wp_presence table and TTL, Heartbeat transport, REST read/write/delete/rooms endpoints, per-room capabilities and ownership, pagination and payload limits, post-type opt-in, usePresenceUsers source hook, stale-screen revisions, collaboration hooks, cleanup and multisite provisioning. Use for who-is-online, active-editor, post-lock, co-presence, Heartbeat, `wp_get_presence`, `wp_set_presence`, `wp-presence/v1`, or high-frequency ephemeral-state work. Do not confuse this experimental plugin with WordPress 7.1 core. license: GPLv2-or-later metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "presence-api" wp-skills-plugin-version-tested: "0.1.23" wp-skills-wp-version-tested: "7.1" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-20" --- # WordPress Presence API Integrate with Presence API 0.1.23 as an experimental feature plugin, not as a WordPress 7.1 core API. It supplies awareness of active users and editors using a dedicated per-site table, a 60-second TTL, Heartbeat, REST, admin surfaces, and a small public PHP API. Pin and feature-detect the plugin; its `0.1.x` contract may still change. ## When to use this skill - Build who-is-online, active-editor, post-lock, or co-presence UI. - Review `wp_get_presence()`, `wp_set_presence()`, `wp_presence_post_room()`, `presence-ping`, `wp_presence_editor_state`, or `/wp-presence/v1` code. - Add presence support to a custom post type. - Decide where to store high-frequency ephemeral state. - Audit Heartbeat load, room authorization, presence privacy, cleanup, or multisite behavior. ## Establish the runtime contract first Feature-detect a public function and avoid loading plugin internals yourself: ```php if ( ! function_exists( 'wp_get_presence' ) ) { return; } ``` Presence API 0.1.23 requires WordPress 7.0+ and PHP 7.4+. WordPress 7.1 does not provide these functions or the `wp_presence` table by itself. Do not test only `version_compare( get_bloginfo( 'version' ), '7.1', '>=' )`. Treat an experimental-plugin version constraint as a deliberate product decision. Fail softly when it is absent, and verify the installed source again before relying on signatures in a later `0.1.x` release. ## Use only the seven public PHP functions The source explicitly marks these as its public contract: | Function | Contract | |---|---| | `wp_get_presence( $room, $timeout )` | Return active entry objects for one room. | | `wp_set_presence( $room, $client_id, $state, $user_id )` | Atomically upsert one `(room, client_id)` row. | | `wp_remove_presence( $room, $client_id )` | Remove one client entry. | | `wp_remove_user_presence( $user_id )` | Remove a user's entries across all rooms. | | `wp_can_access_presence_room( $room, $user_id )` | Check the plugin's room access policy. | | `wp_presence_post_room( $post )` | Return the canonical post room or `false`. | | `wp_presence_admin_room()` | Return the canonical `admin/online` room. | Everything after the public section in `includes/functions.php` is marked private even when it has a global `wp_*` function name. Do not depend on `wp_get_active_rooms()`, `wp_get_presence_summary()`, table/provisioning helpers, or cleanup internals. The direct PHP write/remove functions are trusted server-side primitives. They do not reproduce the REST controller's room-length, payload, ownership, entry limit, or capability checks. Validate and authorize before calling them from any request handler. ## Model rooms and authorization together Core post types `post` and `page` opt in automatically. Add support to a custom post type during registration or afterwards: ```php register_post_type( 'book', array( 'show_ui' => true, 'supports' => array( 'title', 'editor', 'presence' ), ) ); $room = wp_presence_post_room( $book_id ); // postType/book:123 or false. ``` `postType/{post_type}:{id}` rooms require `edit_post` for that object. Other room strings, including `admin/online`, require only `edit_posts`. Therefore a custom room name is not a custom authorization boundary. Do not put data in a generic room when every user with `edit_posts` must not see it; enforce the narrower capability in your own server handler or use an object-backed room. Presence is awareness, not authorization. Never grant locks, saves, or content access merely because a user has a presence entry. ## Keep state ephemeral and bounded ```php $room = wp_presence_post_room( $post_id ); $client_id = 'my-plugin-' . get_current_user_id(); if ( $room && current_user_can( 'edit_post', $post_id ) ) { wp_set_presence( $room, $client_id, array( 'mode' => 'reviewing' ), get_current_user_id() ); } ``` Use stable, namespaced client IDs. Store only small UI state, never secrets, tokens, unpublished content bodies, or durable workflow state. Entries expire from reads after the TTL and are later removed in bounded cron batches. TTL is not a delivery guarantee, logout is not guaranteed to run, and a crashed tab can remain visible until expiry. The table is the correct architectural pattern for high-frequency awareness: it avoids repeatedly invalidating `wp_options` or object meta caches. It does not make every custom ephemeral feature a reason to depend on this plugin; use the public contract only when Presence API's room and capability model fit. ## Use the REST contract safely Authenticated endpoints are: | Method | Route | Purpose | |---|---|---| | `GET` | `/wp-presence/v1/presence` | Paginated entries for `room`. | | `POST` | `/wp-presence/v1/presence` | Upsert `room`, `client_id`, and `data`. | | `DELETE` | `/wp-presence/v1/presence` | Delete an owned entry; `manage_options` can delete any. | | `GET` | `/wp-presence/v1/presence/rooms` | Paginated, access-filtered active rooms. | | `POST` | `/wp-presence/v1/presence/screen-revisions/stale` | Bump an authorized screen revision. | Use `wp.apiFetch` in WordPress admin/editor JavaScript so the REST nonce and root middleware are applied. Respect pagination headers and request `_fields` when only user identity is needed. Responses use `Cache-Control: no-store`. The 0.1.23 controller bounds room/client IDs to 191 characters, REST state to 10 KiB and three nested array levels, list pages to 100 rows, and active entries to 50 per user. It rejects an active `client_id` owned by a different user and restricts delete-by-client ownership. Do not clone these values into a competing endpoint; use the plugin route or implement an independently reviewed contract. Read `references/api-and-runtime.md` before adding a REST client, custom room, screen-revision integration, or multisite dependency. ## Integrate Heartbeat without multiplying traffic The plugin enqueues WordPress Heartbeat and writes initial presence on eligible admin/front-end requests, then refreshes state through Heartbeat. The block editor can run Heartbeat faster than its normal 60-second interval for post locks. Do not add a second timer that posts the same state independently. The shipped `usePresenceUsers()` React hook performs one REST read initially and on Heartbeat ticks, deduplicates by user ID, supports `_fields`, and can exclude the current user. In 0.1.23 it is shipped as source, not as a registered WordPress package or script handle. Do not deep-import another installed plugin's filesystem path at runtime. If a build deliberately vendors that experimental source, pin the plugin release and review license/update drift; otherwise implement a small `wp.apiFetch` consumer around the REST contract. ## Treat collaboration hooks as advisory in 0.1.23 `wp_presence_editor_state` can enrich editor state. The plugin also declares `wp_presence_collaboration_started` and `wp_presence_collaboration_ended`. Do not use the latter actions for billing, durable workflow transitions, or exact participant lifecycle: in 0.1.23 threshold memory is a request-local static variable, so it does not persist a transition state across separate Heartbeat requests. Recompute current membership from the room for decisions and verify this implementation again after upgrading. ## Test the whole lifecycle 1. Absence of the feature plugin: integration fails softly. 2. Supported and unsupported post types: room string versus `false`. 3. Author can access own editable post but not a post they cannot edit. 4. Generic custom room visibility for every `edit_posts` user. 5. REST create/read/delete, ownership conflict, 191-character keys, oversized state, nested state, pagination, `_fields`, and `Cache-Control: no-store`. 6. Two tabs for one user and two users in one room; close/crash/logout/TTL. 7. Heartbeat active, slowed, suspended, and unavailable. 8. Table missing during a front-end request: reads return empty and writes return `false` instead of causing SQL errors. 9. Site activation, network activation, new-site creation, large network, cron cleanup, deactivation, and uninstall on a real multisite test network. 10. Dynamic UI with keyboard focus, empty avatar alt text, live-region announcements, and reduced motion. ## Critical rules - Presence API 0.1.23 is an experimental plugin, not WordPress 7.1 core. - Feature-detect it and use only the seven explicitly public PHP functions. - Keep capability checks at every write/read boundary; presence grants nothing. - Treat generic rooms as visible to all users with `edit_posts`. - Keep state small, non-secret, ephemeral, and retry-safe. - Reuse Heartbeat; do not add a competing polling loop. - Do not depend on private global helpers or exact internal table queries. - Do not treat collaboration threshold hooks as durable transition events. ## Cross-references - Use **`wp-rest-api`** when implementing a separate custom endpoint. - Use **`wp-plugin-options-storage`** for the custom-table decision. - Use **`wp-plugin-cron`** for cleanup reliability and multisite scheduling. ## References - Read `references/api-and-runtime.md` for exact response fields, limits, provisioning, stale-screen, and hook details. - Active repository and source: <https://github.com/WordPress/presence-api> - v0.1.23 release: <https://github.com/WordPress/presence-api/releases/tag/v0.1.23> - Feature-plugin announcement: <https://make.wordpress.org/core/2026/04/27/presence-api-feature-plugin/> - Verified source paths at tag `v0.1.23`: - `presence-api.php` - `includes/functions.php` - `includes/class-wp-rest-presence-controller.php` - `includes/heartbeat.php` - `includes/screen-revisions.php` - `includes/cron.php` - `src/hooks/use-presence-users.js`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.