Roles & Scopes
User permission scopes and how they are enforced on the server and in the app.
Each User has a scope: string[]. There are no role objects — authorization is
a flat set of scope strings. admin is a superuser that bypasses every check.
Scope strings
The editable scope set (app components/user/UserForm.tsx) is:
| Scope | Grants | Server-enforced? |
|---|---|---|
admin | Everything (bypasses all scope checks) | yes (bypass) |
sale | Create sales & queries; vouchers; customer vouchers | yes |
refund | Refund & repay operations | yes |
shift | Open / close shift | yes |
cashio | Cash in / out | yes |
store | Edit store settings (POST /api/store) | yes |
user | Manage users | yes |
interface | Interface / device settings | client-gated (no server route gate found) |
hotkey | Hotkey management | client-gated (hotkey routes have no scopeMiddleware) |
interface and hotkey gate UI access in the app; the corresponding server
routes are open or rely on terminal/user middleware only. Read endpoints like
GET /api/store, GET /api/user/code, GET /api/terminal/me,
GET /api/hotkey are intentionally open.
Enforcement
Server
scopeMiddleware(scope) guards routes
(retail_pos_server/src/v1/user/user.middleware.ts):
export function scopeMiddleware(scope: string) {
return (req, res, next) => {
const user = res.locals.user;
if (!user) throw new UnauthorizedException("Unauthorized");
if (!user.scope.includes("admin") && !user.scope.includes(scope)) {
throw new UnauthorizedException("Insufficient permissions");
}
next();
};
}The user is resolved earlier from the Authorization: Bearer <userId>%%%<lastSignedAt>
token by userMiddleware.
App
The renderer mirrors the same rule with hasScope
(retail_pos_app/src/renderer/src/libs/scope-utils.ts):
export default function hasScope(userScopes: string[], requiredScopes: string[]) {
if (userScopes.includes("admin")) return true;
return requiredScopes.every((scope) => userScopes.includes(scope));
}The app uses this to gate management screens; the server enforces it authoritatively on each request.