Roles and Capabilities
Developer Guide Free & ProHow BuddyNext decides who can do what. This page covers the permission model developers extend: the single buddynext_can() entry point, the four resolution layers in PermissionService, the capability catalog registered through the WordPress Abilities API, and the filter seams (buddynext_user_can, buddynext_role_map, buddynext_abilities) an extension uses to add, gate, or override a capability.

Overview / Contract#
BuddyNext does not register any custom WordPress roles. Neither Free nor Pro makes an add_role() call, and Pro's manifest confirms this with customRoles: []. There are no new WP_Role capabilities to map. Member authority is modeled in two BuddyNext-owned layers instead:
- A community role stored in user meta (
bn_community_role): one ofmember,moderator,admin. Defaults tomemberwhen unset.RoleService::set_role()ignores any other value;ownerexists only as a per-space role (below). - A per-space role stored in the
bn_space_memberstable (rolecolumn):owner,moderator, ormemberfor an active membership.
WordPress site administrators are recognized through the native manage_options capability, not a BuddyNext role. Every wp-admin screen BuddyNext registers gates on manage_options (see Admin Pages and Settings).
Every permission decision in the plugin flows through one function:
buddynext_can( int $user_id, string $capability, array $context = array() ): bool
Defined in buddynext.php, it resolves the permissions service from the container and calls PermissionService::can(). Never call current_user_can() against a BuddyNext capability or read bn_community_role directly to make an authorization decision - route it through buddynext_can() so all four layers and the filter seams apply.
The four resolution layers#
PermissionService::can() resolves a check in this order:
| Layer | Source | Effect |
|---|---|---|
| 1. WP site admin | manage_options |
Holders pass every check ($result = true). Logged-out visitors resolve to false before this layer. |
| 2. Community / space role | ROLE_MAP + role hierarchy |
The capability needs a minimum role; the user's community role must meet or exceed it. With a space_id in context the user must be an active member of that space, and for the Roles tab rows (spaces/post, spaces/moderate) the higher of their in-space role and their community role counts (a space owner ranks above Admin). The owner-scoped spaces/manage-settings and spaces/delete use the in-space role alone. |
| 3. Explicit ability grant | bn_ability_{slug} user meta |
A per-user grant with an expiry (0 = never, otherwise a unix timestamp). Checked only when the role check fails. |
| 4. Developer filter | buddynext_user_can |
Runs on every check and can flip the resolved result in either direction. Always the final word. |
Three hard-deny short-circuits apply around the layers above:
- A user who is space-banned (a row in
bn_space_bans, or abn_space_membersrow withstatus = 'banned') is denied everybuddynext-spaces/*capability when aspace_idis in context, regardless of role. - A suspended member is denied every capability except
buddynext-profile/viewandbuddynext-moderation/review-queue(PermissionService::READ_CAPS), so write controls hide and writes refuse from one source. Site admins are exempt. - The space-scoped capabilities
buddynext-moderate-space,buddynext-manage-space, andbuddynext-own-spacebypass the generic role map and resolve through dedicated methods that read the caller's role in that specific space.
The role hierarchy#
Roles are ranked numerically. A capability mapped to a role is granted to that role and everything above it. PermissionService ranks four roles; the community role only ever holds admin, moderator or member, and owner applies to the in-space role of a space owner.
| Role | Weight |
|---|---|
owner |
4 |
admin |
3 |
moderator |
2 |
member |
1 |
A capability mapped to null in the role map has no role gate - it can only be granted through an explicit ability grant or the buddynext_user_can filter.
The capability catalog#
Capabilities are dot-namespaced slugs (buddynext-{domain}/{action}).
There are three lists in code and they are not the same list. Know which one you are looking at:
| List | Where | What it decides |
|---|---|---|
PermissionService::ROLE_MAP |
includes/Core/PermissionService.php |
Enforcement. The default role each capability requires. This is the authority - if a slug is here, it is gated. |
Abilities::CATALOG |
includes/Core/Abilities.php |
Registration with the WordPress Abilities API (WP 6.9+), so the capability appears in the admin Abilities UI and can be granted or revoked through that API. On WordPress below 6.9 this no-ops silently and PermissionService still enforces the gate. |
RolesTab::catalog() |
includes/Admin/RolesTab.php |
The admin Roles tab. A deliberate subset - the capabilities a site owner is offered a UI control for. handle_save() drops any submitted slug that is not in it. |
Enforcement is the list that matters for buddynext_can(). The other two only decide whether an owner gets a control for it.
The role map holds 28 generic capabilities:
| Capability | Default required role |
|---|---|
buddynext-profile/edit-own |
member |
buddynext-profile/edit-any |
admin (the edit-profile page, PUT /users/{id}/profile, and a member's self-select member type; types with self-select off stay with site admins) |
buddynext-profile/view |
none (public) |
buddynext-feed/create-post |
member |
buddynext-feed/delete-own-post |
member |
buddynext-feed/delete-any-post |
moderator (any post or comment; on the Roles tab as "Delete anyone's post or comment") |
buddynext-feed/announce |
admin (post and end site-wide announcements; "Post site-wide announcements"; a space announcement follows the space role) |
buddynext-feed/edit-any-post |
admin (edit any post or comment, hide a post, edit past the edit window; "Edit anyone's post or comment"). Anyone but a site administrator acts only on posts they can already read, and a post's audience changes only when its author edits it |
buddynext-feed/schedule-post |
member |
buddynext-feed/interact |
member (umbrella for react, share, bookmark and poll vote; on the Roles tab as "React, share, bookmark and vote" and enforced on those REST routes) |
buddynext-comments/create |
member |
buddynext-spaces/create |
member (see note) |
buddynext-spaces/join |
member |
buddynext-spaces/join-gated |
none |
buddynext-spaces/post |
member |
buddynext-spaces/moderate |
moderator (always checked with a space_id: who may work the space's report queue. Community Admin opens on buddynext-moderation/review-queue instead, so a stricter Moderate spaces row never locks the moderation team out of it. Approving, banning and removing members inside a space stays with the space's own owner and moderators, plus WordPress administrators) |
buddynext-spaces/manage-settings |
moderator |
buddynext-spaces/delete |
moderator |
buddynext-connections/follow |
member |
buddynext-connections/connect |
member |
buddynext-moderation/report |
member |
buddynext-moderation/review-queue |
moderator |
buddynext-moderation/dismiss |
moderator (acting on a report; on the Roles tab as "Resolve reports") |
buddynext-moderation/issue-strike |
moderator |
buddynext-moderation/suspend-user |
moderator (for a set number of days) |
buddynext-moderation/suspend-indefinitely |
admin (a suspension with no end date; "Suspend members with no end date") |
buddynext-moderation/resolve-appeals |
admin (read and decide appeals; "Decide appeals"; still passed through buddynext_can_resolve_appeals) |
buddynext-community/assign-roles |
admin (change members' community roles on the Community Admin panel; fixed, not on the Roles tab) |
A site-wide moderator holds day-to-day moderation authority without being a WordPress administrator. Clearing the queue, removing any post or comment, issuing strikes and warnings, suspending for a set time, shadow-banning, bulk moderation, and seeing posts from suspended or shadow-banned members are all moderator powers. Open-ended suspensions, appeals, editing other members' content, site-wide announcements and role changes default to the community
adminrole; an owner can move all but role changes on the Roles tab. Granting the community Admin role itself, and site configuration in wp-admin, stay with WordPress administrators (manage_options). Everyone holdingbuddynext-moderation/review-queuegets the new-report notification, linked to the front-end Community Admin queue. Whether a viewer may read a hidden author's post is answered in one place,ModerationService::hides_author_from( $author_id, $viewer_id ), which the feed, the post page and the REST read all use.
buddynext-spaces/createis set here and nowhere else. The Spaces > Settings tab links to this tab instead of keeping its own "Who can create spaces" select. Version 1.2.7 retired the oldbuddynext_space_creation_roleoption: on upgrade, a stored moderator or admin choice is copied intobn_role_map_overrides(unless this tab already had a row for the ability) and the option is deleted.
buddynext-comments/createis grantable from the admin. It is present inPermissionService::ROLE_MAP, inAbilities::CATALOG, and inRolesTab::catalog(), so it is registered with the WordPress Abilities API and renders a row ("Comment on posts") on the Roles and Capabilities tab like any other capability. The filter also works if you want to set the role in code:add_filter( 'buddynext_role_map', static function ( array $map ): array { $map['buddynext-comments/create'] = 'moderator'; return $map; } );
Three additional space-scoped capabilities - buddynext-moderate-space, buddynext-manage-space, and buddynext-own-space - are resolved by dedicated per-space methods (can_moderate_space() / can_manage_space() / can_own_space()) and are not part of the generic role map. buddynext-moderate-space and buddynext-manage-space are both granted to a space owner OR moderator (moderators can manage space settings, they just cannot remove the owner or delete the space); buddynext-own-space is the strictly owner-only gate, split out for the two things a moderator must never do because neither is reversible from their side: delete the space, and change who owns it (transfer ownership, assign a new owner).
Ability slugs may contain
/and-. The grant meta key translates those to_, sobuddynext-feed/schedule-postis stored asbn_ability_buddynext_feed_schedule_post. UsePermissionService::ability_meta_key( $slug )to build the key rather than hand-rolling it.
Examples#
Check a capability#
// A plain capability check.
if ( buddynext_can( get_current_user_id(), 'buddynext-feed/create-post' ) ) {
// Render the composer.
}
// A space-scoped check - pass the space_id in context.
if ( buddynext_can( $user_id, 'buddynext-spaces/post', array( 'space_id' => 42 ) ) ) {
// Allow posting into space 42.
}
Register a new capability and gate it behind a role#
Adding a capability is two filters: register the slug with the Abilities API, then give it a role in the role map. Do both on plugins_loaded (or earlier) so they are in place before any check runs.
// 1. Add the slug to the catalog so the Abilities API registers it.
add_filter( 'buddynext_abilities', function ( array $abilities ) {
$abilities[] = 'buddynext-feed/create-event';
return $abilities;
} );
// 2. Gate it behind a community role. 'member', 'moderator', 'admin', 'owner',
// or null for "no role gate" (explicit grant / filter only).
add_filter( 'buddynext_role_map', function ( array $map ) {
$map['buddynext-feed/create-event'] = 'moderator';
return $map;
} );
The capability is now enforceable through buddynext_can( $user_id, 'buddynext-feed/create-event' ).
Override a capability for one decision#
buddynext_user_can is the final layer and runs on every check, so it can grant or deny regardless of what the role and grant layers resolved.
add_filter(
'buddynext_user_can',
function ( bool $result, int $user_id, string $capability, array $context ) {
// Let verified members delete any post even though that defaults to moderator.
if ( 'buddynext-feed/delete-any-post' === $capability
&& get_user_meta( $user_id, 'is_verified_member', true ) ) {
return true;
}
return $result;
},
10,
4
);
Grant a capability to one user#
An explicit grant (layer 3) is a user-meta entry whose value is the expiry: 0 for no expiry, or a future unix timestamp.
use BuddyNext\Core\PermissionService;
// Grant the review-queue capability to user 12, never expiring.
update_user_meta(
12,
PermissionService::ability_meta_key( 'buddynext-moderation/review-queue' ),
0
);
Notes / gotchas#
- No custom roles to clean up. Because BuddyNext registers no
WP_Role, there is nothing to remove on deactivation and no capability bloat on theadministratorrole. Authority lives in user meta andbn_space_members. manage_optionsis the admin gate, not a BuddyNext role. Site admins pass everybuddynext_can()check by virtue of layer 1, and every BuddyNext admin screen checksmanage_optionsdirectly.buddynext_role_mapfires once and is memoised per request;buddynext_user_canfires on every check. Put baseline role mappings in the role map and per-decision logic in the filter.- Space context matters. For
buddynext-spaces/*capabilities, pass['space_id' => N]: only an active member of that space passes, and on a Roles tab row the higher of their in-space role (frombn_space_members) and their community role counts;manage-settingsanddeleteread the in-space role alone. Thejoincapability is the exception - a non-member has no in-space role yet, sojoinis gated by the community role, with space type (open/request/invite) enforced separately in the join flow. - Free / Pro boundary. The catalog and the four-layer model live entirely in free. Pro consumes
buddynext_can()like any other caller and does not add custom roles or its own permission engine.
See also Admin Pages and Settings for how the admin surface gates on manage_options, and the REST contract page for how controllers resolve buddynext_can() before serving a request.