Permissions and scope
Three different questions, three different declarations. Conflating them is the most common mistake in a first app.
| Question | Declared in | Page |
|---|---|---|
| May this operator see this screen? | A view's permissions in cockpit.json | This page |
| May this buyer do this thing? | provides_permissions in manifest.json | This page, and Person permissions |
| May this app reach that resource? | permissions in manifest.json | App permissions |
Gating a view
Every view kind accepts a permissions array. It gates whether a Cockpit operator sees the view at all.
{
"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:
"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"] }
]
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.
"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 | |
|---|---|
| Format | Dotted <area>.<verb>, lowercase with underscores. |
| Uniqueness | Globally unique across the platform. One definer; a second app defining a registered key is rejected at publish time. |
| Referencing | Other apps reference the key without redefining it. |
title | Rendered in the role editor and in customer-facing copy. Make it a sentence an operator understands. |
capabilities | Which 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:
{
"entities": {
"suppliers": {
"scopeable": ["market"],
"columns": {
"id": { "type": "uuid", "pk": true, "default": "gen_random_uuid()" }
}
}
}
}
| Value | Means |
|---|---|
false (default) | Not scoped. |
true | Scopeable 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:
{ "name": "markets", "label": { "en": "Markets" }, "type": "markets", "width": "160px" }
A markets column on a list view, showing a row's assignment.
{ "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 checklist for one entity
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
- App permissions — the access register, in full.
- Person permissions — the buyer-side ledger, in full.
- Market concept — how market scoping works underneath.
- Roles and permissions — who holds a buyer-side grant.
- Settings — the market header's effect on reads and writes.
- Schema reference — the rest of
schema.json.