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 upvendo.invite.expiry_days (default 7 days). This is deliberately not the OTP window -- a 15-minute expiry left most team invites dead on arrival.
  • 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 slugs — Str::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 upvendo.invite.expiry_days — 7 days by default)
        │
        ▼
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 upvendo.invite.expiry_days (default 7 days). (Verified: upvendo-backend config/upvendo.php:36-38, which documents in place why this is not the OTP expiry.)

FAQs ​

  • "I got an email with a temporary password -- what is it, and is that the team invitation?" No, they are two different emails. Invitation to Join Upvendo is what a team member gets when you add them under Settings -> Team. It carries no password, just an Accept Invitation link on which they set their own. The link expires after 7 days by default; if it has, use Resend Invitation. Your Upvendo Login Credentials ("Temporary Password") is a different mail, sent only by your reseller or partner from your client record in their back office, and only to the merchant owner account. It contains a generated password and forces a change on first login. Team members are never sent one -- if a staff member is waiting on credentials, resend their invitation instead. (Verified: upvendo-backend app/Services/BackOffice/Settings/TeamService.php:165-190 and app/Services/Common/EmailService.php:68-105 for the invite; app/Services/BackOffice/ResellerService.php:630-651 and EmailService.php:320-345 for the credentials mail.)

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