Roles and permissions
A member holds exactly one role per workspace. Roles and permissions are defined in one place in the code (App\Enums\Permission and WorkspaceUser::hasPermission()) and a matrix test must change with them. The UI never re-derives role logic; it reads a shared capability map.
| Capability | Viewer | Staff | Manager | Admin | Owner |
|---|---|---|---|---|---|
| View orders, products, inventory | yes | yes | yes | yes | yes |
| Create orders and advance their status forward | yes | yes | yes | yes | |
Record offline payments (orders.payments, ADR-014) |
yes | yes | yes | yes | |
| Edit, cancel and delete orders; reverse a payment entry; issue a new status link | yes | yes | yes | ||
| Create, edit and delete products | yes | yes | yes | ||
| Manage inventory (suppliers, supplies, batches) | yes | yes | yes | ||
| See purchase cost, margins and procurement prices (ADR-001) | yes | yes | yes | ||
See Finance and export it (finance.view, ADR-018) |
yes | yes | yes | ||
Record, correct and remove operating expenses (finance.expenses) |
yes | yes | yes | ||
| Use AI generation | yes | yes | yes | ||
| Invite users | yes | yes | yes | ||
| Manage users and remove users | yes | yes | |||
| Edit workspace settings and provider credentials | yes | yes | |||
| Delete or transfer the workspace (ADR-003) | yes |
Additional protections by role:
- Viewers receive masked customer phone numbers and email addresses.
- Staff see only forward status options on an order.
- Cross-tenant records return 404; actions a role lacks return 403.
Settings → Team (ADR-021) is where the roles are given:
- managers and above invite by email;
- admins and the owner also change roles and remove members.
Everyone acts only on roles below their own: the owner can make admins, admins can make managers, and managers can add staff and viewers. Nobody changes the owner or themselves. Invitation links are single-use, expire after seven days, and join only the invited address.
Enforcement pattern
Section titled “Enforcement pattern”IdentifyTenantresolves the workspace from the host before route binding.- Route model binding is tenant-scoped, so a foreign identifier never resolves.
- Controllers call
authorize(); form requests callcan(); non-resource actions use a gate named after the permission value. - Policies check tenant alignment first, then the role.
SetWorkspaceContextshares acanmap with the React front end for showing and hiding controls. It is a convenience, not a security boundary.