WordPress Capabilities API: Fine-Grained Permissions for Custom Post Types

WordPress’s capability system lets you define fine-grained permissions that go far beyond the five default roles. Custom capabilities, primitive vs meta capabilities, and the map_meta_cap filter give you complete control over who can do what — essential for membership sites, client portals, and multi-author publications.

Problem: WordPress plugins check user capabilities with hardcoded strings scattered throughout the codebase — using current_user_can('manage_options') everywhere conflates broad admin access with specific feature access, making it impossible to grant partial permissions to contributors.

Solution: Define plugin-specific capabilities (edit_my_plugin_settings, view_my_plugin_reports) and map them onto roles with get_role()->add_cap() during plugin activation. Gate every feature with the specific capability, never with role names or manage_options. Use map_meta_cap to handle object-level capabilities like edit_post.

The examples below add custom capabilities for a "Document" CPT, map meta capabilities to ownership-based checks, assign capabilities to roles, and use the capabilities system to gate plugin features.

 [ 'document', 'documents' ],  // singular, plural
    'map_meta_cap'    => true,    // let WordPress map primitive caps via map_meta_cap filter
    'capabilities'    => [
        'edit_post'    => 'edit_document',
        'read_post'    => 'read_document',
        'delete_post'  => 'delete_document',
        'edit_posts'   => 'edit_documents',
        'publish_posts'=> 'publish_documents',
    ],
    // ... other args
] );

// Grant capabilities to roles (run once on plugin activation)
function myplugin_setup_roles(): void {
    // Editor: can manage all documents
    $editor = get_role( 'editor' );
    $editor?->add_cap( 'edit_documents' );
    $editor?->add_cap( 'edit_others_documents' );
    $editor?->add_cap( 'publish_documents' );
    $editor?->add_cap( 'delete_documents' );
    $editor?->add_cap( 'delete_others_documents' );

    // Author: can create and edit own documents only
    $author = get_role( 'author' );
    $author?->add_cap( 'edit_documents' );
    $author?->add_cap( 'publish_documents' );
    $author?->add_cap( 'delete_documents' );
    // Note: NOT 'edit_others_documents'

    // Subscriber: can read documents
    $subscriber = get_role( 'subscriber' );
    $subscriber?->add_cap( 'read_document' );
}
register_activation_hook( __FILE__, 'myplugin_setup_roles' );

// Clean up on deactivation
register_deactivation_hook( __FILE__, function() {
    foreach ( [ 'editor', 'author', 'subscriber' ] as $role_name ) {
        $role = get_role( $role_name );
        foreach ( [ 'edit_documents','edit_others_documents','publish_documents',
                    'delete_documents','delete_others_documents','read_document' ] as $cap ) {
            $role?->remove_cap( $cap );
        }
    }
} );

Map meta capabilities to ownership-based checks:

post_type ) {
        return $caps;
    }

    $post_author = (int) $post->post_author;
    $is_own      = ( $post_author === $user_id );

    switch ( $cap ) {
        case 'edit_document':
            $caps = $is_own
                ? [ 'edit_documents' ]
                : [ 'edit_others_documents' ];
            break;

        case 'delete_document':
            $caps = $is_own
                ? [ 'delete_documents' ]
                : [ 'delete_others_documents' ];
            break;

        case 'read_document':
            // Published documents are public; drafts require capability
            if ( 'publish' === $post->post_status ) {
                $caps = [ 'read' ];
            } elseif ( $is_own ) {
                $caps = [ 'read' ];
            } else {
                $caps = [ 'read_private_documents' ];
            }
            break;
    }

    return $caps;
}, 10, 4 );

// Check in templates or controllers:
if ( current_user_can( 'edit_document', $doc_id ) ) {
    // show edit button
}
if ( current_user_can( 'read_document', $doc_id ) ) {
    // show content
}

NOTE: Avoid storing custom capabilities in user meta with $user->add_cap() directly — that creates per-user capability overrides that are hard to audit and maintain. Use role-based capabilities (get_role()->add_cap()) for group permissions, and user->add_cap() only for individual exceptions like temporarily elevating a specific user.

Leave Comment

Your email address will not be published. Required fields are marked *