Skip to main content

Roles & Permissions

Updated (2026-06)

The access model was redesigned. A team now maps to exactly one workspace (1:1); a user's effective permissions in a workspace are the union of all roles they hold there, looked up in a single organization-wide capability matrix. Owners are a narrow billing/ownership role (not all-powerful); they administer workspaces via an automatic admin membership. Per-role capabilities and the new Access Tiers (need-to-know application locks) are configured on the Roles and Access Tiers pages. Implementation detail lives in ai/claude/rules/access-model.md.

Team / org roles​

These roles are assigned per Member record (user in a team):

RoleLevelCapabilities
SUPERADMINPlatformPlatform-wide admin
OWNEROrgFull control, billing, delete org
ADMINOrgUser management, template configuration, all applications
MANAGERWorkspaceApplication management, document review, client management
MEMBERTeamProcess applications, add comments
CLIENTWorkspaceSee own applications + EXTERNAL comments only

Per-application roles (Applicant roles)​

Each Applicant on an application has an independent role:

RoleCapabilities
VIEWERRead-only access to the application
COMMENTERCan add comments
EDITORCan 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 check
  • hasUserAnyTeamRole(id, roles...) — team-level check
  • hasUserAnyRoleForTeam(id, roles...) — cascading check (team → workspace → org)

Data scoping by role​

This is a critical business rule — what each role sees:

DataStaff (OWNER/ADMIN/MANAGER/MEMBER)Client (CLIENT)
ApplicationsAll applications in workspaceOnly their own applications
CommentsINTERNAL + EXTERNALEXTERNAL only
Applicant detailsAllTheir own only
Client listAll clientsNot 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.

warning

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​