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):

Field Notes
email, username, password Login identity; password is optional (invitations)
isActive, isOwner Owner bypasses all permission checks
details firstName, lastName, fullName, avatar, position, employeeId, …
permissionGroupIds Mix of default group ids (core:admin) and custom PermissionGroup _ids
customPermissions Per-user overrides outside any group
branchIds, departmentIds, positionIds, unitId Structure scoping
brandIds Brand scoping for forms and inbox content
customFieldsData, links, emailSignatures Extended 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)

Operation Key arguments
users searchValue, isActive, ids, brandIds, departmentId, branchId, branchIds, departmentIds, unitId, segment, isAssignee, status, sortField, excludeIds, cursor params; returns UsersListResponse
allUsers isActive, ids, assignedToMe, searchValue; unpaginated [User]
userDetail _id
usersTotalCount same selector args as users
userMovements userId!, contentType
usersInvite entries: [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 / usersConfirmInvitation email! / token
usersCreateOwner email!, password!, firstName!, lastName, purpose, subscribeEmail; first-account bootstrap

Structure (structure/graphql/schemas/*)

Operation Key arguments
branches / branchesMain / branchDetail searchValue, ids, withoutUserFilter / _id!
departments / departmentsMain / departmentDetail same pattern
units / unitsMain / unitDetail searchValue / _id!
positions / positionsMain / positionDetail searchValue, ids / _id
structureDetail none; returns the single org structure record
branchesAdd, departmentsAdd, unitsAdd, positionsAdd, structuresAdd title, 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)

Operation Key arguments
permissionModules none; returns every plugin's declared modules and actions
permissionDefaultGroups none; returns declared defaultGroups with resolved members
permissionGroups / permissionGroupDetail none / id!
currentUserPermissions none; returns effective permissions for the caller
permissionGroupAdd / permissionGroupEdit name!, description, permissions: [PermissionInput]! / _id! plus the same optional fields
permissionGroupRemove _id!
userUpdatePermissionGroups / usersUpdatePermissionGroups userId!, groupIds: [String]! / userIds: [String]!, groupIds
userAddCustomPermission / userRemoveCustomPermission userId!, 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"] }
  ])
}

Operate on one system.

Talk to our team about consolidating your stack onto erxes. 30-minute walkthrough, custom to your org.