Feature Groups and Tiers
BuddyNext is plug-and-play: every Layer 2 feature is a self-contained module that the site owner can turn on or off, and a developer can override by filter. This page documents the tier system that governs which features are active, the ~20 feature groups and their tiers, and how toggling a feature removes its routes, templates, options, and admin pages together.


The source of truth is BuddyNext\Core\FeatureRegistry (includes/Core/FeatureRegistry.php), resolved through the features container key.
The three tiers
Section titled “The three tiers”| Tier | Default state | Can the owner disable it? | How it resolves |
|---|---|---|---|
mandatory |
On | No - no toggle, no disable filter | is_enabled() returns true immediately. |
default_on |
On | Yes (Settings -> Features) | Tier default true, overridable by the stored option, then by a per-feature filter. |
opt_in |
Off | Yes - owner must enable it | Tier default false, then the stored option / filter can turn it on. |
FeatureRegistry::is_enabled( $slug ) resolves in this order (first match wins):
- Mandatory tier -> always
true. - Any unmet dependency in
depends_on-> forcedfalse. - For a bridge feature, an absent partner plugin -> forced
false(presence_met()). - Tier default (
default_on= true,opt_in= false), overridden by the storedbuddynext_featuresoption if the owner set it. - The per-feature filter
buddynext_feature_{slug}returns the final boolean.
// Programmatic overrides (the filter wins over the stored option).add_filter( 'buddynext_feature_sidebar', '__return_false' ); // force-disableadd_filter( 'buddynext_feature_webhooks', '__return_true' ); // force-enable
// Resolve a feature's state in code.if ( buddynext_service( 'features' )->is_enabled( 'hashtags' ) ) { // hashtag indexing is active}Mandatory features cannot be persisted off: clean_state() drops them from any saved toggle map. Dependencies cascade - hashtags, reactions, comments, and announcements all depend on feed, and verification depends on auth, so disabling a dependency forces its dependents off too.
The feature groups
Section titled “The feature groups”The registry catalogs 20 features, organized into display groups (core, community, bridges, integrations) for the Settings -> Features UI. Tiers below are read straight from the registry and the audit manifest’s featureGroups.
| Feature slug | Tier | Display group | Depends on | What it covers |
|---|---|---|---|---|
feed |
mandatory | core | - | Posts, comments, reactions, polls, shares - the heart of the community. |
profile |
mandatory | core | - | Per-member profile pages (cover, avatar, bio, custom fields). |
social_graph |
mandatory | core | - | Follows, connections, blocks - the relationships layer. |
notifications |
mandatory | core | - | In-app notifications for follows, reactions, comments, mentions, moderation. |
auth |
mandatory | core | - | Custom login + registration pages and the email-verification handshake. |
search |
mandatory | core | - | Unified FULLTEXT index across posts, users, spaces, hashtags. |
moderation |
mandatory | core | - | Reports, strikes, suspensions, appeals - the integrity layer. |
spaces |
mandatory | community | - | Topic-scoped sub-communities with their own posts, members, settings. Always on: member, profile and URL paths resolve spaces, so it cannot be toggled off. |
hashtags |
default_on | community | feed | Extract #tags, build trending lists, per-tag feeds. |
reactions |
default_on | community | feed | Emoji reactions on posts and comments. |
comments |
default_on | community | feed | Threaded comments on posts. |
sidebar |
default_on | community | - | Right-column hub widgets (trending, suggested people, your spaces). |
onboarding |
default_on | community | - | Multi-step welcome flow for new members. |
verification |
default_on | community | auth | Send a verification link on registration; gate actions on verified status. |
announcements |
default_on | community | feed | Pin an announcement to the top of every member’s feed. |
gamification |
default_on* | bridges | - | Bridges WBGamification (points, badges, leaderboard). |
jetonomy |
default_on* | bridges | feed | Surfaces Jetonomy forum activity in BuddyNext feeds. |
wpmediaverse |
default_on* | bridges | - | Bridges WPMediaVerse for direct messages. |
career_board |
default_on* | bridges | feed | Surfaces Career Board job posts as activity. |
webhooks |
opt_in | integrations | - | Outbound signed HTTPS POSTs to external endpoints on community events. |
* Bridge features are default_on in the registry so an installed integration works out of the box, but presence_met() forces them off whenever the partner plugin is absent (WPMediaVerse\Core\Plugin, Jetonomy\Jetonomy, wb_gam_submit_event(), WCB_VERSION). The Settings -> Features UI renders such a toggle disabled with a “Requires X” notice. The audit manifest’s featureGroups lists these four plus webhooks as the opt_in / bridge set; the registry is the canonical tier source.
Note: The manifest groups routes/templates/options by feature, while the registry groups features for the admin UI. The
social_graphregistry feature has nofeatureGroupsroutes of its own in the manifest because its endpoints are filed under the feature surfaces that consume them (feed, profile). Trust the registry for tiers and the manifest for the route/template/option inventory.
What a feature group binds
Section titled “What a feature group binds”Each feature group ties together four kinds of surface. The audit manifest (features.featureGroups) records exactly which routes, templates, options, and admin pages belong to each group. Examples from the current manifest:
- Routes - REST endpoints under
buddynext/v1.feedowns 11 (/feed/home,/feed/explore,/feed/announcements/{id}/dismiss, …);spacesowns 28;webhooksowns 5. Note:REST/Routerregisters its controllers unconditionally; onlywebhooksis wrapped in anis_enabled()check. A toggleable feature is disabled at the UI + hub layer (nav hidden, hub route redirected - see below), not by unregistering its REST controller, so its endpoints still answer for a direct API caller. Enforce a disabled feature’s access rule in the controller’s permission callback, never by assuming the route is absent. - Templates - the hub templates and partials the feature renders.
spacesships 28 templates,profile24,sidebar12. A disabled feature’s templates are never reached because the route or theContainer::has()guard short-circuits first. - Options - the settings the feature persists. Examples:
spaces->buddynext_space_creation_role,buddynext_space_max_sub_spaces,buddynext_notif_default_space_join;reactions->buddynext_enabled_reactions;hashtags->buddynext_banned_hashtags;comments->buddynext_notif_default_comment;webhooks->buddynext_webhook_secret;feed/jetonomy->buddynext_jetonomy_feed_sync. - Admin pages - the dedicated wp-admin screens. Only
spacesregisters one in the manifest (buddynext-spaces); the other features expose their settings inside the shared BuddyNext settings tabs rather than a standalone page.
The wiring lives in Plugin::register_services() and Plugin::init(). A feature binds its services only when the registry reports it enabled, and its listener is wired only when the binding exists:
// In register_services(): bind the Service + Cache only when enabled.if ( $features->is_enabled( 'sidebar' ) ) { $container->bind( 'sidebar_cache', fn() => new \BuddyNext\Sidebar\WidgetCache() ); $container->bind( 'sidebar_widgets', fn( $c ) => new \BuddyNext\Sidebar\WidgetService( $c->get( 'sidebar_cache' ) ) );}
// In init(): wire the listener only when the binding exists.if ( $container->has( 'sidebar_widgets' ) ) { ( new \BuddyNext\Sidebar\WidgetListener( $container->get( 'sidebar_cache' ) ) )->register();}How toggling a feature removes its UI and REST surface
Section titled “How toggling a feature removes its UI and REST surface”Turning a toggleable feature off removes it from the member’s path - the nav links and the hub route - so a member never lands on a half-disabled page. BuddyNext enforces this at these points:
- Service + listener may not bind. A feature whose Service/Cache is registered behind an
is_enabled()guard inregister_services()(e.g.sidebar,webhooks) leaves its container keys unregistered when off, so nothing downstream resolves them. Not every feature is wired this way - many controllers are plain and always constructed. - REST controllers mostly register unconditionally.
REST/Router::register_routes()constructs every controller regardless of feature state; onlywebhooksis gated byis_enabled('webhooks'). So a disabled feature’s endpoints usually still respond to a direct API caller - the toggle is a UI/hub control, not a route kill-switch. A route that must be closed when its feature is off enforces that in its own permission callback. - Hub routes redirect.
PageRouter::dispatch_hub_template()re-checks the registry for the toggleable hubs and bounces visitors away from a disabled surface. For example, when a toggleable hub is off, its/hub/path redirects to the activity hub; the same guard protectsonboarding, thehashtagsper-tag feed, and (viaMessagesData::entry_enabled()) thewpmediaverse-backed messages hub. (spacesis mandatory, so it is never in this set.)
Templates that optionally use a feature follow the plug-and-play degradation pattern - check Container::has(), and fall back to an empty result when the feature is absent:
$widgets = function_exists( 'buddynext_service' ) && \BuddyNext\Core\Container::instance()->has( 'sidebar_widgets' ) ? buddynext_service( 'sidebar_widgets' ) : null;
$trending = ( null !== $widgets ) ? $widgets->trending_hashtags( 5 ) : array();The minimal-mode contract is the floor: with every default_on and opt_in feature disabled, Core plus the mandatory features must still deliver a working community - login/register, posts, direct follow, basic notifications, and search. If disabling a feature breaks any of those, that feature was misplaced in Layer 2 and belongs in Core.
Extending the catalog
Section titled “Extending the catalog”Third-party plugins register their own features under the same contract via the buddynext_features filter. Each entry uses the registry’s shape (slug, tier, group, depends_on, and a translatable label/description supplied at the same time), after which it participates in the Settings -> Features UI, the buddynext_feature_{slug} override filter, and the same enable/disable resolution as the built-in features.
- The Pro plugin (
BuddyNextPro) layers its own modules on top of these free features - Membership, AI, Realtime, Push, Analytics, White-label, and enhancements to Reactions/Feed/Moderation/Members/Profile/Search. Pro modules are not part of the freeFeatureRegistrycatalog; they boot atplugins_loaded:20and extend free through the documented hooks and container rebinding rather than appearing as free feature toggles. - Read tiers from
FeatureRegistry::catalog()- it is the only inventory. Free ships noaudit/manifest.json; an earlier revision of this page pointed atfeatures.featureGroupsin that file, and it does not exist.

