/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 formread_<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.
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:groupplaces 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.resourcesis a lambda returning the CanCanCan subjects the keys grant; it resolves lazily, so load order does not matter.- Pass
write: falsefor read-only scopes.
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.
- Leaving
:storeout keeps the scope off the staff permission picker, out ofread_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.
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 throughcurrent_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.
