Organizations, Users & Permissions

Manage team members, organization structure (branches, departments, units, positions), brands, and the role-based access control that every plugin enforces.

For sign-in flows and tokens, see Authentication. For a full plugin permission declaration example, see Plugin Metadata Extensions.

Module layout

Users, structure, brands, and settings all live under modules/organization/ in core-api. RBAC is a separate modules/permissions/ module. The User Mongoose schema is shared through erxes-api-shared so plugins can use the same model.

backend/core-api/src/modules/
  organization/
    routes.ts                    # /initial-setup, /get-frontend-plugins,
                                 # /sso-callback, /ml-callback, /core-login
    team-member/
      graphql/{schema,queries,mutations}.ts
      db/models/Users.ts         # model over the shared user schema
      trpc/user.ts               # users.find, findOne, create, updateOne,
                                 # updateMany, setActiveStatus, getCount,
                                 # comparePassword, checkLoginAuth
      meta/activity-log/         # member change, login, invitation events
      meta/import-export/        # CSV headers, row processors
    structure/
      graphql/schemas/{branch,department,position,units,structure}.ts
      graphql/resolvers/{queries,mutations,customResolvers}/
      db/definitions/structure.ts, db/models/Structure.ts
      trpc/{branch,department,unit,index}.ts
    brand/graphql/{schema,queries,mutations}.ts + trpc/brand.ts
    settings/                    # configs + favorites
    whitelabel/                  # orgWhiteLabel definitions/models
  permissions/
    graphql/schemas/permission.ts, resolvers/{queries,mutations}
    db/definitions/permissions.ts, db/models/Permissions.ts
    trpc/permission.ts           # permissionGroups.find
  auth/                          # login, password reset, magic link, OAuth apps
backend/core-api/src/meta/permissions.ts   # core's IPermissionConfig

Frontend: frontend/core-ui/src/modules/settings/team-member/, settings/structure/, settings/permissions/, settings/brands/, plus reusable pickers in frontend/libs/ui-modules/src/modules/team-members/ and structure/.

Data model

User (users.ts):

FieldNotes
email, username, passwordLogin identity; password is optional (invitations)
isActive, isOwnerOwner bypasses all permission checks
detailsfirstName, lastName, fullName, avatar, position, employeeId, …
permissionGroupIdsMix of default group ids (core:admin) and custom PermissionGroup _ids
customPermissionsPer-user overrides outside any group
branchIds, departmentIds, positionIds, unitIdStructure scoping
brandIdsBrand scoping for forms and inbox content
customFieldsData, links, emailSignaturesExtended profile data

Structure documents (structure/db/definitions/structure.ts) share one schema pattern: title, code, parentId, supervisorId, userIds, status. PermissionGroup (permissions/db/definitions/permissions.ts) holds name, description, and permissions[] of { plugin, module, actions, scope }.

GraphQL API

Team members (team-member/graphql/schema.ts)

OperationKey arguments
userssearchValue, isActive, ids, brandIds, departmentId, branchId, branchIds, departmentIds, unitId, segment, isAssignee, status, sortField, excludeIds, cursor params; returns UsersListResponse
allUsersisActive, ids, assignedToMe, searchValue; unpaginated [User]
userDetail_id
usersTotalCountsame selector args as users
userMovementsuserId!, contentType
usersInviteentries: [InvitationEntry] (email, password, permissionGroupIds)
usersEdit / usersEditProfile_id! plus member fields / username!, email!, details, links, employeeId, positionIds
usersResetMemberPassword_id!, newPassword!
usersSetActiveStatus / usersSetActiveStatusBatch_id! / _ids: [String!]!
usersResendInvitation / usersConfirmInvitationemail! / token
usersCreateOwneremail!, password!, firstName!, lastName, purpose, subscribeEmail; first-account bootstrap

