Skip to content
v0.3

Roles

The enumerable half of docs/conventions/roles-and-powers.md, generated from the two sources that own the truth: role → scopes from the RBAC migrations (packages/db/migrations/tenant/), scope → power from the kernel command registry. Editing this page by hand is undone by pnpm codegen and breaks CI.

What it deliberately does NOT carry is the prose — what each role is for, the three axes of authority, the platform operator, the reasoning behind a boundary. That is hand-written, and lives in docs/conventions/roles-and-powers.md.

Seeded into every tenant and immutable (role.update/role.archive refuse them) — to customize, clone with role.create. An operator holds one role; it materializes into the scopes of the credential minted at login.

Key Name Scopes Description
owner Owner 34 Full control, including managing operators and roles, and reading every area of the admin. There is always at least one active Owner.
admin Admin 30 Full control of the store, including reading every area of the admin, but cannot manage operators or roles.
catalog Catálogo 18 Manage the catalog: products, SKUs, categories, content, composition, search and media. Reads every area of the admin — logistics and stock, promotions, apps, settings and the activity trail included — a power a clone of this role can now be built without.
marketing Marketing 14 Shape the storefront and the offer, and follow the orders that come out of it (without access to the buyer’s personal data): promotions, composition, content, search and media. Reads every area of the admin — a power a clone of this role can now be built without.
support Atendimento 12 Serve customers: manage orders and customers (with PII access). Reads every area of the admin — catalog, logistics and stock, promotions, apps and settings — a power a clone of this role can now be built without.

⚠️ This section used to be the other half of the truth; it is now a footnote, and that is the change. A read that needs no scope appears in no column of the matrix below, so for as long as the floor was 43 reads deep every role’s row understated what that role reached and no chip could ever show the difference. The floor is now 1: whoami. The matrix is the answer, not half of it.

This is not a gap in the gate, and since IAM-READ-SCOPES it is not a gap at all. null in that file no longer means “we found no scope for this” — it means nobody may be excluded from this read, and the reason is on its own line. This floor used to hold 43 reads; the other 42 now carry one of seven AREA scopes (catalog.admin.read, content.read, logistics.read, promotion.read, extensions.read, tenant.settings.read, audit.read) and appear in the matrix below as ordinary rows.

⚠️ That was lesser privilege and tidying, NOT protection. Nothing was leaking. The rule those reads were classified under — written after a measured incident: the scope follows what the data is, not the folder it lives in — still stands and was never repealed: a carrier is still a company name and the shape of a tracking URL, and it is still not gated on a catalogue WRITE scope. What was added is a second reading — subject: a read with no personal data still belongs to an area of the business, and an area can be granted. “A stock-read-only key” was simply not expressable before; now it is.

⚠️ Before you revoke one: these areas are read from OUTSIDE their own area. The promotion editor reads the catalogue, the order sheet reads stock_levels and routing_suggestion, and half a dozen screens read stores. Cutting logistics.read from a role that edits promotions WILL break that editor. That is an informed choice by whoever cuts it, not a defect to report — the seven are the granularity of REVOCATION, not a default-deny, which is also why the migration that introduced them granted all seven to every role that already existed. Nothing changed on anybody’s screen the day they landed; what changed is that there is now something to take away.

And what is left on the floor holds no customer personal data. The reads that carry a shopper’s identity — customers, orders, clusters — are behind their own scopes and never were on this floor; pii.read governs what is legible inside THOSE, not inside this.

⚠️ The activity trail — who-did-what among the staff — has LEFT this floor. It was the one judgement call in the kernel’s table, left open because the admin’s own navigation already hands /userlog and /changelog to every operator, so gating it would have silently removed two screens from three roles. That reason is now spent rather than overruled: audit and promotion_activity both carry audit.read (one name, because they are the same question asked twice), and the migration that introduced it granted it to every existing role — so nothing was removed from anybody. What changed is WHO DECIDES: a store that does not want its whole staff reading who did what now revokes audit.read, instead of asking us to change a line.

Scope owner admin catalog marketing support
admin.users.write
audit.read
catalog.admin.read
catalog.category.write
catalog.product.publish
catalog.product.write
catalog.sku.write
composition.write
content.read
content.write
custom_fields.write
customer.admin.write
extensions.egress.consent
extensions.read
extensions.write
iam.roles.write
inventory.adjust
inventory.reserve
logistics.read
media.write
notification.send
operator.self.write
order.payment.write
order.read
order.write
pii.read
promotion.read
promotion.write
purchase.on_behalf
search.write
shipping.write
tenant.settings.read
tenant.settings.write
tenant.store.write

The owner bundle has two sources, and the one that governs a live session is not the migration. What the kernel mints at login is derived from the command registry by deriveTenantOperatorScopes (packages/core/src/iam/owner-scopes.ts): every tenant-operator scope in the registry, plus OPERATOR_GATE_SCOPES — so a new tenant-operator command reaches the Owner with no migration and nobody having to remember. The row seeded in the migrations is the mirror that keeps the role table truthful for whatever reads it (the admin’s roles screen, an operator inspecting the schema).

Both were read when this page was generated, and compared:

They agree. The 34 scopes on the migration row are exactly the 34 the derivation produces, so the matrix above is safe to read for owner.

