Permissions and scope

The three separate permission questions a Commerce Studio app touches — gating an operator view, publishing your own buyer-side grants, and making an entity market-scopeable.

Three different questions, three different declarations. Conflating them is the most common mistake in a first app.

QuestionDeclared inPage
May this operator see this screen?A view's permissions in cockpit.jsonThis page
May this buyer do this thing?provides_permissions in manifest.jsonThis page, and Person permissions
May this app reach that resource?permissions in manifest.jsonApp permissions

Gating a view

Every view kind accepts a permissions array. It gates whether a Cockpit operator sees the view at all.

cockpit.json
{
  "route": "/suppliers",
  "type": "list",
  "entity": "suppliers",
  "columns": [],
  "permissions": ["suppliers.read"]
}

The convention across every shipped commerce app is <entity>.read for a view that only reads and <entity>.write for one that writes:

cockpit.json
"views": [
  { "route": "/suppliers",          "type": "list",   "permissions": ["suppliers.read"] },
  { "route": "/suppliers/:id",      "type": "detail", "permissions": ["suppliers.read"] },
  { "route": "/suppliers/new",      "type": "form",   "permissions": ["suppliers.write"] },
  { "route": "/suppliers/:id/edit", "type": "form",   "permissions": ["suppliers.write"] }
]
This is a UI gate on the operator, not a grant to your app. It decides whether a screen appears. It does not give your app access to anything, and it is not what stops a request at the gateway.What your app may reach is manifest.permissions — see App permissions. An app gets no access by default; every grant is declared.

Follow the convention. An operator's role is configured against these keys, and a manifest that invents its own spelling makes a role editor unusable.

Publishing your own buyer-side grants

If your app enforces something against the acting buyer — the person a storefront request is made on behalf of — declare the grant so a merchant can hand it to a role.

manifest.json
"provides_permissions": [
  {
    "permission": "suppliers.read",
    "title": { "en": "See suppliers", "de": "Lieferanten ansehen" },
    "description": { "en": "See the supplier list and their agreed prices." }
  },
  {
    "permission": "suppliers.manage",
    "title": { "en": "Manage suppliers" },
    "description": { "en": "Create, edit and retire suppliers." },
    "capabilities": ["suppliers.create", "suppliers.update"]
  }
]
Rule
FormatDotted <area>.<verb>, lowercase with underscores.
UniquenessGlobally unique across the platform. One definer; a second app defining a registered key is rejected at publish time.
ReferencingOther apps reference the key without redefining it.
titleRendered in the role editor and in customer-facing copy. Make it a sentence an operator understands.
capabilitiesWhich of your capabilities the grant gates.

The gateway resolves the acting principal once per request and enforces the permission on the capabilities you list, so you do not check it in your own code.

The role provider decides who holds what. For buyers, that is the customers app: a merchant maps your grant onto a role with PUT /v1/customers/roles/{key}/permissions, and a key for an app that is not installed simply has nothing to act on. See Roles and permissions.

Two of the platform's commerce apps publish grants today — customers (contacts.read, contacts.manage, organization.manage, addresses.manage) and orders (orders.read, orders.create, orders.request, orders.approve). Look at those as models.

Market-scopeable entities

A row can belong to one market or be global. That is opt-in per entity, declared in schema.json:

schema.json
{
  "entities": {
    "suppliers": {
      "scopeable": ["market"],
      "columns": {
        "id": { "type": "uuid", "pk": true, "default": "gen_random_uuid()" }
      }
    }
  }
}
ValueMeans
false (default)Not scoped.
trueScopeable by every registered dimension.
["market"]Scopeable by exactly that dimension — what every commerce app declares.

It requires a uuid id column. There is no market_id column on your table — the assignment lives in the platform's scope registry, which is why a market-assignment surface has its own save.

Twelve of the thirteen commerce apps declare scopeable: ["market"] on their entities. The markets app itself does not, for the obvious reason.

What that gives you in the UI

Two things become available once an entity is scopeable:

cockpit.json
{ "name": "markets", "label": { "en": "Markets" }, "type": "markets", "width": "160px" }

A markets column on a list view, showing a row's assignment.

cockpit.json
{ "title": { "en": "Markets" }, "variant": "market-assignment" }

A market-assignment section on a detail view. It holds no field of the record and renders nothing on an entity that is not scopeable — so if the section is blank, check the schema.

There is also a market-assignment field type for a form.

And what the header does

X-Revenexx-Market on a write assigns the new row to that market. Without the header the row is unassigned — and unassigned means visible in every market, not hidden.

On most reads the header is a no-op. Both halves of that are covered in Settings.

A market is never a security boundary. The tenant is fixed by the credential. Do not model access control as market scope.

A checklist for one entity

text
schema.json          scopeable: ["market"] if the row can be market-specific
manifest.json        permissions[]           what your app may reach
manifest.json        provides_permissions[]  buyer-side grants you enforce, if any
cockpit.json         permissions: ["x.read"]  on the list and detail views
cockpit.json         permissions: ["x.write"] on the create and edit forms
cockpit.json         a `markets` column and a `market-assignment` section, if scopeable

Where to go next

Was this page helpful?