Managing access with WordPress custom capabilities

A client called recently because their custom CRM plugin, built on WordPress, had turned into a nightmare. Every time they wanted to restrict access to a new data set or feature, the previous developer had just bolted on another current_user_can() check. The result was a sprawling mess: impossible to debug and fragile. Hundreds of lines of conditional logic just to decide who could see what.

That’s the job of proper WordPress custom capabilities. They go beyond user roles: you define fine-grained permissions that keep custom applications maintainable as they grow. Without them, you’re stuck fighting spaghetti code.

Extending WordPress custom capabilities

My first instinct, I’ll admit, was to add more if ( current_user_can(...) ) statements. Quick fix, right? But that’s a trap. It works for about five minutes, until someone adds a new role or changes a permission, and then you’re debugging why half your users can’t reach what they need, or worse, can reach things they shouldn’t. The fix had to live at the capability level.

WordPress has a capable, often overlooked system for defining custom capabilities. It isn’t only for core actions; you can use it for your plugin’s own features. Think of it as a small access-management API of your own, similar to what the WordPress Core-AI team’s “Abilities API” is after, as covered in their recent check-in. They want to standardize access for AI features; you can do the same for your custom work.

The trick is to register these capabilities properly and then assign them to roles. Here’s a basic example that sets up a custom capability for managing ‘bbioon_crm_records’ when your plugin activates:

<?php
/**
 * Register custom capabilities for bbioon CRM plugin.
 */
function bbioon_register_custom_capabilities() {
    // Get the administrator role.
    $role = get_role( 'administrator' );

    // Ensure the role exists before adding capabilities.
    if ( null !== $role ) {
        $role->add_cap( 'bbioon_manage_crm_records' );
        $role->add_cap( 'bbioon_edit_crm_record' );
        $role->add_cap( 'bbioon_delete_crm_record' );
    }

    // Example for a custom role if you have one.
    // $custom_role = get_role( 'bbioon_crm_manager' );
    // if ( null !== $custom_role ) {
    //     $custom_role->add_cap( 'bbioon_manage_crm_records' );
    //     $custom_role->add_cap( 'bbioon_edit_crm_record' );
    // }
}
add_action( 'admin_init', 'bbioon_register_custom_capabilities' );

/**
 * Remove custom capabilities when the plugin is deactivated.
 */
function bbioon_remove_custom_capabilities() {
    $role = get_role( 'administrator' );
    if ( null !== $role ) {
        $role->remove_cap( 'bbioon_manage_crm_records' );
        $role->remove_cap( 'bbioon_edit_crm_record' );
        $role->remove_cap( 'bbioon_delete_crm_record' );
    }

    // Also remove from custom roles if they exist.
    // $custom_role = get_role( 'bbioon_crm_manager' );
    // if ( null !== $custom_role ) {
    //     $custom_role->remove_cap( 'bbioon_manage_crm_records' );
    //     $custom_role->remove_cap( 'bbioon_edit_crm_record' );
    // }
}
register_deactivation_hook( __FILE__, 'bbioon_remove_custom_capabilities' );

/**
 * Check if the current user can manage CRM records.
 *
 * @return bool True if the user can manage, false otherwise.
 */
function bbioon_can_manage_crm() {
    return current_user_can( 'bbioon_manage_crm_records' );
}

/**
 * Display content based on capability.
 */
function bbioon_display_admin_content() {
    if ( bbioon_can_manage_crm() ) {
        echo '<!-- wp:paragraph --><p>You have permission to manage CRM records.</p><!-- /wp:paragraph -->';
    } else {
        echo '<!-- wp:paragraph --><p>Access Denied: You cannot manage CRM records.</p><!-- /wp:paragraph -->';
    }
}
// Example of how you'd use it in an admin page or shortcode.
// add_action( 'admin_notices', 'bbioon_display_admin_content' );
?>

This code registers three custom capabilities: bbioon_manage_crm_records, bbioon_edit_crm_record, and bbioon_delete_crm_record. It assigns them to the ‘administrator’ role when the plugin activates and removes them on deactivation. Then you call a small wrapper, bbioon_can_manage_crm(), which in turn calls current_user_can(). The difference is that you’re now checking against capabilities you defined rather than arbitrary strings.

No more capability chaos

So: don’t build custom functionality without proper capability management. It’s an architectural decision that saves you, and whoever maintains the code next, a lot of debugging time. It keeps your code cleaner, more secure, and much easier to extend. That structure is the difference between custom development you can maintain and the kind of mess I opened with.

This stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.