---
title: Permissions
description: The permission catalog, staff roles, and how extensions register their own permissions.
---

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 → Staff → Roles** in the dashboard, or the Admin API when you want it scripted.

```bash
spree api post roles --data '{
  "name": "Support",
  "description": "Read-only support",
  "permissions": ["read_orders", "read_customers"]
}'
```

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:

```ruby
# in your engine's initializer
Spree.permissions.register_scope(:reviews, group: :catalog, resources: -> {
  [SpreeReviews::Review]
})
```

- `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.

```ruby
# Staff configure the marketplace's; a seller configures their own.
Spree.permissions.register_scope(:reviews, group: :catalog, audiences: %i[store seller], resources: -> {
  [SpreeReviews::Review]
})
```

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](api.md).

## 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 in `details.required_permission`. 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.

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.
