Skip to content

Permissions Reference

How roles and permissions control access to back-office features.

For the full Team Management feature (inviting users, assigning roles, location access), see Team.


How Access Works

Access is permission-based, not a fixed tier list. The model has three layers:

  • Permissions: Granular verb-noun action strings (for example view-transactions, edit-items, delete-customers). These are the atomic units of access.
  • Roles: Named sets of permission strings. A role is just a list of permissions. The * wildcard grants every permission.
  • Users: Each user is assigned one or more roles. A user's effective permissions are the union of all their roles' permissions.

When a user attempts an action, the backend checks whether any of their roles contains either the required permission string or the * wildcard. If neither is present, the request is denied (403 Forbidden).

The platform ships default roles, and merchants can also create their own custom roles by picking from the granular permission list. So access can be customized per role; it is not limited to a fixed set of tiers. One naming constraint applies — see Reserved role names.


Default Merchant Roles

When a merchant account is created, six merchant-scoped roles are provisioned — Manager, Staff, Accountant, and the three POS access roles Cashier, Shift manager and Owner — all editable starting points. The Merchant Owner role is a platform-level role that is looked up by slug and assigned to the signing-up user, not created per merchant. (Verified: app/Services/Common/MerchantService.phpcreateMerchantDefaultRole(), which ends by calling ensurePosDefaultRoles(); assignMerchantOwnerRole().)

Display NameSlugAccess
Merchant Ownervendor-ownerFull access via the * wildcard. Always has all-location access. Cannot be deleted.
Managervendor-managerBroad day-to-day operations access (see below).
Staffvendor-staffMostly view access, plus limited customer and loyalty actions.
Accountantvendor-accountantFinance-focused, view-only access.
Cashierpos-cashierSells on the register — open prices, line discounts, void a line before payment, open own till.
Shift managerpos-shift-managerRuns the shift — refunds, voids, drawer, till close, approves escalations; also reads POS reports.
Ownerpos-ownerFull POS authority — every register capability plus POS configuration and reports.

The three POS roles are seeded for every merchant, not only those on the first-party Upvendo POS — the call is unconditional. They are ordinary merchant roles (is_system = false), so they show in the role picker and the merchant can edit or delete them; seeding is idempotent, so a customized bundle survives. Their names are not reserved slugs — Str::slug('Cashier') is cashier, not pos-cashier, so a merchant can still mint their own "Cashier". Full capability breakdown: Team Management.

Note: a per-merchant admin role exists, but nobody can be assigned it. The RolesRepair data-repair backfill mints an admin role scoped to each merchant (vendor_id = <merchantId>, is_system = true, level = global, display name "Administrator"). It is not a platform-only role, and it is not created by ordinary merchant signup. Its permission list is the full expansion of Permissions::getAllPermissions('admin', true) — 144 explicit permission strings, not the * wildcard — and it includes platform-level permissions such as view-field-ops, manage-field-ops, and edit-global-settings. (Verified: app/Console/Commands/DataRepair/RolesRepair.php lines 116-125; app/Constants/Roles.phpself::ADMIN => Permissions::getAllPermissions(self::ADMIN, true).)

Because roleOptions() filters out every is_system role, this admin role does not appear in the role picker, and storeUser() / assignRoles() reject it with "Invalid role" (HTTP 400) — roleOptions() is the only allow-list they validate against. That is deliberate: returning it would let a merchant owner self-assign admin and clear the field-ops permission gate. (Verified: app/Services/BackOffice/Settings/TeamService.php lines 43-53, 114-119, 239-243.)

Genuinely platform-level roles — global-administrator, global-support, global-developer, global-rep, field-ops, reseller-owner, reseller-administrator, reseller-rep — carry no vendor_id and are not part of a merchant's team.

Some back-office surfaces gate on a role slug in addition to permissions. Nav items and route meta can carry an optional role, which is ANDed with the usual CASL/permission gate; hasRole() reads userData.role / userData.roles and fails closed when userData is absent. This exists because view global-settings is also held by global-support and global-developer, and CASL cannot tell those apart from global-administrator — all three carry manage:all. The current example is the Async Processing monitor (async-monitoring), which is restricted to global-administrator on both the nav entry and the route, matching the global-admin middleware on the backend prefix.

Merchant Owner (vendor-owner)

json
{ "slug": "vendor-owner", "permissions": ["*"] }

Full access to everything. Always forced to all-location access and cannot be deleted.

