Roles and Capabilities
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,owner. Defaults tomemberwhen unset. - 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). |
| 2. Community / space role | ROLE_MAP + role hierarchy |
The capability needs a minimum role; the user's community role (or in-space role when space_id is in context) must meet or exceed it. |
| 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. |
Two hard-deny short-circuits run before 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. - The space-scoped capabilities
buddynext-moderate-spaceandbuddynext-manage-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.
| 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 22 generic capabilities:
| Capability | Default required role |
|---|---|
buddynext-profile/edit-own |
member |
buddynext-profile/edit-any |
admin |
buddynext-profile/view |
none (public) |
buddynext-feed/create-post |
member |
buddynext-feed/delete-own-post |
member |
buddynext-feed/delete-any-post |
moderator |
buddynext-feed/pin-post |
moderator |
buddynext-feed/schedule-post |
member |
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 |
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/issue-strike |
moderator |
buddynext-moderation/suspend-user |
moderator |
A site-wide moderator holds full moderation authority. Clearing the queue, issuing strikes, suspending, shadow-banning, and warnings are all moderator powers. Only appeals, and editing what each role is allowed to do, remain admin-only.
buddynext-spaces/createhas one option that overrides its default. The legacy Spaces-tab "who can create spaces" setting is folded into the role map: whenget_option( 'buddynext_space_creation_role' )is'admin', the map default flips frommembertoadmin. The default ('member') leaves the map untouched. It is applied insiderole_map()so it composes with the Roles and Capabilities tab instead of fighting it.
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. An earlier version of this page said it was absent from the catalog and reachable only through thebuddynext_role_mapfilter; that has not been true for some time. The filter still 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; } );
Two additional space-scoped capabilities - buddynext-moderate-space and buddynext-manage-space - are resolved by dedicated per-space methods (can_moderate_space() / can_manage_space()) and are not part of the generic role map. buddynext-moderate-space is granted to a space owner or moderator; buddynext-manage-space only to the space owner.
Ability slugs may contain
/and-. The grant meta key translates those to_, sobuddynext-feed/pin-postis stored asbn_ability_buddynext_feed_pin_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 pin posts even though pin defaults to moderator.
if ( 'buddynext-feed/pin-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]so the in-space role (frombn_space_members) is used. 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.