# Contacts

Customers and companies with tags and segments


Store customers and companies with tags, segments, custom fields, and links to plugin records.

For custom attributes, see [Properties, Tags & Segments](https://erxes.io/docs/core-properties-tags-segments). For bulk loads, see [Import & Export](https://erxes.io/docs/core-import-export).

## Module layout

```text
backend/core-api/src/modules/contacts/
  graphql/schemas/{customer,company}.ts
  graphql/resolvers/{queries,mutations,customResolvers}/
  db/definitions/{customers,company}.ts
  db/models/{Customers,Companies}.ts
  trpc/{customer,company,index}.ts      # service-to-service access
  meta/activity-log/{customers,companies}/
  meta/import-export/{import,export}/   # CSV handlers per entity
  constants.ts, utils.ts, @types/customer.ts
```

Linking modules alongside it: `modules/conformities/` (legacy type-pair links) and `modules/relations/` (generic entity-to-entity edges). Frontend lives in `frontend/core-ui/src/modules/contacts/{customers,companies,client-portal-users}` with shared pickers in `frontend/libs/ui-modules/src/modules/contacts/`.

## Data model

`Customer` ([customers.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/contacts/db/definitions/customers.ts)):

| Field | Notes |
| ----- | ----- |
| `state` | `visitor` by default; promoted to `lead`/`customer` |
| `firstName`, `lastName`, `middleName`, `primaryEmail`, `emails`, `primaryPhone`, `phones`, `code` | Identity and contact points |
| `emailValidationStatus`, `phoneValidationStatus` | `unknown` until verified |
| `visitorContactInfo`, `trackedData`, `location` | Visitor/session capture |
| `leadStatus`, `status`, `isSubscribed`, `ownerId`, `position`, `department` | Pipeline and ownership |
| `tagIds`, `customFieldsData`, `propertiesData`, `links` | Labels and custom data |
| `searchText`, `searchTokens` | Maintained by the search token config (below) |

`Company` (`db/definitions/company.ts`): `primaryName`, `names`, `code`, `size`, `industry`, `businessType`, `website`, `primaryEmail`, `primaryPhone`, `addresses`, `parentCompanyId`, `score`, `doNotDisturb`, `mergedIds`, plus the same `tagIds`, `customFieldsData`, `propertiesData`, `trackedData`, `ownerId`, `status` pattern.

## Search tokens

`customerSearchTokenConfig` in `db/definitions/customers.ts` controls partial-match search. Each entry names a `path`, a `mode` (`prefix`, `word-prefix`, `exact`), and a `minLength`:

| Path | Mode | Min length |
| ---- | ---- | ---------- |
| `firstName`, `middleName`, `lastName` | `prefix` | 2 |
| `primaryEmail`, `emails` | `word-prefix` | 3 |
| `primaryPhone`, `phones` | `word-prefix` | 1 |
| `code` | `exact` | 1 |
| `visitorContactInfo.email` | `word-prefix` | 3 |
| `visitorContactInfo.phone` | `word-prefix` | 1 |

Tokens are materialized into `searchTokens` (indexed with `state` and `status`), so `searchValue` filters stay MongoDB queries. Inputs shorter than `minLength` do not match, so a too-short search can look like it finds nothing.

## GraphQL API

### Customers (`graphql/schemas/customer.ts`)

| Operation | Key arguments |
| --------- | ------------- |
| `customers` | `searchValue`, `type`, `ids`, `excludeIds`, `tagIds`, `excludeTagIds`, `tagWithRelated`, `segment`, `segmentIds`, `brandIds`, `integrationIds`, `integrationTypes`, `clientPortalId`, `formIds`, `leadStatus`, `status`, `sex`, `birthDate`, `dateFilters`, `propertiesData`, `emailValidationStatus`, `sortField`, `sortDirection`, `autoCompletion`, `autoCompletionType`, `startDate`, `endDate`, conformity args, cursor params; returns `CustomersListResponse` |
| `customerDetail` | `_id!` |
| `customersCount` | `types: [CUSTOMER_RELATION_TYPE]` |
| `contactsLogs` | `action`, `content`, `contentType` |
| `cpCustomers` / `cpCustomerDetail` | client-portal variants |
| `customersAdd` / `customersEdit` | `state`, `firstName`, `lastName`, `primaryEmail`, `emails`, `primaryPhone`, `phones`, `primaryAddress`, `addresses`, `ownerId`, `position`, `department`, `leadStatus`, `description`, `links`, `code`, `sex`, `birthDate`, `avatar`, `isSubscribed`, `propertiesData`, validation statuses |
| `customersMerge` | `customerIds`, `customerFields` |
| `customersRemove` | `customerIds` |
| `customersChangeState` / `customersChangeStateBulk` | `_id!`, `value!` / `_ids: [String!]!`, `value!` |
| `customersVerify`, `customersChangeVerificationStatus` | `verificationType!` / `customerIds`, `type!`, `status!` |

### Companies (`graphql/schemas/company.ts`)

| Operation | Key arguments |
| --------- | ------------- |
| `companies` | same selector family as `customers`; returns `CompaniesListResponse` |
| `companyDetail` | `_id!` |
| `companiesAdd` / `companiesEdit` | `primaryName`, `names`, `avatar`, `size`, `industry`, `businessType`, `website`, `primaryEmail`, `emails`, `primaryPhone`, `phones`, `primaryAddress`, `addresses`, `parentCompanyId`, `code`, `location`, `ownerId`, `tagIds`, `propertiesData`, `links`, `isSubscribed` |
| `companiesMerge` | `companyIds`, `companyFields` |
| `companiesRemove` | `companyIds` |

### Linking records

- **Conformities** (`modules/conformities`): `conformityAdd`, `conformityEdit` mutations store main-type/rel-type pairs. Filter contacts with `conformityMainType`, `conformityMainTypeId`, `conformityRelType`, `conformityIsRelated`, `conformityIsSaved` on the list queries.
- **Relations** ([modules/relations](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/relations/graphql/schema.ts)): generic `entities: [{ contentType, contentId }]` edges. `createRelation`, `manageRelations`, `getRelationsByEntity` let plugins link contacts to deals, tickets, or their own types without touching the contact document.

List queries return a cursor envelope `{ list, pageInfo { hasNextPage, endCursor }, totalCount }`, controlled by `limit`, `cursor`, `cursorMode`, `direction`, `orderBy`.

## Example

```graphql
query CustomersByTag($tagIds: [String], $cursor: String) {
  customers(tagIds: $tagIds, cursor: $cursor, limit: 20) {
    list {
      _id
      firstName
      primaryEmail
      tagIds
      customFieldsData
      cursor
    }
    totalCount
    pageInfo { hasNextPage endCursor }
  }
}

mutation AddCustomer {
  customersAdd(
    firstName: "Ari"
    primaryEmail: "ari@example.com"
    primaryPhone: "+15551234567"
    code: "CUST-001"
  ) { _id state }
}
```

## tRPC access

Other services reach contacts through `sendTRPCMessage({ pluginName: 'core', module: 'customers', action: ... })`. Procedures on [trpc/customer.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/contacts/trpc/customer.ts) include `find`, `findOne`, `findActiveCustomers`, `getCustomerName`, `getWidgetCustomer`, `count`, `createCustomer`, `updateCustomer`, `updateOne`, `updateMany`, `removeCustomers`, `markCustomerAsActive`, `createMessengerCustomer`, `updateMessengerCustomer`, `saveVisitorContactInfo`, `updateLocation`, `updateSession`, `setUnsubscribed`, `createOrUpdate`, `tag`. The company router mirrors the core CRUD set.

## Permissions

Mutations check core actions from `meta/permissions.ts`: `contactsCreate`, `contactsUpdate`, `contactsDelete`, `contactsMerge`, `customersImportManage`, `customersExportManage`, `companiesImportManage`, `companiesExportManage`. `contactsRead` is marked `always: true` and supports scopes `own` (records in `createdBy`/`ownerId`) and `all`.