Manager (vendor-manager)

Broad operational access. Granted permissions include:

  • Items: view-items, create-items, edit-items, delete-items, export-items
  • Inventory: view-inventory, create-inventory, edit-inventory, delete-inventory, view-unit-cost, view-inventory-history, export-inventory-history
  • Menus: view-menus, create-menus, edit-menus, delete-menus
  • Categories: view-categories, create-categories, edit-categories, delete-categories
  • Modifier Groups: view-modifier-groups, create-modifier-groups, edit-modifier-groups, delete-modifier-groups
  • Upsell Groups: view-upsell-groups, create-upsell-groups, edit-upsell-groups, delete-upsell-groups
  • Variant Groups: view-variant-groups, create-variant-groups, edit-variant-groups, delete-variant-groups
  • Customers: view-customers, create-customers, edit-customers, delete-customers, export-customers, merge-customers, view-customer-orders, view-customer-gift-cards, view-customer-rewards
  • Transactions: view-transactions, view-transaction-details, send-transaction-receipt, download-transaction, refund-transactions, view-transaction-payment-details, view-transaction-order, export-transaction-reports, view-transaction-items
  • Loyalty: view-loyalty, create-loyalty, edit-loyalty, delete-loyalty, view-loyalty-points, edit-loyalty-points, view-rewards, create-rewards, edit-rewards, delete-rewards
  • Offers: view-offers, create-offers, edit-offers, delete-offers
  • Devices: view-devices, create-devices, edit-devices, delete-devices
  • Device Profiles: view-device-profile, create-device-profile, edit-device-profile, delete-device-profile
  • Tax: view-tax-rates, view-custom-tax-rates, create-custom-tax-rates, edit-custom-tax-rates, delete-custom-tax-rates
  • Sales Channel: view-sales-channel, edit-in-house-channel, edit-online-channel
  • Online Ordering: view-online-ordering, edit-online-ordering
  • Receipts: view-receipts, edit-receipts
  • Branding: view-branding-profile, create-branding-profile, edit-branding-profile
  • Localization: view-language, create-language, edit-language, delete-language, view-translation, create-translation, edit-translation, delete-translation
  • Content: view-content, create-content, delete-content
  • Users: view-users, create-users, edit-users, delete-users, view-roles
  • Locations: view-locations, view-location-details, create-locations, edit-locations, delete-locations, export-locations, edit-opening-hours
  • Notifications: create-notifications

Managers do not get payment/billing profile management by default, and they do not get manage-location-billing — that one is owner-only, because deactivating a location cancels its Stripe subscriptions and payment test mode makes real orders free.

(Verified against Roles::defaultPermissionsMap()[Roles::VENDOR_MANAGER] in app/Constants/Roles.php — 111 permissions.)

Note: view-location-details and edit-opening-hours are granted to Manager but are not exposed in the role builder — they are commented out of Permissions::locationManagement(). A custom role therefore cannot be given them through the UI.

Staff (vendor-staff)

Limited operational access, mostly view-only with a few customer and loyalty actions:

json
{
  "slug": "vendor-staff",
  "permissions": [
    "view-customers",
    "create-customers",
    "edit-customers",
    "view-customer-orders",
    "view-transactions",
    "view-transaction-details",
    "send-transaction-receipt",
    "view-transaction-order",
    "view-transaction-items",
    "view-loyalty",
    "edit-loyalty-points",
    "view-loyalty-points",
    "view-rewards",
    "view-inventory",
    "view-unit-cost",
    "view-inventory-history",
    "view-items",
    "view-categories",
    "view-modifier-groups",
    "view-variant-groups",
    "view-menus"
  ]
}

Accountant (vendor-accountant)

Finance-focused, view-only access:

json
{
  "slug": "vendor-accountant",
  "permissions": [
    "view-tax-rates",
    "view-custom-tax-rates",
    "view-customers",
    "view-customer-orders",
    "view-transactions",
    "view-transaction-details",
    "download-transaction",
    "view-transaction-payment-details",
    "view-transaction-order",
    "export-transaction-reports",
    "view-transaction-items",
    "view-payment-profiles",
    "view-billing-profiles",
    "view-inventory",
    "view-unit-cost",
    "view-inventory-history"
  ]
}

Permission Groups

