Permissions & RBAC
Who can do what — the seven roles, their permissions, and the rules behind them
How Access Control Works
Every protected endpoint is guarded by a permission string (e.g.
hr.employees.create). A user's role grants a fixed default set of these
strings. When a request arrives, the backend checks whether the caller's
permission set contains the string the route requires — if not, it returns
403 AUTHORIZATION_FAILED.
The frontend hides buttons and menu items you can't use, but that's a convenience, not a security boundary. Every rule below is enforced server-side on every request. Tampering with the UI or the request body cannot grant access the role doesn't have.
Anatomy of a permission string
v2 uses dot-style strings shaped as resource.action with an optional
scope suffix:
v1 colon-style strings like product:read are still accepted.
New v2 code uses the dot-style strings shown here. There are ~280 permissions
in the v2 catalogue; this page focuses on the HRM, ESS, and notification
families.
The Seven Roles
| Role | Scope | In one line |
|---|---|---|
| SUPER_ADMIN | Platform | Every permission, across all tenants (checked by a short-circuit before the permission set is even consulted). |
| ADMIN | One business | Full control of their tenant — POS, finance, and all of HRM & Payroll. |
| MANAGER | Store + team | Store operations, plus team-scoped HR: approve their reports' attendance corrections, leave, and shift swaps. |
| CASHIER | POS terminal | Sell, handle their own cash shift, and use self-service. No back-office. |
| HR_MANAGER v2 | HR domain | Runs HR & payroll setup and processing — but does not approve or disburse payroll (see SoD below). |
| ACCOUNTANT v2 | Finance | Finance + the payroll approve/disburse half of the split, plus advances approve/disburse. |
| EMPLOYEE v2 | Self only | Self-service only — exactly the ess.* bundle, nothing else. |
Every role except EMPLOYEE also receives the full
ess.* self-service bundle — because a manager or accountant still
needs to clock in, apply for leave, and read their own payslip. The
EMPLOYEE role is the only one limited to just that
bundle.
Payroll Separation of Duties (Important)
POSVelo deliberately splits payroll across two roles so the same person can't both prepare and authorize a payment. This is a real, built-in control — not just a recommendation.
| Payroll step | Permission | HR_MANAGER | ACCOUNTANT | ADMIN |
|---|---|---|---|---|
| Build structures / assign salaries | hr.payroll.salary-structure.manage | Yes | Read only | Yes |
| Create a run | hr.payroll.run.create | Yes | No | Yes |
| Process (calculate payslips) | hr.payroll.run.process | Yes | No | Yes |
| Approve (finalize payslips) | hr.payroll.run.approve | No | Yes | Yes |
| Disburse (record payment) | hr.payroll.run.disburse | No | Yes | Yes |
An HR_MANAGER prepares and processes a payroll run;
an ACCOUNTANT approves and disburses it. Neither can do the
other half, so two people are always involved. An ADMIN can do
everything — but the run-approve step still enforces that the approver isn't
the same person who processed it, and may require step-up 2FA.
HRM Capability Matrix
A condensed view of who can do the common HR actions. "Team" means the action is limited to employees who report to that manager.
| Capability | ADMIN | HR_MGR | MANAGER | ACCT | CASHIER | EMPLOYEE |
|---|---|---|---|---|---|---|
| Manage departments / designations | ✓ | ✓ | — | — | — | — |
| Create / update employees | ✓ | ✓ | — | — | — | — |
| Read sensitive PII (NID / bank) | ✓ | ✓ | — | — | — | — |
| Read salary | ✓ | ✓ | — | ✓ | — | Own |
| Terminate an employee | ✓ | ✓ | — | — | — | — |
| Read attendance | All | All | Team | — | — | Own |
| Manual punch for others | ✓ | ✓ | Team | — | — | — |
| Approve attendance corrections | ✓ | ✓ | Team | — | — | — |
| Manage shift templates | ✓ | ✓ | — | — | — | — |
| Create / edit roster | ✓ | ✓ | Team | — | — | — |
| Approve a shift swap | ✓ | ✓ | Team | — | — | — |
| Manage leave types / policies | ✓ | ✓ | — | — | — | — |
| Approve / reject leave | ✓ | ✓ | Team | — | — | — |
| Process a payroll run | ✓ | ✓ | — | — | — | — |
| Approve / disburse a payroll run | ✓ | — | — | ✓ | — | — |
| Read own payslips & apply for leave (ESS) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Scopes: own · team · all
Several read permissions come in three widths. The narrower one a role holds determines how much it sees.
| Scope suffix | Sees | Example permission |
|---|---|---|
.own | Only the caller's own records | hr.attendance.read.own |
.team | Records of employees who report to the caller | hr.attendance.read.team |
.all | Every employee in the business | hr.attendance.read.all |
The "team" scope is computed once (employees whose reportsToId resolves to the
manager's own employee record) and reused consistently across attendance,
shifts, and leave.
Permission Catalogue — HRM
| Group | Permissions |
|---|---|
| Org | hr.departments.read · .manage · hr.designations.read · .manage |
| Employees | hr.employees.read · .create · .update · .terminate · .export · .read.sensitive · .update.sensitive · .read.salary · .update.salary |
| Documents & contracts | hr.employees.documents.read · .upload · .delete · hr.contracts.read · .create · .update |
| Attendance | hr.attendance.read.own · .read.team · .read.all · .check-in.manual · .check-out.manual · .regularize.request · .regularize.approve · .regularize.reject · .export · .biometric.read · .biometric.configure |
| Shifts | hr.shifts.read · .template.manage · .schedule.read · .create · .update · .delete · .swap.request.own · .swap.respond · .swap.approve |
| Leave | hr.leave.types.read · .manage · .policies.read · .manage · .balances.read.own · .read.team · .read.all · .adjust · .request.create.own · .create.for · .read.* · .approve · .reject · .cancel.own |
| Holidays | hr.holidays.read · .manage |
| Payroll | hr.payroll.read · .read.payslip.all · .salary-structure.read · .manage · .country-preset.read · .apply · .run.create · .process · .approve · .disburse · .cancel · .payslip.void · .advance.read · .create · .approve · .disburse · .export |
Permission Catalogue — ESS (the ess.* bundle)
Granted to every role (and the only thing the EMPLOYEE role gets).
Permission Catalogue — Notifications
| Permission | Who needs it |
|---|---|
| (none) | Reading/managing your own inbox — list, unread-count, mark-read, read-all — needs no permission. |
notifications.send.manual | Sending a broadcast to a role / store / the whole tenant (admin-level). |
Inspecting a User's Permissions
You don't have to guess — the API can tell you exactly what a caller (or a role) holds.
| Method | Path | Returns |
|---|---|---|
| GET | /api/v1/auth/me | The current user, including a permissions: string[] array. |
| GET | /api/v1/rbac/me/permissions | The caller's effective permissions. |
| GET | /api/v1/rbac/roles | All roles. |
| GET | /api/v1/rbac/roles/:role | The default permission set for one role. |
| GET | /api/v1/rbac/permissions | The full permission catalogue. |
When you add a UI control, gate it on the same permission the backend route
checks (the frontend exposes a usePermissions().can() helper).
That keeps the UI honest — but remember the server still enforces it
regardless. The full endpoint → permission mapping is in the
v2 API Reference.