BuddyNext - community engine for WordPress

Roles and Capabilities

Free & Pro

How 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.

The Members Roles admin tab gated by the capability model and buddynext_can() entry point documented here

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 of member, moderator, admin. Defaults to member when unset. RoleService::set_role() ignores any other value; owner exists only as a per-space role (below).
  • A per-space role stored in the bn_space_members table (role column): owner, moderator, or member for 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 a bn_space_members row with status = 'banned') is denied every buddynext-spaces/* capability when a space_id is in context, regardless of role.
  • A suspended member is denied every capability except buddynext-profile/view and buddynext-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, and buddynext-own-space bypass 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 admin role; 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 holding buddynext-moderation/review-queue gets 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/create is 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 old buddynext_space_creation_role option: on upgrade, a stored moderator or admin choice is copied into bn_role_map_overrides (unless this tab already had a row for the ability) and the option is deleted.

buddynext-comments/create is grantable from the admin. It is present in PermissionService::ROLE_MAP, in Abilities::CATALOG, and in RolesTab::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 _, so buddynext-feed/schedule-post is stored as bn_ability_buddynext_feed_schedule_post. Use PermissionService::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 the administrator role. Authority lives in user meta and bn_space_members.
  • manage_options is the admin gate, not a BuddyNext role. Site admins pass every buddynext_can() check by virtue of layer 1, and every BuddyNext admin screen checks manage_options directly.
  • buddynext_role_map fires once and is memoised per request; buddynext_user_can fires 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 (from bn_space_members) and their community role counts; manage-settings and delete read the in-space role alone. The join capability is the exception - a non-member has no in-space role yet, so join is 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.