These are the permission groups shown in the merchant role builder (from Permissions::getAllPermissionsGrouped()), with their exact permission strings, in the order the builder renders them. (Verified: Permissions::getAllPermissionsGrouped() at app/Constants/Permissions.php:1188, with each group's contents from the corresponding method — e.g. locationManagement() at :792.)

GroupPermissions
Device Managementview-devices, create-devices, edit-devices, delete-devices
Device Profile Managementview-device-profile, create-device-profile, edit-device-profile, delete-device-profile
Item Managementview-items, create-items, edit-items, delete-items, export-items
Inventory Managementview-inventory, create-inventory, edit-inventory, delete-inventory, view-unit-cost, view-inventory-history, export-inventory-history
Menu Managementview-menus, create-menus, edit-menus, delete-menus
Category Managementview-categories, create-categories, edit-categories, delete-categories
Modifier Managementview-modifier-groups, create-modifier-groups, edit-modifier-groups, delete-modifier-groups
Variant Group Managementview-variant-groups, create-variant-groups, edit-variant-groups, delete-variant-groups
Loyalty Program Managementview-loyalty, create-loyalty, edit-loyalty, delete-loyalty, view-rewards, create-rewards, edit-rewards, delete-rewards
Offer Managementview-offers, create-offers, edit-offers, delete-offers
Gift Card Managementview-gift-cards, create-gift-cards, edit-gift-cards, delete-gift-cards
Customer Managementview-customers, create-customers, edit-customers, delete-customers, export-customers, view-customer-orders, view-customer-gift-cards, view-customer-rewards, merge-customers, view-invoice-collection, manage-invoice-collection
Transaction Managementview-transactions, view-transaction-details, send-transaction-receipt, download-transaction, refund-transactions, view-transaction-payment-details, view-transaction-order, export-transaction-reports, view-transaction-items
Kitchen Reportsview-kitchen-reports
KDS Settingsmanage-kds-settings
Payment Settingsview-payment-profiles, create-payment-profiles, edit-payment-profiles, delete-payment-profiles, view-billing-profiles, create-billing-profiles, edit-billing-profiles, delete-billing-profiles
Online Orderingview-online-ordering, edit-online-ordering
User Managementview-users, create-users, edit-users, delete-users, view-roles, create-roles, edit-roles, delete-roles, assign-roles, edit-user-location-access
Location Managementview-locations, create-locations, edit-locations, delete-locations, export-locations, manage-location-billing, create-notifications
Content Managementview-content, create-content, delete-content
Tax Managementview-tax-rates, view-custom-tax-rates, create-custom-tax-rates, edit-custom-tax-rates, delete-custom-tax-rates
Sales Channel Managementview-sales-channel, edit-in-house-channel, edit-online-channel
Receipt Managementview-receipts, edit-receipts
Branding Managementview-branding-profile, create-branding-profile, edit-branding-profile, delete-branding-profile
Localization Managementview-language, create-language, edit-language, delete-language, view-translation, create-translation, edit-translation, delete-translation
Integration Managementmanage-integration-settings
POS Managementview-pos-configuration, manage-pos-configuration, manage-pos-staff, manage-pos-layouts
POS Reportsview-pos-reports
POS Register (ungated)apply-pos-manual-price, apply-pos-line-discount, void-pos-line, void-pos-order, refund-pos-sale, open-pos-drawer, manage-pos-cash-movements, open-pos-till, close-pos-till, approve-pos-variance, close-pos-business-day, approve-pos-as-manager
Business Details Managementview-business-details, edit-business-details

Note: Permission strings are flat verb-noun slugs (for example view-transactions), not dot-notation. Integrations are governed by a single manage-integration-settings permission, not one permission per provider (there is no separate stripe, square, deliveroo, etc. permission).

Note: Additional permission strings exist in the codebase (global-admin, reseller, and system-administration permissions) but are not exposed in the standard merchant role builder.

Note: view-activity-logs gates the Settings → Activity Log surface. The nav entry carries the literal permission (src/navigation/settings/index.ts line 98) and the datatable API is gated permission:view-activity-logs (routes/api/backoffice/settings/activity-logs.php line 9); the Vue route itself is ungated (src/plugins/1.router/additional-routes.ts lines 109-113), so navigating directly to /settings/activity-log loads the page and the data call 403s. The permission lives in Permissions::systemAdministration() (Permissions.php line 939), which is merged into getAllPermissions() (line 1052) but not into getAllPermissionsGrouped() (line 1070) — so it is never rendered in the role builder, even though StoreRoleRequest (line 49) would accept it if set directly via the API. In practice the only assignable merchant role holding it is Merchant Owner via *; the unassignable per-merchant admin role (see the note above) holds it explicitly.

Note: POS Management and the POS pages behind it are additionally gated by the first-party-pos middleware — a merchant whose pos_provider is not upvendo gets a 403 on those routes no matter which permissions their role holds.

Note: view-pos-reports is its own group, not part of POS Management. The approved "Shift manager" bundle reads POS reports without holding any POS-configuration permission (the Square shape: one permission set spans dashboard and register, configuration stays with the owner). While reports rode the POS Management group they were gated on view-pos-configuration in the role editor, which made that bundle unrepresentable.

Note: The POS Register group is ungated (gated: false, group id pos-register-group). Its twelve entries are register capabilities — what a person may do standing at the till — carried by ordinary platform roles and mapped 1:1 onto the register's internal PosPermission enum via PosPermission::platformPermission(). There is no "view the register" read to gate them on, so the role builder renders them as a flat action-only list. They used to ride POS Management, which forced any role wanting a single register capability to also take view-pos-configuration and made the four-capability Cashier bundle literally unrepresentable. The group id itself is not a permission: it is absent from getAllPermissions(), so StoreRoleRequest strips it and it can never be persisted onto a role.

Note: Register capabilities are enforced server-side. Every gated POS route 403s with {message, permission} and the register escalates on the 403. The register app currently stores its capability array and hides nothing — least-privilege register UI is a tracked follow-up, and would be an affordance on top of the 403s, not a replacement for them.

Note: A reseller's sellable POS catalog can never contain the first-party Upvendo POS. GET /back-office/pos-providers — the picker that fills the POS-providers field on the reseller create/edit dialog (upvendo-backoffice src/views/global-settings/resellers/ResellerFormDialog.vue line 249) — drops every provider whose config carries first_party: true before it builds the response: array_filter($providers, fn ($provider) => ! ($provider['first_party'] ?? false)) (app/Http/Controllers/Api/BackOffice/ResellerController.php line 608). The filter is unconditional — nothing about the environment, the caller's role, or the provider's active flag gates it, and it runs before active is even read (it is only copied into the payload at line 619). So it will still hold after config/pos-providers.php's upvendo => active is flipped to true. upvendo is the only entry carrying first_party => true (config/pos-providers.php line 40).

That guarantee comes from the picker, not from validation. StoreResellerRequest (lines 34-35) and UpdateResellerRequest (lines 25-26) validate pos_providers.* as a bare string with no allow-list, so a hand-crafted API call could still write upvendo onto a reseller record. For provider selection that write is inert: ResellerService::getMerchantResellerProviders() drops any provider with active => false first (lines 726-728), then applies the test_only staging filter (line 731), and, for first_party providers, returns early before the reseller-key check (lines 736-738), so the reseller's own list is never consulted for one.

It is not inert everywhere, though — do not treat pos_providers as display-only. SubscriptionCommissionHandler reads $reseller->getPosProviders()[0] at :216 and :583 and keys revenue-split and integration-fee resolution on it (Reseller.php:329 getPosProviderRule, :360 getProductRevenueRuleForProvider). Writing an unexpected slug into first position changes which commission rule resolves, or whether one resolves at all. The picker's filtering is a UI guarantee, not a validation guarantee.

Who can reach that picker: the Resellers nav and its pages are gated on view global-settings (upvendo-backoffice src/navigation/vertical/index.ts lines 343-361), which global-administrator, global-support and global-developer all hold. The endpoint itself carries only the shared back-office stack (auth, type:backoffice, tenant:backoffice, check-user-activity) — routes/api/backoffice/resellers.php line 9 adds no permission: or global-admin middleware of its own.


Location-Based Access

Each user's access can be scoped to locations independently of their role:

SettingEffect
All Location Access (all_location_access = true)Access to all current and future locations.
Specific LocationsOnly the locations explicitly assigned to the user.

The Merchant Owner is always forced to all-location access. For other users, when all_location_access is off, the backend restricts data to the assigned location list.

Example: A user restricted to "Downtown" cannot see or manage "Airport" data, regardless of which role they hold.


Permission Check Flow

User attempts an action


Collect the user's roles (no roles at all → Deny)


Action scoped to another merchant?
  → Allow only if the user holds a reseller role
    AND that merchant is in their merchant list; else Deny


Global Administrator role WITH merchant_id = null?  → Allow (full bypass)


Location-bound role, and the location isn't assigned?  → Deny


Any role has wildcard "*"?      → Allow
Any role has the exact string?  → Allow


Otherwise                        → Deny (403 Forbidden)

The global-administrator bypass requires both the global-administrator slug and merchant_id === null. A role carrying that slug but scoped to a merchant is rejected — role creation is gated only by create-roles, which merchant owners hold, so the slug alone would otherwise be spoofable into a cross-tenant escalation. The same filter is applied to hasRole() and isGlobalUser(). (Verified: app/Services/PermissionService.php lines 46-52, 104-107, and 153-156.)

Location-bound roles are additionally checked against the user's assigned locations; Merchant Owner, reseller, and any role whose level is global, reseller, or partner are not location-bound.


Common Scenarios

"Staff can't edit menu items"

Expected: The default Staff role only includes view-items / view-menus (and similar view permissions). To allow editing, either assign a role that includes edit-items / edit-menus, or add those permissions to a custom role.

"Manager can't manage payment settings"

Expected: The default Manager role does not include payment/billing profile permissions (view-payment-profiles, edit-payment-profiles, etc.). Only the Merchant Owner (via *) has them by default. Add the relevant permissions to a custom role if a manager needs them.

"A manager can't see the Activity Log"

Expected: view-activity-logs is not offered in the role builder (it lives in the system-administration group, which getAllPermissionsGrouped() omits), so only Merchant Owner holds it via *. It cannot be added to a custom role from the UI.

"A user can't see a feature they should"

Check: Whether any of the user's roles includes the required permission string, and whether the feature's location is within the user's assigned locations.

"A new team member can't see other locations"

Check: The user's location assignment. Enable All Location Access, or add the specific location.

"This role name is reserved by the platform"

Expected: The name you chose slugifies onto a platform role slug (for example "Admin" → admin, "Field Ops" → field-ops). Pick a different display name — "Administrator", "Manager", "Kitchen Staff" and similar are all fine. See Reserved role names.

"I can see an Administrator role but can't assign it"

Expected: That is the system-managed per-merchant admin role. System roles are filtered out of the role picker, and assigning one is rejected with "Invalid role". Create a custom role with the permissions you need instead.


Changing What Someone Can Do

There are two ways to adjust access:

  1. Reassign roles: In Settings → Team → Users, edit the user and change which role(s) they hold. A user's effective permissions are the union of all assigned roles.
  2. Edit or create a role: In Settings → Team → Roles, edit an existing role's permission list or create a custom role with exactly the permissions you need (for example "Kitchen Staff" or "Marketing"). Custom roles are scoped to your merchant account. Role names must be unique within your account, and a small set of names is reserved — see below.

Note: You cannot assign roles to yourself or change your own location access (the backend rejects both). The Merchant Owner (vendor-owner) role cannot be deleted, and you cannot delete your own account.

Permission updates take effect on the user's subsequent requests.


Reserved Role Names

A role's slug is derived from its display name (lower-cased, spaces to hyphens), and creating or renaming a role onto a slug that matches a platform role is rejected:

This role name is reserved by the platform. Please choose a different name.

The response status is 422. This guard exists because role creation is gated only by create-roles / edit-roles, which merchant owners hold — without it a merchant could name a role "Global Administrator", land on the global-administrator slug, and satisfy every slug-based authority check.

The reserved slugs are exactly the keys of Roles::defaultPermissionsMap():

global-administrator · global-support · global-developer · global-rep · field-ops · reseller-owner · reseller-administrator · reseller-rep · vendor-owner · admin · vendor-manager · vendor-staff · vendor-accountant

So the display names that trip this are the ones that slugify onto that list — "Admin", "Field Ops", "Global Administrator", "Reseller Owner", "Vendor Manager", and so on.

Names that merchants often reach for are not reserved, because they slugify to something else: "Administrator" → administrator, "Manager" → manager, "Staff" → staff, "Owner" → owner, "Kitchen Staff" → kitchen-staff, "Marketing" → marketing. Location-level roles the merchant fully customizes are deliberately left unreserved.

Note that a plain "Manager", "Staff", or "Accountant" will usually still be refused, but for a different reason and with a different message — the default roles already occupy those display names, so you get "Name should be unique" (400) instead.

(Verified: app/Services/BackOffice/Settings/TeamService.phpguardReservedSlug() lines 375-384, called from storeRole() line 362 and updateRole() line 410; uniqueness check at lines 352-357; app/Constants/Roles.php lines 84-87.)