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:
- Calls
checkLogin, which rejects anonymous callers withUNAUTHORIZED. - Runs
canGroup: owners always pass; otherwise the user's action map is built from theirpermissionGroupIdsandcustomPermissions, cached in Redis underuser_actions_<userId>. Group ids containing:(such ascore:admin) are looked up in each plugin's declareddefaultGroups; other ids arePermissionGroupdocuments fetched over tRPC (module: 'permissionGroups',action: 'find'). - Runs
checkOAuthScope: sessions authenticated with an OAuth token must also hold a scope mapped to the action (oauthScope/oauthScopesin 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"] }
])
}