RBAC & Permissions
Ezvento uses RBAC v2 — a flat, explicit-permission system with no inheritance. Every role has its permissions listed explicitly. There are 8 fixed system roles, ~80 granular permissions, and 6 scope types that govern where a permission applies.
Overview
RBAC v2 replaces the legacy module/action permission model with a flat namespace of explicitly-named permissions. There is no inheritance: if a Store Manager should be able to do everything a Cashier can, every Cashier permission is repeated in the Store Manager list. This makes the permission matrix auditable at a glance.
Key design decisions:
- Flat namespace: Every permission is a single string like
SALE_CREATEorINVENTORY_TRANSFER_APPROVE. No nested groups, no wildcard expansion. - No inheritance: Each role's
permissionsarray is the complete list of what that role can do. There is no "extends" or "inherits from" relationship. - Scope binding: A role assignment includes a
scopeType(e.g. STORE) andscopeIds(e.g. ["store_abc", "store_def"]). The same user can have different roles in different stores. - Approval workflow: High-risk actions (cancellations, refunds, deep discounts) can be blocked unless a manager enters their PIN to generate an
ApprovalRequest. - Snapshot tests lock the matrix: Any unintentional change to role-config.ts fails CI. See
__tests__/lib/rbac/role-permissions.test.ts.
Permission Catalog
All 80 permissions organized by business domain. Each permission follows the NOUN_VERB naming convention.
System Roles
The 8 fixed system roles, their scope, landing page, and permission count. Roles cannot be created or deleted by users — they are seeded at the database level.
Detailed Capabilities per Role
For each role, the exact actions they can and cannot perform. These are derived from the explicit permission lists in src/lib/rbac/role-config.ts.
Scope Types & Hierarchy
A role assignment binds a user to a scope type and a list of scope IDs. The authorizer checks whether the assignment "covers" the target resource using the resolution rules below.
Resolution Rules
- ORG assignments cover any target unconditionally. Owner, Accountant, and Auditor use this scope.
- REGION assignments cover REGION targets by ID, and STORE targets whose
regionIdis in the assignment'sscopeIds. - STORE assignments cover STORE targets by ID, and COUNTER / LOCATION targets that belong to those stores (caller resolves the parent storeId).
- COUNTERassignments cover only the exact counter ID. Cashiers cannot access other counters' data.
- WAREHOUSE assignments cover WAREHOUSE targets by ID, and LOCATION targets whose
warehouseIdis in the assignment'sscopeIds. - LOCATION assignments cover only the exact location ID. Inventory Staff cannot access stock at other locations.
Cross-hierarchy resolution (e.g., "does this STORE assignment cover this COUNTER?") is the caller's responsibility. Pass resolved parent IDs in target.parentScopeIds.
Auth Requirement Helpers
Two helpers enforce RBAC in different contexts: requireApi for API route handlers and requirePageAccess for server components.
Approval Workflow
High-risk actions require a manager to enter their PIN to generate a temporary approval token. The original request is then retried with the X-Approval-Id header.
Approval Flow Steps
- Cashier attempts a high-risk action (e.g., cancel a bill exceeding their DISCOUNT_APPLY_LINE limit).
requireApireturns{ error: "Approval required", code: "APPROVAL_REQUIRED" }with HTTP 403.- Client shows a PIN entry dialog. Cashier asks the on-duty Store Manager to enter their manager PIN.
- Client POSTs
/api/approvalswith{ pin, permission, storeId, reason }. - Server finds eligible approvers with
anyRoleCanApprove(), validates the PIN againstmanagerPinhash, and creates anApprovalRequestrow with statusAPPROVEDand a 5-minute expiry. - Client retries the original API call with header
X-Approval-Id: <approvalId>. requireApiwithallowApprovalOverride: truevalidates the approval, marks itUSED, and allows the request.
Platform Admin
Platform admin is completely separate from tenant RBAC. It is for the SaaS operator (Ezvento team) to manage tenants, view billing, and administer the platform.
Security note: Returning 404 instead of 403 on unauthorized admin access is intentional. A 403 would confirm that an admin panel exists, making it a target for enumeration attacks. A 404 response makes the surface indistinguishable from a non-existent route.