Roles & Permissions
Team / org roles
These roles are assigned per Member record (user in a team):
| Role | Level | Capabilities |
|---|---|---|
| SUPERADMIN | Platform | Platform-wide admin |
| OWNER | Org | Full control, billing, delete org |
| ADMIN | Org | User management, template configuration, all applications |
| MANAGER | Workspace | Application management, document review, client management |
| MEMBER | Team | Process applications, add comments |
| CLIENT | Workspace | See own applications + EXTERNAL comments only |
Per-application roles (Applicant roles)
Each Applicant on an application has an independent role:
| Role | Capabilities |
|---|---|
| VIEWER | Read-only access to the application |
| COMMENTER | Can add comments |
| EDITOR | Can edit application details |
These are separate from the workspace-level team roles.
Cascading permissions
Permissions cascade through the hierarchy: team → workspace → org.
A user with a role on an org workspace implicitly has that role on all teams within that workspace. The evaluator checks: team role → workspace role → org role.
Custom evaluator: AxveroRolePermissionEvaluator (backend)
hasUserAnyOrgRole(id, roles...)— org-level checkhasUserAnyTeamRole(id, roles...)— team-level checkhasUserAnyRoleForTeam(id, roles...)— cascading check (team → workspace → org)
Data scoping by role
This is a critical business rule — what each role sees:
| Data | Staff (OWNER/ADMIN/MANAGER/MEMBER) | Client (CLIENT) |
|---|---|---|
| Applications | All applications in workspace | Only their own applications |
| Comments | INTERNAL + EXTERNAL | EXTERNAL only |
| Applicant details | All | Their own only |
| Client list | All clients | Not accessible |
Implementation: In the backend service, resolve currentUserId in the controller and pass to service. Service calls ClientService.findClientIdByUserIdAndWorkspaceId(currentUserId, workspaceId). If a client ID is found, scope results to that client's data.
Never show INTERNAL comments to clients. This is enforced at the GraphQL query level — type filtering happens before returning comment data.
Permission annotations (backend)
Read guards — membership checks (unchanged)
// Any workspace member (read access)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAnyWorkspaceRole(#workspaceId, 'OWNER','ADMIN','MANAGER','MEMBER','CLIENT')")
// Org-level read
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAnyOrgRole(#orgId, 'OWNER','ADMIN','MANAGER','MEMBER')")
// Workspace or client role (mixed workspaces)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAnyWorkspaceOrClientRole(#workspaceId, #clientId)")
Write guards — permission matrix checks
Write and mutate operations use the configurable permission matrix instead of hardcoded role lists:
// System permission (org-level operation)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserSystemPermission(#orgId, 'MANAGE_TEAMS')")
// System permission resolved from workspaceId
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserSystemPermission(@workspaceRepository.findOrgIdByWorkspaceId(#workspaceId), 'MANAGE_APPLICATION_SETUP')")
// Application permission (action inside an application)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAppPermissionForApplication(#applicationId, 'DECIDE')")
All GraphQL controllers carry @PreAuthorize("isAuthenticated()") at the class level.
Automatic creator membership
When creating entities (teams, workspaces), the creator is automatically added as OWNER member. This must always be implemented when adding new create operations.
See also
- Permission Matrices — configurable system & application permission matrices, default values, UI access
- Developer guide: Permission matrices — entities, services, @PreAuthorize patterns, frontend hooks
- Backend Rules: Security
- Data Model