Skip to main content
Staff roles and their permissions are managed as data — in the dashboard under Settings → Roles, through the Admin API (/api/v3/admin/roles), or in seeds. There is nothing to configure in Ruby for day-to-day permission management. This page covers what extension and host-application developers can plug into.

The permission catalog

Every grantable capability is a flat key of the form read_<resource> or write_<resource> — the same vocabulary secret API keys use as scopes. write_* implies the matching read_*. Discover the catalog at runtime with GET /api/v3/admin/permissions; the role editor and the API-key scope picker are both rendered from it. Keys are per resource, not per action. The money-adjacent areas (payments, refunds, gift_cards, store_credits) are separate resources from orders, so “view orders but don’t refund” needs no special casing. Roles are data, so create them where you manage the rest of your store’s data: Settings → Roles in the dashboard, or the Admin API when you want it scripted.
A role marked immutable renders read-only in the dashboard, for roles the application does not want store staff editing. The admin role is always protected.

Registering extension permissions

Register your models as a catalog scope once; the keys appear in the role editor and become mintable API-key scopes with no further wiring:
  • group places the row in the permission pickers (:analytics, :orders, :catalog, :customers, :sellers, :loyalty, :marketing, :settings, :access — or your own). Rows render in registration order, so a scope registered from an initializer lands after core’s.
  • resources is a lambda returning the CanCanCan subjects the keys grant; it resolves lazily, so load order does not matter.
  • Pass write: false for read-only scopes.
Localize the labels in your engine’s locale file under spree.permissions_catalog.resources.<name> (label and description), and declare the same scope on your admin controllers with scoped_resource :reviews.

Who may hold a key

audiences is the complete list of audiences whose roles may hold a scope’s keys. The store’s own back office is :store, and is the list when a registration names none — so the example above is grantable to staff and nobody else.
Naming another audience exposes the scope to a principal outside the store’s own staff, so it is always a deliberate act. Two consequences worth knowing:
  • Leaving :store out keeps the scope off the staff permission picker, out of read_all / write_all, and unmintable on a secret API key — which is how a seller panel’s own scopes (seller_profile, seller_earnings) exist without a store role ever being offered them.
  • read_only_for: %i[seller] gives that audience the read key alone, for a vocabulary they consult but do not own.
A key never decides which rows its holder reaches. That is the controller’s job, through scope fetching — a seller holding write_products manages their own products because the seller API reads through current_seller, not because the key says so.

What the catalog does not cover

The catalog is deliberately flat: it answers “may this role touch this kind of record”, and that is the whole grant vocabulary. Record-state rules — “a completed order cannot be deleted” — belong on models and workflows, not on roles. Put them there and they bind every caller, including secret API keys, which never consult the role system at all. A rule enforced only in the permission layer is a rule the API can walk straight past. Row-level restrictions, such as a support agent limited to one market, are not expressible as permission keys. Scope them where the records are read: a controller reaching through current_store.orders rather than Spree::Order already restricts by tenant, and the same technique narrows by market or seller. See extending the API.

How enforcement works

Every Admin API controller declares its resource (scoped_resource :orders). Each request checks the principal’s keys — a staff member’s role permissions on the current store, or a secret key’s scopes — against read_<resource> or write_<resource> for the action. A missing key produces a 403 naming it — details.required_permission for staff, details.required_scope for a secret key. Behind that gate, keys compile to CanCanCan rules for record-level concerns, and GET /api/v3/admin/me returns both the rule dump the dashboard mirrors and permission_keys, the flat key list. Those rules come from Spree::Ability. When an application needs CanCanCan rules the catalog cannot express, replace the class with Spree::Dependencies.ability_class = 'MyApp::Ability' — subclass Spree::Ability, keep its initialize(user, options = {}) signature (options[:store], and options[:resource] for a seller panel), and add can / cannot rules after super. The Admin and Seller APIs build every staff ability from it; secret API keys use Spree::ApiKeyAbility and never consult it. Storefront customers are not part of this system — customer authorization is ownership, enforced by the Store API’s scoped lookups, with nothing to configure. The one swappable piece is Spree::Storefront::AccessPolicy (via Spree::Dependencies.storefront_access_policy_class): a generic readable? / writable? / scope protocol that defaults to “the caller owns the record”, with carts and orders adding guest-token access. Replace it only when access must widen beyond the owner, such as company accounts sharing purchases or wishlists.