# Organizations, Users & Permissions

Tenants, team members, structure, and RBAC


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](https://erxes.io/docs/api-authentication). For a full plugin permission declaration example, see [Plugin Metadata Extensions](https://erxes.io/docs/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.

```text
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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/users/db/definitions/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` `_id`s |
| `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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/utils.ts)) injects `checkPermission` into every resolver context. A resolver calls it with the action name:

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

`checkPermission` is `checkPermissionGroup(subdomain, user)` from [permissions/utils.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/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):

```ts
// 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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/modules/settings/team-member/components/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

```graphql
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: "dev@example.com", password: "Temp#1234", permissionGroupIds: ["core:admin"] }
  ])
}
```

