Skip to content

Team Management

Overview

Team Management lets merchants add staff members (users), assign them one or more roles, and control which locations each user can access in the back-office.

Key Purpose: Manage staff access and permissions.

Purpose

This page lets you invite team members, build roles from a granular permission list, assign one or more roles to each user, and limit which locations each user can access.

Key Concepts

  • Users: Individual staff accounts. Each user has a name, email, optional phone, one or more roles, and a set of accessible locations.
  • Roles: Named permission sets. The platform ships system roles (Merchant Owner, Vendor Manager, Vendor Staff, Vendor Accountant) and lets merchants create their own custom roles scoped to their account.
  • Permissions: Granular verb-noun action strings (for example view-transactions, edit-items). A role is a list of these strings; the * wildcard grants all permissions.
  • Location Access: A user either has all_location_access (all current and future locations) or is restricted to a specific list of locations.
  • Invitation Flow: New users are created with Inactive status and emailed a single-use invite token generated via Str::random(60) that expires after OTP_EXPIRY_MINUTES (default 15 minutes).
  • POS Access: For merchants on the first-party Upvendo POS, a team user's register passcode and capabilities live on the person, in a "POS access" panel inside the user dialog. A merchant's POS people are their team users — there is no separate staff roster.

Actions

Invite a Team Member

Enter the user's first name, last name, email, and optional phone; pick one or more roles; choose all-location access or specific locations. The system creates an Inactive account, validates the roles and locations against the merchant's own roles/locations, and sends an invitation email.

Assign Roles

Update which role(s) a user holds. You cannot assign roles to yourself; the backend rejects self-assignment.

Assign Locations

Update which locations a user can access (or grant all-location access). You cannot change your own location access; the backend rejects self-assignment.

Manage Custom Roles

Create, edit, or delete custom roles with a tailored set of permissions to match specific job functions (for example "Kitchen Staff" or "Marketing"). Custom roles are scoped to your merchant account.

Resend Invitation

Re-trigger the invitation email with a fresh token for users who have not yet activated their account. The "Resend Invitation" action is only available for users whose status is Inactive.

Grant POS Access (first-party Upvendo POS only)

Open a team user and use the POS access panel to give them a register passcode, view the capabilities their roles grant them, and optionally record a Belgian INSZ. See POS Access below for the full contract.

Location

  • Back-office Route (Users): /settings/team/users
  • Back-office Route (Roles): /settings/team/roles
  • Add Role: /settings/team/roles/add
  • Edit Role: /settings/team/roles/edit/:id
  • Backend Controller: app/Http/Controllers/Api/BackOffice/Settings/TeamController.php
  • Backend Routes: routes/api/backoffice/settings/team.php (mounted at /back-office/settings/team)
  • Vue Components: src/views/settings/Team/Users/TeamUsersComponent.vue, src/views/settings/Team/Users/TeamUsersFormDialog.vue, src/views/settings/Team/Users/TeamUserPosAccess.vue, src/views/settings/Team/Roles/TeamRolesComponent.vue, src/views/settings/Team/Roles/TeamRolesForm.vue

Note: Users and Roles are two separate pages reached from the "Team" group in the settings navigation, not a single tabbed page.

Concepts

Users

Individual team members with login credentials, roles, and location access.

Roles

Named permission sets that define what users can access. System roles ship with the platform; custom roles are created per merchant.

Permissions

Specific verb-noun action strings (view, create, edit, delete, plus a few specialized actions).


User Fields

First Name

PropertyValue
Field IDfirst_name
LabelFirst Name
TypeText
RequiredYes
Validationrequired, string

Description: Team member's first name. Combined with the last name into a name value on the backend.


Last Name

PropertyValue
Field IDlast_name
LabelLast Name
TypeText
RequiredYes
Validationrequired, string

Description: Team member's last name.


Email

