MenuProviderExtension
MenuProviderExtension is the plugin-facing way to add menu items to the platform's navigation at runtime. Unlike the static menus.json manifest contribution, this SPI fires on every menu-build request and receives live user and tenant context — enabling role-gated navigation, feature-flag-driven menu trees, and tenant-scoped menu customization without touching the plugin manifest.
This is the correct contract for any menu entry that depends on who the current user is, what tenant is active, or what runtime settings are in play. If the menu entry is always visible to everyone and never changes, menus.json is simpler; if visibility depends on context, use this SPI.
Concept
The platform's menu-build pipeline collects contributions from two sources: static menus.json entries declared in the plugin manifest, and dynamic providers registered as MenuProviderExtension implementations. Providers are discovered via pf4j at plugin load time and invoked after static entries are resolved. The platform calls isActive(context) first — if it returns false, getMenuItems(context) is never called and the provider contributes nothing for that request.
MenuContext carries the full identity of the current request: tenantId, userId, userRoles, userPermissions, the originating pluginId, its namespace, and a settings map for runtime flags. Two convenience methods — hasRole(String) and hasPermission(String) — let providers gate items inline without manual list iteration.
MenuItem is the unit of contribution. Items nest via children, carry their own requiredRoles and requiredPermissions declarations (enforced by the platform renderer), and support target for external links. The metadata map is forwarded to the frontend renderer for extension-specific decoration.
Interface signature
package com.auraboot.framework.plugin.extension;
public interface MenuProviderExtension extends ExtensionPoint {
/**
* The menu group this provider contributes to.
* Common groups: "main-sidebar", "header-menu", "user-menu", "settings-menu"
*/
String getMenuGroup();
/**
* Returns the items to add for the given context.
*/
List<MenuItem> getMenuItems(MenuContext context);
/**
* Guards the provider. Return false to contribute nothing.
* Default: true (always active).
*/
default boolean isActive(MenuContext context) {
return true;
}
/**
* Ordering within the group. Lower values appear first.
* Default: 100.
*/
default int getOrder() {
return 100;
}
record MenuContext(
Long tenantId,
String pluginId,
String namespace,
Long userId,
List<String> userRoles,
List<String> userPermissions,
Map<String, Object> settings
) {
public boolean hasRole(String role) { ... }
public boolean hasPermission(String permission) { ... }
}
record MenuItem(
String key,
String label,
String icon,
String path,
String target, // default "_self"
int order, // default 100
List<MenuItem> children,
List<String> requiredRoles,
List<String> requiredPermissions,
Map<String, Object> metadata
) {}
}Implementing a provider
@Extension
public class BillingMenuProvider implements MenuProviderExtension {
@Override
public String getMenuGroup() {
return "main-sidebar";
}
@Override
public boolean isActive(MenuContext context) {
// Only show when the current user holds the billing.invoice.view permission
return context.hasPermission("billing.invoice.view");
}
@Override
public List<MenuItem> getMenuItems(MenuContext context) {
return List.of(
MenuItem.builder()
.key("billing-invoices")
.label("Invoices")
.icon("receipt")
.path("/billing/invoices")
.order(100)
.requiredPermissions(List.of("billing.invoice.view"))
.build(),
MenuItem.builder()
.key("billing-payments")
.label("Payments")
.icon("credit-card")
.path("/billing/payments")
.order(110)
.requiredPermissions(List.of("billing.payment.view"))
.build()
);
}
@Override
public int getOrder() {
return 50; // appear before default providers
}
}Three things to notice:
@Extensionis required. Without it pf4j will not discover the implementation and the provider is silently absent.isActiveis a guard, not a filter. Use it to skip the entire provider cheaply. UserequiredPermissionson individualMenuIteminstances when only some items within a group need gating.labelshould be an i18n key or aLocalizedText-resolved string. Hardcoding display text violates the platform's i18n contract (see Plugin Manifest).
Use cases and rules
| Scenario | Recommended approach |
|---|---|
| Menu visible only to users with a specific permission | isActive checks context.hasPermission(...) |
| Nested submenu under an existing group entry | Set children on the parent MenuItem |
| External link that opens in a new tab | Set target = "_blank" on the item |
Ordering relative to static menus.json entries | Use getOrder() — lower wins across both static and dynamic entries |
| Menu varies by tenant (e.g. different modules licensed) | Read context.tenantId or tenant-scoped flags from context.settings() |
| Provider should never run in a specific namespace | Guard on context.namespace() inside isActive |
Error semantics
getMenuItems and isActive are called synchronously during the request-scoped menu build. The platform wraps each provider invocation in a catch block — an unhandled exception from your provider will suppress that provider's contribution for the current request and log a warning; it will not fail the page load. This means silent failures are possible if an exception is swallowed.
Do not throw checked exceptions from getMenuItems. If a required dependency is unavailable, return an empty list and log the reason. Do not attempt to load data from a database or make remote calls inside getMenuItems — the method must be fast and side-effect-free.
Audit and correlation
Menu provider invocations are not individually traced in the platform audit log, because they carry no side effects. If your provider uses context.settings() to read a feature flag backed by a platform service, any state reads that service performs are audited under that service's own trail.
If you need to trace which provider contributed which item (e.g. for debugging a missing entry), set a metadata key on the MenuItem with a stable provider identifier. The platform forwards metadata to the frontend renderer and it appears in the React DevTools component tree.
Common pitfalls
| Pattern | Why it breaks |
|---|---|
Omitting @Extension | pf4j does not discover the class; menu items are silently absent |
Making I/O calls inside getMenuItems | Blocks the menu-build request path; use isActive to skip early and cache upstream data in plugin startup |
Hardcoding display text in label | Violates i18n contract; text appears as raw string in non-default locales |
Returning null from getMenuItems | Platform expects a non-null List; return List.of() instead |
Duplicating a key already present in menus.json | Results in duplicate menu entries; use namespaced keys like billing-invoices not invoices |
Checking permissions inside getMenuItems but not isActive | Slightly inefficient; use isActive for whole-provider guards to avoid building item lists that are discarded |
Relationship to other SPIs
- Plugin Manifest —
menus.jsonstatic contribution vs. dynamic via this SPI; prefer static for unconditional entries. - Permissions — Menu visibility is permission-gated at two layers:
isActive/requiredPermissionsin the provider, and the platform's permission enforcement in the renderer. Both must align with registered permission codes. CommandHandlerExtension— Menu items often invoke commands; thepathon an item typically routes to a page that surfaces a command palette.
Enterprise note — Per-tenant menu entitlement gating (exposing menu groups only to tenants on specific subscription tiers), role-based dynamic menu trees driven by organization hierarchy, and A/B testing of menu variants with cohort assignment are commercial-only capabilities built on top of this SPI. The open-source
MenuProviderExtensioninterface is identical across editions.
Next steps
- Plugin Manifest — declare static menu entries and understand when dynamic providers are the right choice
- Permissions — register permission codes that back your
requiredPermissionsdeclarations CommandHandlerExtension— implement the commands that your menu items navigate to