Skip to main content
Examples here import from @spree/dashboard, which re-exports the framework and the design system for host applications. A distributed plugin imports @spree/dashboard-core and @spree/dashboard-ui directly instead — it extends the shell rather than shipping it. See plugin overview.
The dashboard exposes the current admin’s permissions to every component via the usePermissions() hook: the catalog keys their role holds on the current store (read_orders, write_products, …) and the class-level ability rules the server compiles from those keys. The registry surfaces — nav entries, settings entries, custom routes — also accept a subject shortcut, and nav entries take a generic if predicate that reads from permissions.

Two layers

UI gating is for UX. Backend authorization is for security. They are not the same: Always rely on the backend for security. Hiding a button is a hint to the user, not a wall. A user with browser dev tools (or the API token) can always hit the endpoint directly — the API’s permission check is what stops them.

The permissions object

It mirrors the backend ability at the class level:
Subjects are short names: the model’s class name without its namespace, underscored ('order', 'product', or 'report' for your own MyApp::Report). Use the Subject constants from @spree/dashboard-core for Spree’s own models. There is no client-side record-level check — when a rule is conditional on record attributes, isConditional returns true and the API is the arbiter: render the control and handle a possible 403.

subject shortcut

Available on NavEntry, SettingsNavEntry, and RouteEntry. It hides the entry unless the user can read the subject:
For routes, subject also renders a 403 page if the user navigates directly:
Use subject whenever you want a “user can read this resource” check. For anything else, use if.

if predicate (nav entries)

if on a nav entry is the escape hatch. It runs at render with { permissions, store, user } and returns a boolean:
(The if context types store loosely — cast to the SDK’s Store for typed access, the same way the built-in Getting Started entry does.) Use it for combined checks (permission + store state), feature flags, multi-action checks (can(update) && can(read)), or anything the subject shortcut can’t express in one string. It combines with subject — both must pass. (Slot entries have an if too, but it receives only the slot’s own context — see Slots.)

Inside components

Use the usePermissions() hook — it returns { permissions, rules, permissionKeys, isLoading, refresh }. permissionKeys is the flat key list (the same vocabulary as the role editor and API-key scopes), for when permissionKeys.includes('read_orders') reads better than a subject check:
The same permissions object backs the nav registry’s if predicate, so you can move logic between the two without changing behaviour. For declarative gating, <Can I="update" a="order">…</Can> (also from @spree/dashboard-core) renders children only when the check passes.

Custom permissions

If your customization introduces a new model on the backend, register it as a permission catalog scope there (Spree.permissions.register_scope in your engine’s initializer). Its keys then appear in the role editor and the API-key scope picker, and any role granted them makes permissions.can('read', 'report') resolve in the dashboard exactly like a first-party check — the abilities ship with the current-user response (GET /api/v3/admin/me) at sign-in, alongside permission_keys, the flat key list the role editor grants from.

Reference