PropertyValue
Field IDemail
LabelEmail
TypeEmail
RequiredYes
Validationrequired, valid email, unique

Description: Login email and contact address.

Business Logic:

  • Used for login
  • Must be unique
  • Receives the invitation email

Phone

PropertyValue
Field IDphone
LabelPhone
TypePhone
RequiredNo
Validationnullable, phone regex, unique

Description: Contact phone number.


Roles

PropertyValue
Field IDroles (array of { id })
LabelRoles
TypeMulti-select
RequiredYes (UI requires at least one role)
Validationnullable array on the API; each role must exist for the merchant

Description: One or more roles assigned to this user, which together determine their permissions.


All Location Access

PropertyValue
Field IDall_location_access
LabelAll Locations
TypeToggle
RequiredNo
Validationnullable, boolean

Description: When enabled, the user can access all current and future locations.

Business Logic:

  • When true, the user is not restricted to a location list.
  • Merchant Owners are always forced to all_location_access = true and cannot have it removed.

Locations

PropertyValue
Field IDlocations (array of { id })
LabelLocations
TypeMulti-select
RequiredConditional (required in the UI when All Location Access is off)
Validationnullable array on the API; each location must exist for the merchant

Description: Which specific locations this user can access when All Location Access is off.


Role Fields

Role Name

PropertyValue
Field IDname
LabelName
TypeText
RequiredYes
Validationrequired, string; must be unique within the merchant

Description: Name of the role.

Examples:

  • "Manager"
  • "Kitchen Staff"
  • "Cashier"

Description

PropertyValue
Field IDdescription
LabelDescription
TypeText
RequiredNo
Validationnullable, string

Description: Description of what this role is for.


Permissions

PropertyValue
Field IDpermissions (array of strings)
LabelPermissions
TypePermission list
RequiredNo (nullable array)
Validationeach value must be one of the known permission strings

Description: The list of permission strings this role grants. Invalid permission strings are filtered out before saving.

Note: A role's access level (level) is set to vendor automatically when a merchant creates a role; it is not a user-facing field.


Permission Categories

These are the permission groups exposed in the role builder (from Permissions::getAllPermissionsGrouped()), with their exact permission strings.

Device Management

  • view-devices, create-devices, edit-devices, delete-devices

Device Profile Management

  • view-device-profile, create-device-profile, edit-device-profile, delete-device-profile

Item Management

  • view-items, create-items, edit-items, delete-items, export-items

Inventory Management

  • view-inventory, create-inventory, edit-inventory, delete-inventory, view-unit-cost, view-inventory-history, export-inventory-history
  • view-menus, create-menus, edit-menus, delete-menus

Category Management

  • view-categories, create-categories, edit-categories, delete-categories

Modifier Management

  • view-modifier-groups, create-modifier-groups, edit-modifier-groups, delete-modifier-groups

Variant Group Management

  • view-variant-groups, create-variant-groups, edit-variant-groups, delete-variant-groups

Loyalty Program Management

  • view-loyalty, create-loyalty, edit-loyalty, delete-loyalty, view-rewards, create-rewards, edit-rewards, delete-rewards

Offer Management

  • view-offers, create-offers, edit-offers, delete-offers

Gift Card Management

  • view-gift-cards, create-gift-cards, edit-gift-cards, delete-gift-cards

Customer Management

  • view-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 Management

  • 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

Kitchen Reports

  • view-kitchen-reports

KDS Settings

  • manage-kds-settings

Payment Settings

  • view-payment-profiles, create-payment-profiles, edit-payment-profiles, delete-payment-profiles, view-billing-profiles, create-billing-profiles, edit-billing-profiles, delete-billing-profiles

Online Ordering

  • view-online-ordering, edit-online-ordering

User Management

  • view-users, create-users, edit-users, delete-users, view-roles, create-roles, edit-roles, delete-roles, assign-roles, edit-user-location-access

