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.