Appearance
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-nounaction strings (for exampleview-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
Inactivestatus and emailed a single-use invite token generated viaStr::random(60)that expires afterOTP_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
| Property | Value |
|---|---|
| Field ID | first_name |
| Label | First Name |
| Type | Text |
| Required | Yes |
| Validation | required, string |
Description: Team member's first name. Combined with the last name into a name value on the backend.
Last Name
| Property | Value |
|---|---|
| Field ID | last_name |
| Label | Last Name |
| Type | Text |
| Required | Yes |
| Validation | required, string |
Description: Team member's last name.
Email
| Property | Value |
|---|---|
| Field ID | email |
| Label | |
| Type | |
| Required | Yes |
| Validation | required, valid email, unique |
Description: Login email and contact address.
Business Logic:
- Used for login
- Must be unique
- Receives the invitation email
Phone
| Property | Value |
|---|---|
| Field ID | phone |
| Label | Phone |
| Type | Phone |
| Required | No |
| Validation | nullable, phone regex, unique |
Description: Contact phone number.
Roles
| Property | Value |
|---|---|
| Field ID | roles (array of { id }) |
| Label | Roles |
| Type | Multi-select |
| Required | Yes (UI requires at least one role) |
| Validation | nullable 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
| Property | Value |
|---|---|
| Field ID | all_location_access |
| Label | All Locations |
| Type | Toggle |
| Required | No |
| Validation | nullable, 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 = trueand cannot have it removed.
Locations
| Property | Value |
|---|---|
| Field ID | locations (array of { id }) |
| Label | Locations |
| Type | Multi-select |
| Required | Conditional (required in the UI when All Location Access is off) |
| Validation | nullable 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
| Property | Value |
|---|---|
| Field ID | name |
| Label | Name |
| Type | Text |
| Required | Yes |
| Validation | required, string; must be unique within the merchant |
Description: Name of the role.
Examples:
- "Manager"
- "Kitchen Staff"
- "Cashier"
Description
| Property | Value |
|---|---|
| Field ID | description |
| Label | Description |
| Type | Text |
| Required | No |
| Validation | nullable, string |
Description: Description of what this role is for.
Permissions
| Property | Value |
|---|---|
| Field ID | permissions (array of strings) |
| Label | Permissions |
| Type | Permission list |
| Required | No (nullable array) |
| Validation | each 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 tovendorautomatically 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
Menu Management
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-reportsis 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 onview-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
PosPermissionvocabulary (PosPermission::platformPermission()). The group is ungated (gated: false, group idpos-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 fromgetAllPermissions(), soStoreRoleRequeststrips 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.
| Role | Slug | Register capabilities |
|---|---|---|
| Cashier | pos-cashier | apply-pos-manual-price, apply-pos-line-discount, void-pos-line, open-pos-till |
| Shift manager | pos-shift-manager | Everything 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 |
| Owner | pos-owner | Every 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 holdsapprove-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')iscashier, notpos-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'sstaff_idis the platform user id.
Who sees it
- The panel renders for operators holding the raw
manage-pos-staffpermission. 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 onedit-users/assign-roles/edit-user-location-access, which the credential endpoints do not care about). - The endpoints additionally carry the
first-party-posmiddleware. A merchant whosepos_provideris notupvendogets 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
| Item | Contract |
|---|---|
| Passcode | Exactly 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. |
| Capabilities | Resolved 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 exceptions | The 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. |
| INSZ | Belgian 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. |
| Wallet | wallet_enabled opts the staff member into waiter banking (staff_wallet tills). |
| Robot credentials | is_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):
| Method | Path | Purpose |
|---|---|---|
| GET | / | List the merchant's credentials |
| GET | /assignable-staff | The 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-pin | Issue a new passcode |
| DELETE | /{credentialId} | Revoke access |
On update,
inszis 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 listInvitation 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.
Related Features
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 afterOTP_EXPIRY_MINUTES(default 15 minutes).
FAQs
- "Can a user have more than one role?" Yes, the
rolesfield 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:
- User is still
Inactive(invitation not yet accepted) - Wrong email/password
Solutions:
- Resend the invitation so the user can set a password
- Confirm the correct email is being used
Problem: User can't access a feature
Causes:
- None of the user's roles include the required permission
- User assigned to the wrong role
- Location restriction
Solutions:
- Add the permission to one of the user's roles
- Assign the correct role
- Grant the relevant location (or enable All Location Access)
Problem: User sees the wrong locations
Causes:
- Location assignments are incorrect
- A new location was not assigned
Solutions:
- Update the user's location access
- 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"
]
}