Location Management

  • view-locations, create-locations, edit-locations, delete-locations, export-locations, manage-location-billing, create-notifications

Content Management

  • view-content, create-content, delete-content

Tax Management

  • view-tax-rates, view-custom-tax-rates, create-custom-tax-rates, edit-custom-tax-rates, delete-custom-tax-rates

Sales Channel Management

  • view-sales-channel, edit-in-house-channel, edit-online-channel

Receipt Management

  • view-receipts, edit-receipts

Branding Management

  • view-branding-profile, create-branding-profile, edit-branding-profile, delete-branding-profile

Localization Management

  • view-language, create-language, edit-language, delete-language, view-translation, create-translation, edit-translation, delete-translation

Integration Management

  • manage-integration-settings

POS Management

  • view-pos-configuration, manage-pos-configuration, manage-pos-staff, manage-pos-layouts

POS Reports

  • view-pos-reports

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, which was unrepresentable while the two shared a group gated on view-pos-configuration (Permissions::posReporting()).

POS Register

  • 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

These twelve 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 vocabulary (PosPermission::platformPermission()). The group is ungated (gated: false, group id pos-register-group): there is no "view the register" read to hang them off, so the role builder renders them as a flat action-only list. The group id is deliberately not a permission — it is absent from getAllPermissions(), so StoreRoleRequest strips it and it can never be persisted onto a role.

Business Details Management

  • view-business-details, edit-business-details

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


Default (System) Roles

These merchant-level system roles ship with the platform. Each has a fixed slug and a default permission set.

Merchant Owner (vendor-owner)

Full access via the * wildcard. Always granted all-location access. Cannot be deleted.

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

Vendor Manager (vendor-manager)

Broad operational access: full item/menu/category/modifier/variant/upsell management, inventory, customers, transactions, loyalty, offers, devices, tax, localization, content, sales channel, receipts, branding, online ordering, and user management (view/create/edit/delete users plus view-roles). Does not include payment/billing profile management.

Vendor Staff (vendor-staff)

Limited operational access (mostly view, plus limited customer/loyalty edits):

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"
  ]
}

Vendor 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"
  ]
}

POS roles — Cashier, Shift manager, Owner

Every merchant is provisioned three further roles alongside Manager / Staff / Accountant (Roles::posDefaultRoles(), seeded from MerchantService::ensurePosDefaultRoles()), so a first-party POS onboarding needs zero permission work: assign the role, set a passcode, done. Seeding is unconditional — it does not wait for the merchant to select the Upvendo POS — and idempotent, so a merchant who has customized a bundle keeps their version.

