nav) and settings sub-shell (settingsNav). Both are module singletons exposed from @spree/dashboard-core and consumed by <AppSidebar> and <SettingsSidebar> via useSyncExternalStore, so late registration re-renders the sidebars automatically.
Main sidebar (nav)
Add an entry
position controls ordering — built-ins use 100–600 (Home 100, Orders 200, Products 300, Customers 400, Promotions 500, Reports 600; a conditional Getting Started entry sits at 50), leaving gaps to slot into. The default is 100. Keys must be unique, including against the built-in keys (getting-started, home, orders, products, customers, promotions, reports, settings) — duplicates throw at boot.
Add via defineDashboardPlugin
The facade groups multiple registry calls:
Nest children under a parent
subject independently — the parent doesn’t auto-gate them.
Nest under an existing (built-in) parent
To add an item inside a menu you don’t own — e.g. a “Brands” page under the built-in Products menu — useaddChild, which preserves the parent’s existing children. (nav.update('products', { children: [...] }) would replace them, dropping the built-ins.)
updateChild / removeChild mutate an existing one). Built-in parents (products, orders, customers, promotions) register during app bootstrap, before any plugin runs, so they’re always available to nest into.
Insert relative to an existing entry
position ± 1 unless you specify your own.
Modify or remove
Imperatively:defineDashboardPlugin — the nav object form adds, removes, and patches in one config (built-in entries included; they register before any plugin runs):
nav: [...]) remains the shorthand for { add: [...] }. The object form also takes addChildren (see Nest under an existing parent). remove of an unknown key is a no-op; update of an unknown key throws. settingsNav accepts the same object form (minus addChildren — settings entries are flat).
Bottom-pinned entries
section: 'bottom' pins to the sidebar footer (where “Settings” lives by default):
Conditional visibility and badges
Beyond permission gating withsubject, a top-level entry can hide itself based on app state (if) and render a component after its label (badge):
if combines with subject — both must pass.
Settings sub-nav (settingsNav)
The settings page has its own sub-shell sidebar with grouped entries (Store, Localization, Team & Access, …). Entries cluster under groups defined separately.
Register a group
Add entries to a group
comingSoon badge
Disabled entries with a “Soon” badge — useful for staging the rollout:
Permission gating
subject on a nav entry checks permissions.can('read', subject) — when it fails, the sidebar item is hidden. This is UX, not authorization. The backend still enforces CanCanCan via authorize! on every API call. Hiding the link is a hint, not a security boundary.
Order of operations
Nav registrations run at module-load time. The sidebar reads viauseSyncExternalStore, so:
- Registering before the sidebar mounts: works (the snapshot picks it up on first render)
- Registering after the sidebar mounts: also works (the store notifies subscribers)
i18n.t('admin.foo.label') for label, your translation bundle must be registered first. The framework’s own translations load before src/plugins.ts runs, so the rule reduces to: put your i18n.addResourceBundle(...) call at the top of plugins.ts, above the registrations that use it. See Translations.
Reference
NavEntry— the full shape including types forchildren,position,subject,sectionSettingsNavEntry— settings sub-nav types

