{"id":303803,"date":"2026-05-11T06:51:31","date_gmt":"2026-05-11T06:51:31","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/user-teams\/"},"modified":"2026-05-11T06:52:52","modified_gmt":"2026-05-11T06:52:52","slug":"user-teams","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/user-teams\/","author":148148,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.5","stable_tag":"0.5","tested":"7.0","requires":"6.9","requires_php":"7.4","requires_plugins":null,"header_name":"User Teams","header_author":"Dion Hulse","header_description":"Unix-style user groups for WordPress multisite, exposed as \"Teams\". Define teams, attach a role to each, and grant access to users by team membership across the network.","assets_banners_color":"","last_updated":"2026-05-11 06:52:52","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/dd32\/wp-user-teams","header_author_uri":"https:\/\/dd32.id.au\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":30,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","faq"],"tags":{"0.5":{"tag":"0.5","author":"dd32","date":"2026-05-11 06:52:52"}},"upgrade_notice":[],"ratings":[],"assets_icons":[],"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["0.5"],"block_files":[],"assets_screenshots":[],"screenshots":[],"jetpack_post_was_ever_published":false},"plugin_section":[],"plugin_tags":[441,895,1915,3491,1917],"plugin_category":[51,54,58],"plugin_contributors":[77591],"plugin_business_model":[],"class_list":["post-303803","plugin","type-plugin","status-publish","hentry","plugin_tags-multisite","plugin_tags-permissions","plugin_tags-roles","plugin_tags-team","plugin_tags-users","plugin_category-multisite","plugin_category-security-and-spam-protection","plugin_category-user-management","plugin_contributors-dd32","plugin_committers-dd32"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/s.w.org\/plugins\/geopattern-icon\/user-teams.svg","icon_2x":false,"generated":true},"screenshots":[],"raw_content":"<!--section=description-->\n<p>On a large multisite network, granting a group of people the same role across many sites usually means adding each user to each site by hand \u2014 and remembering to remove them everywhere when they leave. User Teams turns that into:<\/p>\n\n<ol>\n<li>Create a team, pick a role.<\/li>\n<li>Add the user to the team.<\/li>\n<li>They have that role everywhere the team applies.<\/li>\n<li>Remove them from the team \u2192 access is gone everywhere.<\/li>\n<\/ol>\n\n<p>User Teams is multisite-only by design \u2014 on a single-site install, native WordPress roles and capabilities already cover the whole feature set, so WordPress won't offer it for activation there.<\/p>\n\n<h4>Features<\/h4>\n\n<ul>\n<li><strong>Teams with roles.<\/strong> Each team carries a WordPress role (editor, administrator, a custom role, or none).<\/li>\n<li><strong>Runtime capability grants.<\/strong> Team access is added via the <code>user_has_cap<\/code> filter, so removing a user from a team drops their access on the next request \u2014 no stale capabilities usermeta to clean up.<\/li>\n<li><strong>Multisite-native.<\/strong> Team definitions are real <code>wp_users<\/code> rows (\"team accounts\"), so <code>WP_Users_List_Table<\/code>, <code>is_user_member_of_blog()<\/code>, and every column-adding plugin \"just work\".<\/li>\n<li><strong>Site scoping.<\/strong> A team can apply network-wide (including sites added later) or to a specific list of sites, each with its own role.<\/li>\n<li><strong>Admin UI.<\/strong> Network Admin \u2192 Users \u2192 Teams. Per-user checkboxes on the Edit User screen. Team rows with \"\u2014 Team\" markers on the Users list table.<\/li>\n<li><strong>Cleans up after itself.<\/strong> Deleting a team removes its memberships. Deleting a user drops their memberships. Deleting a site removes its grants from any team that targeted it.<\/li>\n<\/ul>\n\n<h4>How access is granted<\/h4>\n\n<p>Team-derived capabilities are merged in at runtime, never written into the member user's own meta:<\/p>\n\n<ul>\n<li><code>user_has_cap<\/code> \u2014 adds the team role's capabilities to the user's effective caps, plus <code>role-{slug}<\/code> for plugins that check by role.<\/li>\n<li><code>get_blogs_of_user<\/code> \u2014 adds team-linked sites to the user's \"My Sites\" navigation.<\/li>\n<li><code>get_user_metadata<\/code> \u2014 returns an empty-array <code>{prefix}{blog_id}_capabilities<\/code> for team members so <code>is_user_member_of_blog()<\/code> returns true without stamping real capabilities on the user. Workaround for <a href=\"https:\/\/core.trac.wordpress.org\/ticket\/65096\">Core #65096<\/a>.<\/li>\n<\/ul>\n\n<p>The user's actual <code>{prefix}capabilities<\/code> usermeta is never modified. That's what makes team-based access instantly revocable.<\/p>\n\n<h4>Capability requirements<\/h4>\n\n<ul>\n<li>Managing teams (create \/ edit \/ delete) requires <code>manage_network_users<\/code> \u2014 super admin only.<\/li>\n<li>Attaching an existing team to the current site uses <code>promote_users<\/code>, the same cap as inviting an individual user there.<\/li>\n<\/ul>\n\n<p>Team management is restricted to super admins so a compromised single-site admin can't grant themselves access across the network.<\/p>\n\n<h4>Extension filters<\/h4>\n\n<ul>\n<li><code>user_teams_team_caps_for_user( $caps, $user_id, $blog_id )<\/code> \u2014 modify the capability map computed from a user's teams.<\/li>\n<li><code>user_teams_team_applies_to_site( $applies, $team_id, $blog_id, $team )<\/code> \u2014 gate coverage (e.g. pause a team during a freeze).<\/li>\n<li><code>user_teams_team_save_data( $data, $team_id_or_null, $op )<\/code> \u2014 filter sanitised input on create\/update.<\/li>\n<\/ul>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20this%20work%20on%20a%20single-site%20install%3F\"><h3>Does this work on a single-site install?<\/h3><\/dt>\n<dd><p>No. On a single-site install, native roles and capabilities already cover the whole feature set, so the plugin is marked <code>Network: true<\/code> and WordPress won't offer it for activation.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20a%20user%27s%20role%20when%20they%27re%20removed%20from%20a%20team%3F\"><h3>What happens to a user's role when they're removed from a team?<\/h3><\/dt>\n<dd><p>Nothing is persisted on the user, so removing them from the team drops the team-derived capabilities on the next request. The user's own native role on each site is untouched.<\/p><\/dd>\n<dt id=\"where%20are%20teams%20stored%3F\"><h3>Where are teams stored?<\/h3><\/dt>\n<dd><p>Each team is a real <code>wp_users<\/code> row (a \"team account\") marked with <code>user_teams_is_team<\/code> meta. Per-site role grants use native <code>wp_{blog_id}_capabilities<\/code> meta on that row. Memberships are on the member user's own <code>user_teams<\/code> meta. No custom tables.<\/p><\/dd>\n<dt id=\"how%20do%20i%20create%20a%20team%20programmatically%3F\"><h3>How do I create a team programmatically?<\/h3><\/dt>\n<dd><p>use dd32\\WordPress\\UserTeams\\Plugin;<\/p>\n\n<pre><code>$id = Plugin::create_team( 'Meta Team', 'meta-team', 'editor' );\nPlugin::add_team_to_site( $id, 1, 'editor' );\nPlugin::add_team_to_site( $id, 4, 'author' );\nPlugin::add_user_to_team( $user_id, $id );\n<\/code><\/pre>\n\n<p>Checking access uses the normal cap system \u2014 team-derived caps participate via <code>user_has_cap<\/code>:<\/p>\n\n<pre><code>if ( user_can( $user_id, 'edit_others_posts' ) ) {\n    \/\/ ...\n}\n<\/code><\/pre><\/dd>\n<dt id=\"where%27s%20development%20happening%3F\"><h3>Where's development happening?<\/h3><\/dt>\n<dd><p>On GitHub at <a href=\"https:\/\/github.com\/dd32\/wp-user-teams\">dd32\/wp-user-teams<\/a>. That's where the source, issues, pull requests, and release tags live.<\/p><\/dd>\n<dt id=\"i%20have%20a%20bug%20report%20or%20feature%20request\"><h3>I have a bug report or feature request<\/h3><\/dt>\n<dd><p>File it on <a href=\"https:\/\/github.com\/dd32\/wp-user-teams\/issues\">GitHub<\/a>. General support questions belong in the <a href=\"https:\/\/wordpress.org\/support\/plugin\/user-teams\/\">WordPress.org Support Forums<\/a>.<\/p><\/dd>\n\n<\/dl>","raw_excerpt":"Unix-style user groups for WordPress multisite. Create a team, give it a role, drop users in \u2014 access lights up across the network.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/303803","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/users\/148148"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=303803"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=303803"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=303803"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=303803"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=303803"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=303803"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=303803"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}