They are ordinary merchant roles (is_system = false, scoped to the merchant's vendor_id), which is what makes them assignable in the team picker at all, and editable or deletable like any other. All three are pinned to vendor level: a staff-level role would be location-bound, and enforcing location on POS access is a deliberate out-of-scope behaviour change.

RoleSlugRegister capabilities
Cashierpos-cashierapply-pos-manual-price, apply-pos-line-discount, void-pos-line, open-pos-till
Shift managerpos-shift-managerEverything a Cashier has, plus void-pos-order, refund-pos-sale, open-pos-drawer, manage-pos-cash-movements, close-pos-till, approve-pos-variance, approve-pos-as-manager, and view-pos-reports
Ownerpos-ownerEvery register capability (including close-pos-business-day) plus the whole POS Management group and POS Reports
  • Cashier deliberately excludes anything that moves money the customer already paid or touches the cash box — refunds, drawer no-sale, pay in/out, closing a till, approving a variance. Those escalate to a manager.
  • Shift manager withholds close-pos-business-day: the fiscal day-end is the owner's act. It holds approve-pos-as-manager, which is what makes a cashier's escalation prompt resolvable by the person actually on the floor.
  • Owner exists so a merchant owner can hand over full POS authority without granting the entire back office (the Merchant Owner user already covers it via *).
  • These names are not reserved slugsStr::slug('Cashier') is cashier, not pos-cashier, so a merchant remains free to mint their own "Cashier" role.

Note: There are also global- and reseller-level roles (for example global-administrator, admin, reseller-owner, reseller-administrator) used by platform/partner staff rather than ordinary merchant team members.


POS Access

For merchants on the first-party Upvendo POS, register access is part of the team user's profile — a POS access panel inside the user dialog (src/views/settings/Team/Users/TeamUserPosAccess.vue), not a separate screen.

The standalone POS → Staff PINs roster (/pos/staff) was retired on 2026-07-30. The route still resolves — it redirects to /settings/team/users — and the nav row is gone. One credential exists per team user; the credential's staff_id is the platform user id.

Who sees it

  • The panel renders for operators holding the raw manage-pos-staff permission. That is also the permission every staff-credential endpoint is gated on, so an operator who holds it can edit the panel even when the surrounding user dialog is view-only for its own sections (those are gated on edit-users / assign-roles / edit-user-location-access, which the credential endpoints do not care about).
  • The endpoints additionally carry the first-party-pos middleware. A merchant whose pos_provider is not upvendo gets no answer, and the panel says the surface is unavailable rather than showing a broken empty state.
  • Under an admin vendor override (?vendorId=…) the panel refuses and says why: the Team surface is not vendor-scoped, so the user ids on screen and the credentials the call returns would belong to different merchants.

What it holds

ItemContract
PasscodeExactly 4 digits (digits:4) — the register auto-submits on the fourth digit, which is only unambiguous if every passcode is the same length. Stored as a hash plus a peppered lookup key; never returned by any endpoint. Reset rather than read. A collision returns 409.
CapabilitiesResolved server-side as union(role-derived, per-person override) and returned as effective_pos_permissions with a pos_permission_source of roles, override or mixed. Roles are the intended path.
Custom exceptionsThe per-person pos_permissions array — a transitional escape hatch, collapsed behind "Custom exceptions for this person" in the UI and reduced to a quiet link when none exists.
INSZBelgian national number, 11 digits, write-only — encrypted at rest and never returned; only a has_insz badge surfaces its presence. Region-gated on the merchant locale (it exists only inside the Belgian GKS/FDM regime) but stays visible once one is on file. It identifies the actor towards the FDM on GKS S/P/N events; when null, the robot INSZ signs instead.
Walletwallet_enabled opts the staff member into waiter banking (staff_wallet tills).
Robot credentialsis_robot marks a non-human credential. It has no linked user, so it surfaces its staff_id with a null name.

How capabilities actually reach the register

Capabilities are displayed, not primarily edited — the panel leads with their source ("From this person's roles: Cashier") and only then shows the chips.

Enforcement is entirely server-side. Every gated POS route 403s with {message, permission} and the register escalates on the 403. The register app currently stores the capability array and hides nothing — do not describe it as "the register hides buttons for a cashier". Least-privilege register UI is a tracked follow-up, and when it lands it is an affordance on top of the 403s, never a replacement for them.

Endpoints

All under /back-office/pos/staff-credentials, all gated manage-pos-staff + first-party-pos (routes/api/backoffice/pos.php:43-58):

MethodPathPurpose
GET/List the merchant's credentials
GET/assignable-staffThe merchant's users, for the creation picker
POST/Grant access (staff_id, pin, optional insz, pos_permissions, wallet_enabled)
PUT/{credentialId}Update capabilities / wallet / INSZ
POST/{credentialId}/reset-pinIssue a new passcode
DELETE/{credentialId}Revoke access

On update, insz is three-state: key absent = unchanged, '' = clear, value = set.


Business Logic

Permission Check

User attempts action


Collect permissions from the user's role(s)


Check permissions:
├── Has wildcard (*)? → Allow
├── Has the required permission string? → Allow
└── Otherwise → Deny (403 Forbidden)

Location Filtering

User requests data


Check the user's location access:
├── all_location_access = true? → All locations
└── Otherwise → Limited to the assigned location list

Invitation Flow

Admin invites user (status = Inactive)


System generates a single-use token (Str::random(60), expires after OTP_EXPIRY_MINUTES)


System sends invitation email


User clicks link and sets password


Account activated (status = Active)

Customer Impact

Team management doesn't directly affect customers, but:

  • Staff with limited permissions may not be able to help with certain issues.
  • Location-restricted staff only see their location's data.

Relations

Depends On

  • Locations: Users can be restricted to specific locations.

Affects

  • All Features: Permissions control access.
  • Activity Log: Actions tracked by user.

Business Rules

  • A user's email must be unique. Adding an email that is already associated with a different merchant account is rejected with a 400 error.
  • You cannot assign roles to yourself, and you cannot change your own location access; the backend rejects both with a 400 error.
  • You cannot delete yourself, and you cannot delete the Merchant Owner (vendor-owner); both are rejected with a 400 error.
  • The Merchant Owner role grants the * wildcard and is always forced to all-location access.
  • Custom role names must be unique within the merchant account.
  • Role and location IDs are validated against the merchant's own roles and locations before any assignment is persisted.
  • Invitation tokens are single-use, generated with Str::random(60), and expire after OTP_EXPIRY_MINUTES (default 15 minutes).

FAQs

  • "Can a user have more than one role?" Yes, the roles field is a list, and a user's effective permissions are the union of all their roles' permissions.
  • "What happens when a user is still Inactive?" They have not yet accepted their invitation. You can use "Resend Invitation" to send a fresh token.
  • "Can a user belong to multiple locations?" Yes. Assign a list of locations, or enable All Location Access to grant access to all current and future locations.
  • "Can I delete the owner?" No. The Merchant Owner (vendor-owner) cannot be deleted.
  • "Are permission changes immediate?" Once a role's permissions are updated, the change applies to its users on subsequent requests.

Troubleshooting

Problem: User can't log in

Causes:

  1. User is still Inactive (invitation not yet accepted)
  2. Wrong email/password

Solutions:

  1. Resend the invitation so the user can set a password
  2. Confirm the correct email is being used

Problem: User can't access a feature

Causes:

  1. None of the user's roles include the required permission
  2. User assigned to the wrong role
  3. Location restriction

Solutions:

  1. Add the permission to one of the user's roles
  2. Assign the correct role
  3. Grant the relevant location (or enable All Location Access)

Problem: User sees the wrong locations

Causes:

  1. Location assignments are incorrect
  2. A new location was not assigned

Solutions:

  1. Update the user's location access
  2. Add the new location, or enable All Location Access

Examples

Invite a team member with a role and all locations

json
{
  "first_name": "Jane",
  "last_name": "Manager",
  "email": "jane@restaurant.com",
  "all_location_access": true,
  "roles": [{ "id": "<vendor-manager-role-id>" }]
}

Invite a location-restricted staff member

json
{
  "first_name": "Amir",
  "last_name": "Cashier",
  "email": "amir@chain.com",
  "all_location_access": false,
  "roles": [{ "id": "<vendor-staff-role-id>" }],
  "locations": [{ "id": "<location-amsterdam-id>" }]
}

Custom Role: Kitchen Staff

json
{
  "name": "Kitchen Staff",
  "description": "View orders and inventory only",
  "permissions": [
    "view-transactions",
    "view-transaction-order",
    "view-inventory",
    "view-items",
    "view-menus"
  ]
}

Custom Role: Marketing

json
{
  "name": "Marketing",
  "description": "Manage loyalty, offers, and customers",
  "permissions": [
    "view-loyalty",
    "create-loyalty",
    "edit-loyalty",
    "view-offers",
    "create-offers",
    "edit-offers",
    "view-customers",
    "edit-customers"
  ]
}