One row per scope above: the commands it lets its holder run. Scopes carried by no command are the privileged gates — the kernel consults them directly instead of guarding a command with them.

Scope Commands
admin.users.write admin_user.disable, admin_user.enable, admin_user.invite, iam.api_key.create, iam.api_key.revoke, iam.api_key.rotate, operator.access_key.create, operator.access_key.revoke
audit.read no command — a privileged gate, consulted by the kernel (see below)
catalog.admin.read no command — a privileged gate, consulted by the kernel (see below)
catalog.category.write catalog.category.create, catalog.category.move, catalog.category.set_custom_fields, catalog.category.update
catalog.product.publish catalog.product.publish, catalog.product.publish_bulk, catalog.product.unpublish, catalog.product.unpublish_bulk
catalog.product.write catalog.brand.archive, catalog.brand.create, catalog.brand.update, catalog.collection.archive, catalog.collection.create, catalog.collection.pin, catalog.collection.reorder, catalog.collection.unarchive, catalog.collection.unpin, catalog.collection.update, catalog.media.attach, catalog.media.detach, catalog.media.reorder, catalog.media.update, catalog.option.create, catalog.option.delete, catalog.option.update, catalog.option_value.create, catalog.option_value.update, catalog.product.archive, catalog.product.categorize, catalog.product.create, catalog.product.uncategorize, catalog.product.update
catalog.sku.write catalog.sku.create, catalog.sku.delete, catalog.sku.update
composition.write composition.move, composition.place, composition.placement.remove, composition.placement.reorder, composition.placement.set, composition.remove, composition.reorder, composition.toggle, composition.update_config
content.read no command — a privileged gate, consulted by the kernel (see below)
content.write content.page.archive, content.page.create, content.page.update
custom_fields.write custom_field.archive, custom_field.define, custom_field.update
customer.admin.write customer.anonymize, customer.set_custom_fields, customer_cluster.archive, customer_cluster.create, customer_cluster.pin, customer_cluster.unarchive, customer_cluster.unpin, customer_cluster.update
extensions.egress.consent extension.connection.set
extensions.read no command — a privileged gate, consulted by the kernel (see below)
extensions.write extension.config.set, extension.install, extension.uninstall, notification.channel.register
iam.roles.write admin_user.set_role, role.archive, role.create, role.update
inventory.adjust inventory.adjust, inventory.set_level, warehouse.create, warehouse.update
inventory.reserve inventory.commit, inventory.reclaim_expired, inventory.release, inventory.reserve
logistics.read no command — a privileged gate, consulted by the kernel (see below)
media.write asset.archive, asset.create, media.request_upload
notification.send notification.send
operator.self.write admin_user.set_locale
order.payment.write order.payment.record
order.read no command — a privileged gate, consulted by the kernel (see below)
order.write notification.resend, order.cancel, order.cancel_request.approve, order.cancel_request.decline, order.comment.add, order.create, order.document.attach, order.hold, order.mark_delivered, order.mark_paid, order.mark_preparing, order.mark_shipped, order.refund, order.release_hold, order.shipment.create, order.shipment.mark_delivered, order.shipment.mark_picked_up, order.shipment.mark_ready_for_pickup, order.shipment.mark_shipped, order.shipment.set_tracking, order.shipment.update_tracking_status
pii.read no command — a privileged gate, consulted by the kernel (see below)
promotion.read no command — a privileged gate, consulted by the kernel (see below)
promotion.write promotion.activate, promotion.archive, promotion.code.add, promotion.code.remove, promotion.create, promotion.pause, promotion.unarchive, promotion.update
purchase.on_behalf cart.assign_customer
search.write search.redirect.remove, search.redirect.set, search.synonym.remove, search.synonym.set
shipping.write pickup_location.create, pickup_location.update, shipping.carrier.create, shipping.carrier.update, shipping.method.create, shipping.method.update, shipping.rate.delete, shipping.rate.set, shipping.rate.set_many, shipping.zone.create, shipping.zone.update
tenant.settings.read no command — a privileged gate, consulted by the kernel (see below)
tenant.settings.write notification.channel.set_enabled, notification.template.reset, notification.template.set, tenant.settings.update
tenant.store.write tenant.store.create, tenant.store.set_custom_fields, tenant.store.update

Gate scopes: audit.read, catalog.admin.read, content.read, extensions.read, logistics.read, order.read, pii.read, promotion.read, tenant.settings.read

The login-driver credential (FORGE_ADMIN_SERVICE_TOKEN) is infra, not a person: it carries operator.otp.request, operator.session.mint and never a role.

The shopper holds customer.self.read, customer.self.write — resolved from the session identity, never from a parameter — and never appears in the table above.

The platform operator acts above the tenants, on the CONTROL face (/v1/control/commands/*, a type: 'platform' token). These are the system: true commands; no tenant role grants them.

Scope Commands
platform.admin_driver.mint platform.admin_driver.mint
platform.extension.write platform.extension.config.set, platform.extension.connection.set, platform.extension.install, platform.extension.uninstall
platform.iam.write platform.credential.issue, platform.credential.revoke
platform.tenant.write platform.admin_host.remove, platform.admin_host.set, platform.tenant.provision