Structure (structure/graphql/schemas/*)

OperationKey arguments
branches / branchesMain / branchDetailsearchValue, ids, withoutUserFilter / _id!
departments / departmentsMain / departmentDetailsame pattern
units / unitsMain / unitDetailsearchValue / _id!
positions / positionsMain / positionDetailsearchValue, ids / _id
structureDetailnone; returns the single org structure record
branchesAdd, departmentsAdd, unitsAdd, positionsAdd, structuresAddtitle, code, parentId, supervisorId, userIds, status, plus type-specific fields
*sEdit / *sRemove_id! / ids: [String!]

cp* variants (cpBranches, cpUnits, cpDepartments, cpCustomers, …) expose the same records to client-portal contexts.

Permissions (permissions/graphql/schemas/permission.ts)

OperationKey arguments
permissionModulesnone; returns every plugin's declared modules and actions
permissionDefaultGroupsnone; returns declared defaultGroups with resolved members
permissionGroups / permissionGroupDetailnone / id!
currentUserPermissionsnone; returns effective permissions for the caller
permissionGroupAdd / permissionGroupEditname!, description, permissions: [PermissionInput]! / _id! plus the same optional fields
permissionGroupRemove_id!
userUpdatePermissionGroups / usersUpdatePermissionGroupsuserId!, groupIds: [String]! / userIds: [String]!, groupIds
userAddCustomPermission / userRemoveCustomPermissionuserId!, permission: PermissionInput! / userId!, module!

How RBAC is enforced

generateApolloContext (apollo/utils.ts) injects checkPermission into every resolver context. A resolver calls it with the action name:

// backend/core-api/src/modules/contacts/graphql/resolvers/mutations/company.ts
await checkPermission('contactsCreate');

checkPermission is checkPermissionGroup(subdomain, user) from permissions/utils.ts. It:

  1. Calls checkLogin, which rejects anonymous callers with UNAUTHORIZED.
  2. Runs canGroup: owners always pass; otherwise the user's action map is built from their permissionGroupIds and customPermissions, cached in Redis under user_actions_<userId>. Group ids containing : (such as core:admin) are looked up in each plugin's declared defaultGroups; other ids are PermissionGroup documents fetched over tRPC (module: 'permissionGroups', action: 'find').
  3. Runs checkOAuthScope: sessions authenticated with an OAuth token must also hold a scope mapped to the action (oauthScope/oauthScopes in the config, else <plugin>-<module>:manage). Cookie sessions skip this check.

wrapPermission (login only) and wrapPublicResolver (forClientPortal, cpUserRequired) wrap resolvers that need weaker checks. clearGroupActionsCache invalidates the Redis action map when groups or users change.

Declaring permissions in a plugin

Each service exports an IPermissionConfig from src/meta/permissions.ts and passes it to startPlugin({ meta: { permissions } }). This is the Sales plugin's declaration (trimmed):

// backend/plugins/sales_api/src/meta/permissions.ts
export const permissions: IPermissionConfig = {
  plugin: 'sales',
  modules: [
    {
      name: 'deal',
      description: 'Deals management',
      scopeField: null,
      ownerFields: ['userId', 'assignedUserIds'],
      scopes: [
        { name: 'own', description: 'Deals created by or assigned to user' },
        { name: 'group', description: 'Deals in user departments' },
        { name: 'all', description: 'All deals' },
      ],
      actions: [
        { title: 'View deals', name: 'showDeals', description: 'View deals', always: true },
        { title: 'Add deals', name: 'dealsAdd', description: 'Create deals' },
      ],
    },
  ],
  defaultGroups: [
    {
      id: 'sales:admin',
      name: 'Sales Admin',
      description: 'Full access to Sales plugin',
      permissions: [
        { plugin: 'sales', module: 'deal', actions: ['showDeals', 'dealsAdd', 'dealsEdit'], scope: 'all' },
      ],
    },
  ],
};

Default groups are not seeded into MongoDB. They are read live from the plugin registry: permissionDefaultGroups reads config.meta.permissions.defaultGroups from every active plugin, and getGroupActionsMap grants the group's actions to any user whose permissionGroupIds contains the group id. Actions marked always: true are pre-checked in the permission UI; they are not an automatic grant.

Core's own declaration lives at src/meta/permissions.ts: modules contacts, products, properties, tags, segments, documents, brands, organization, teamMembers, broadcasts, importExport, bundle, permissions, apps, clientPortal, approval, automations, logs, internalNotes, with a core:admin default group.

Member email activity log

The team-member record table's "Activity log" sheet (TeamMemberActivityLogSheet.tsx, opened from TeamMemberMoreColumn) shows the member's outbound email activity: Messages (EmailDeliveriesRecordTable) and Addresses (EmailAddressesRecordTable) filtered by their email address. It is backed by the emailDeliveries and emailAddresses queries in the notifications module, not by a separate audit collection. Separately, team-member/meta/activity-log/ sends member profile changes, logins, and invitations to the shared activity-log pipeline (userMovements exposes structure/position moves).

Example

query Members($searchValue: String) {
  users(searchValue: $searchValue, isActive: true, limit: 20) {
    list {
      _id
      email
      details { fullName position }
      branchIds
      permissionGroupIds
    }
    totalCount
    pageInfo { hasNextPage endCursor }
  }
}

mutation Invite {
  usersInvite(entries: [
    { email: "[email protected]", password: "Temp#1234", permissionGroupIds: ["core:admin"] }
  ])
}
Was this helpful?