Roles & Permissions
How access is decided across the whole console: permissions are seeded in code, roles bundle them, and a user's effective access is simply the union of every permission on every role they currently hold.
The role-only model
There is no way to grant a permission to a person directly — access always flows permission → role → user. This is deliberate: it keeps access auditable (you can always answer "why can they do that?" by looking at their roles) and keeps individual exceptions out of the system.
super_admin role skips every permission check entirely, in every resource.Two access-gate styles exist side by side in the console today. A handful of setup-style resources — including this page's own two resources, plus Warehouses, Geography and most of Customers — check isSuperAdmin() only, with no granular permission string honoured at all, even where one is seeded (e.g. admin.manage_roles, admin.manage_users). Everything else checks isSuperAdmin() || hasPermission('module.action'). Practically: today, only super_admin role holders can reach the pages on this page, regardless of any other permission granted to them.
Roles Built
Nav location: Access Control → Roles & Permissions lists roles first. Access: super-administrator only.
List columns
| Column | Shows |
|---|---|
| Name | The slug identifier, e.g. sales_manager. Searchable, sortable. |
| Display Name | The human-readable label shown everywhere else (attach pickers, badges). Searchable, sortable. |
| System | Check icon when is_system is true — a role the application itself depends on. |
| Permissions | Live count of attached permissions. Sortable. |
| Users | Live count of users currently holding this role. Sortable. |
| Created | Date + time, hidden by default. Sortable. |
There are no filters on this list.
Creating or editing a role
- Select New role (or a row to edit)
- Set the Name (slug)
A short, code-friendly identifier such as
sales_manager— required, max 100, unique. On a system role this field is disabled; you cannot rename it. - Set the Display Name
What people actually see in the console — required, max 150.
- Optionally add a Description
Free text explaining the role's purpose.
- Save
Permissions are not assigned on this form — see the Permissions tab below, which appears once the role exists.
| Field | Type | Required | Notes |
|---|---|---|---|
| name | Text | Yes | Max 100, unique, slug-style. Disabled for system roles. |
| display_name | Text | Yes | Max 150. |
| description | Textarea | No | Full width. |
If is_system is true — for example the super_admin role itself — the Delete button is simply hidden, both on the list row and on the role's own edit page header. This protects roles the application logic depends on from being removed by accident. Its name field is also locked on the edit form for the same reason, though its display name, description and permissions remain editable.
Assigning permissions to a role Built
Open a role for editing and use its Permissions tab. Permissions are never created or edited here — only attached to, or detached from, the role. The table is grouped by module by default, so you can see at a glance how much of one module (e.g. all of billing) a role already covers.
- Open the role, then its Permissions tab
- Select Attach
Opens a picker preloaded with every permission in the system, searchable by name.
- Choose one permission and confirm
Repeat for each permission the role needs — there is no multi-select in the attach picker, and no "assign whole module at once" bulk action. Building a role that should cover, say, all of
billingmeans attaching eachbilling.*permission individually. - Remove a permission with row Detach
Or select several rows and use the bulk Detach action. Detaching never deletes the permission — only the role's grant of it.
Some other systems let you tick a module and grant every permission in it at once. This console does not — each permission is attached one at a time. When building a broad role, work module-by-module using the grouped table as your checklist.
Permissions Built (view-only)
Nav location: Access Control → Roles & Permissions, the Permissions list. Access: super-administrator only. Every permission is seeded in code and always carries is_system = true — there is no way to create, edit or delete a permission from this screen. Its canCreate() check is explicitly hardcoded to always refuse, so you will not even see a New button here.
List columns
| Column | Shows |
|---|---|
| Name | The permission string, e.g. payment.approve. Searchable, sortable. |
| Display Name | Human-readable label, e.g. "Approve Payment". Searchable. |
| Module | Badge — the module this permission belongs to. Sortable. Table is grouped by this column by default. |
| System | Check icon — always on for every permission in this console. |
Filters
A single Module filter, built dynamically from whatever module names actually exist in the database — it is never a hardcoded list, so it always matches reality even as new modules are seeded.
There are no row actions at all on this list — no Edit, no Delete. Select a row to open its view page instead.
The permission's view page
Two read-only sections:
| Section | Shows |
|---|---|
| Permission Details | name, display_name, description, module (badge), and is_system formatted as "System Permission: Yes/No". |
| Assigned to Roles | Every role currently granted this permission, as a comma-separated list of badges — the quickest way to answer "which roles can do this?" for any single permission. |
Permission modules at a glance
Permissions are grouped by a module field. The table below summarises the breadth by module with a few representative examples — it is not the full list. For every seeded permission name, see the Glossary reference page.
| Module | Covers | Examples |
|---|---|---|
admin | Console administration itself. | admin.manage_users, admin.manage_roles, admin.view_system_config |
user | A user acting on their own account. | user.view_self, user.change_password |
zone / region / pincode | The Geography hierarchy. | region.create, pincode.import Planned |
customer | Customer records and their categorisation. | customer.verify, customer.view_financial, customer.merge Planned |
warehouse | Warehouses, stock locations, stock levels, carriers. | warehouse.create, stock_level.edit, delivery_carrier.manage |
inventory | Products, pricing, stock moves/transfers, closing stock. | product.view, closing_stock.audit, stock_transfer.approve Planned |
sales | Sale orders and their credit/discount approval side-flows. | sale_order.approve, sale_order.cancel, credit.approve |
despatch | Picking waves, packages, returns, VPP payment collection. | wave.manage, package.track, vpp_payment.reconcile |
billing | Payment vouchers, bank reconciliation, journals, GST e-invoices, PADs, refunds, charges. | payment.approve, bank_statement.reconcile, gst_einvoice.manage |
reports | Dashboard KPIs and reporting. | dashboard.view, report.view Planned |
A number of seeded permissions — pincode.import, customer.merge, most of stock_move.*/stock_transfer.*, product.create/edit, and others — describe capabilities that have no wired action anywhere in this console yet. Granting one of these on a role does no harm, but it will not make a corresponding button appear; those actions happen via the API, the field app, or are simply not built yet. This page can still assign such a permission to a role in preparation for when the feature ships.
See Users for how roles (with an optional expiry) are attached to a person, and Getting started for the general "why did a button disappear" mental model that permissions and states share.