Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 035 / 100

Users, roles and capabilities: best practices explained

Control who can do what.

Expona Admin7 min read

Users, roles and capabilities: best practices explained

WordPress security hinges on understanding the permission layer that sits between your users and destructive actions. Whether you're building a multi-author blog, a membership site, or a complex SaaS platform running on WordPress, getting users, roles and capabilities right prevents unauthorized access and keeps your codebase predictable. In this article, we'll walk through the architecture that powers WordPress permissions and show you how to implement it safely in your plugins.

Roles bundle capabilities

At the heart of WordPress permissions sits a simple model: roles are collections of capabilities. When you assign a user the "Editor" role, you're not giving them a label—you're granting them a specific set of powers, like edit_posts, delete_posts, and publish_posts.

WordPress ships with five default roles:

  • Administrator — full system access, can manage users, themes, plugins, and settings.
  • Editor — can create, edit, and publish posts (including others' posts) and manage categories.
  • Author — can create and publish their own posts only.
  • Contributor — can write and manage their own posts but cannot publish.
  • Subscriber — read-only access, typically for logged-in users viewing protected content.

Each role holds a set of capability strings. For example, the Author role includes:

  • edit_posts
  • edit_published_posts
  • delete_posts
  • delete_published_posts
  • publish_posts
  • read

You can inspect roles programmatically:

php
global $wp_roles;

// Get all capabilities for a role
$author_caps = $wp_roles->get_role( 'author' )->capabilities;
var_dump( $author_caps );

// Check if role exists
if ( $wp_roles->is_role( 'my_custom_role' ) ) {
    echo 'Role exists';
}

Understanding this structure is crucial because it informs how you design plugin features. If you're adding functionality that only certain types of users should access, you need to tie it to capabilities, not hardcoded role names. This gives site administrators the flexibility to assign those capabilities to any role—default or custom—without your plugin breaking.

Add custom capabilities for plugin features

When building plugins, you'll often need permissions that don't map to WordPress's built-in capabilities. Suppose you're developing a testimonial management plugin. You want some users to submit testimonials, others to moderate them, and only administrators to export data. The publish_posts capability isn't relevant here—you need custom capabilities like submit_testimonials, moderate_testimonials, and export_testimonial_data.

Register custom capabilities on plugin activation using add_role() for new roles or add_cap() to grant capabilities to existing roles:

php
// In your plugin activation hook
register_activation_hook( __FILE__, 'my_plugin_create_roles' );

function my_plugin_create_roles() {
    // Create a new role and grant it specific capabilities
    add_role(
        'testimonial_moderator',
        'Testimonial Moderator',
        array(
            'read'                   => true,
            'moderate_testimonials'  => true,
            'delete_testimonials'    => true,
            'export_testimonial_data' => false, // They can't do this
        )
    );

    // Or add capabilities to an existing role
    $editor_role = get_role( 'editor' );
    if ( $editor_role ) {
        $editor_role->add_cap( 'submit_testimonials' );
        $editor_role->add_cap( 'moderate_testimonials' );
    }
}

Keep a clear naming convention for your custom capabilities. We recommend prefixing them with your plugin slug: my_plugin_manage_settings, my_plugin_view_reports. This prevents collisions if another plugin uses the same capability name.

When you register custom capabilities, avoid hardcoding which roles get them. Instead, let administrators decide via your plugin's settings page or use sensible defaults (like giving editors the capability) but make it configurable.

Check capabilities, not roles

This is the single biggest mistake we see in plugin code: checking for specific roles instead of capabilities. Code like this is brittle:

php
// DON'T DO THIS
if ( 'administrator' === $user->roles[0] ) {
    echo 'Admin only content';
}

Why? Because an administrator might customize their setup so that Editors have certain powers too. If you hardcode role checks, you're forcing rigid permission logic and ignoring the site's actual configuration.

Instead, always use current_user_can() or user_can() to check capabilities:

php
// CORRECT
if ( current_user_can( 'moderate_testimonials' ) ) {
    echo 'Moderation panel';
}

// For a specific user
$user_id = 42;
if ( user_can( $user_id, 'export_testimonial_data' ) ) {
    // Generate export file
}

WordPress resolves the capability by checking if the current (or specified) user's roles include that capability. If a custom role has been granted it, the check passes. If the administrator has removed it from a role, the check fails—automatically and correctly.

This also applies to multisite setups where capabilities may differ between sites. A user might be an administrator on site A but an editor on site B. Using current_user_can() respects the context automatically.

When displaying admin notices, form fields, or menu items, wrap them in capability checks early:

php
// In admin_menu hook
add_action( 'admin_menu', 'my_plugin_add_menu' );

function my_plugin_add_menu() {
    if ( ! current_user_can( 'manage_testimonials' ) ) {
        return;
    }

    add_menu_page(
        'Testimonials',
        'Testimonials',
        'manage_testimonials',
        'my-testimonials',
        'my_plugin_admin_page'
    );
}

Notice we're checking the capability twice: once before registering the menu, and again in the manage_testimonials parameter to add_menu_page(). This redundancy is intentional—it protects against direct URL access and menu traversal attacks.

Remove capabilities on uninstall

When a plugin is deleted, you should clean up the custom roles and capabilities it created. Leaving them behind creates orphaned permissions that confuse administrators and clutter the database.

Use a plugin uninstall hook to remove custom roles and capabilities:

php
// In your main plugin file
register_uninstall_hook( __FILE__, 'my_plugin_uninstall' );

function my_plugin_uninstall() {
    // Remove custom roles
    remove_role( 'testimonial_moderator' );

    // Remove capabilities from existing roles
    $editor_role = get_role( 'editor' );
    if ( $editor_role ) {
        $editor_role->remove_cap( 'submit_testimonials' );
        $editor_role->remove_cap( 'moderate_testimonials' );
    }

    $admin_role = get_role( 'administrator' );
    if ( $admin_role ) {
        $admin_role->remove_cap( 'export_testimonial_data' );
    }
}

Be thorough and check all roles you've modified. If you added a capability to editors, authors, and subscribers, remove it from all three. Use remove_role() for custom roles only—never remove WordPress's default roles.

If your plugin manages complex hierarchies of permissions, consider logging which capabilities you added during activation so you can safely remove them on uninstall. Store this in a plugin option:

php
register_activation_hook( __FILE__, 'my_plugin_create_roles' );

function my_plugin_create_roles() {
    $added_caps = array(
        'editor'        => array( 'submit_testimonials' ),
        'administrator' => array( 'export_testimonial_data' ),
    );

    foreach ( $added_caps as $role_name => $caps ) {
        $role = get_role( $role_name );
        if ( $role ) {
            foreach ( $caps as $cap ) {
                $role->add_cap( $cap );
            }
        }
    }

    update_option( 'my_plugin_added_capabilities', $added_caps );
}

register_uninstall_hook( __FILE__, 'my_plugin_uninstall' );

function my_plugin_uninstall() {
    $added_caps = get_option( 'my_plugin_added_capabilities', array() );

    foreach ( $added_caps as $role_name => $caps ) {
        $role = get_role( $role_name );
        if ( $role ) {
            foreach ( $caps as $cap ) {
                $role->remove_cap( $cap );
            }
        }
    }

    delete_option( 'my_plugin_added_capabilities' );
}

Common mistakes

  • Checking roles instead of capabilities: Role names vary per site. Capabilities are the stable contract. Always use current_user_can().
  • Forgetting to verify capabilities in AJAX handlers: AJAX endpoints must check current_user_can() before processing data. Don't assume a nonce means the user is authorized for the action.
  • Creating capabilities without documentation: If you add custom capabilities, document them in your plugin's README or admin settings. Administrators need to know what they're granting.
  • Not removing capabilities on deactivation: Using register_deactivation_hook() to remove capabilities temporarily is fine, but use register_uninstall_hook() for permanent cleanup when the plugin is deleted.
  • Assigning capabilities to users directly: WordPress allows adding capabilities to individual users via $user->add_cap(), but avoid this. Always work through roles. Individual capabilities become unmaintainable at scale.

Key takeaways

  • Roles are bundles of capabilities; WordPress provides five default roles and lets administrators create custom ones.
  • For plugin features, define custom capabilities with a clear naming convention and register them on activation.
  • Always check capabilities using current_user_can() or user_can(), never hardcode role names.
  • Remove custom roles and capabilities on plugin uninstall to keep the database clean and prevent orphaned permissions.
  • Think of capabilities as the stable API of your permissions system; roles are implementation details that administrators control.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles