> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# People

> People is a workspace-scoped entity representing an individual contact — an employee, owner, or other relationship a business interacts with

People is a workspace-scoped entity representing an individual contact — an employee, owner, or other relationship a business interacts with. It is the central node for personal contact data, linked to atomic contact details (emails, phones, locations, web links) through dedicated pivot entities. A People record is connected to one or more Company entities via the CompanyPerson pivot, participates in workspace Membership, and tracks its data-source provenance through the PeopleWorkspaceConnector relation. It is a primary records-page root exposed in the data-views pipeline.

| Naming                          | Value        |
| ------------------------------- | ------------ |
| Object                          | People       |
| Resource type (JSON:API `type`) | `people`     |
| Collection / records root       | `people`     |
| REST base                       | `/v1/people` |
| Entity class                    | `People`     |

## API operations

| Operation | Method & path            | Status        |
| --------- | ------------------------ | ------------- |
| List      | `GET /v1/people`         | ✅ Implemented |
| Retrieve  | `GET /v1/people/{id}`    | ✅ Implemented |
| Create    | `POST /v1/people`        | ✅ Implemented |
| Update    | `PATCH /v1/people/{id}`  | ✅ Implemented |
| Delete    | `DELETE /v1/people/{id}` | ✅ Implemented |
| Enrich    | `POST /v1/people/enrich` | ✅ Implemented |

## Data model

### Attributes

| Field                | Type            | Required | Constraints                                                                                                                                                                                                   | Allowed values | Description                                                                                                                                                                                                                                                                                                              |
| -------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| person\_id           | string, UUID    | ✅ Yes    | unique; generated by gen\_random\_uuid() on insert                                                                                                                                                            | —              | Public stable identifier for this person record. Used in all API responses and external references. Never expose the internal pk.                                                                                                                                                                                        |
| full\_name           | string          | ✅ Yes    | varchar(255), not null                                                                                                                                                                                        | —              | Display name for the person. Stored directly on the entity (not derived at query time from first\_name + last\_name, though both component fields are also persisted). Used as the primary display label across the records UI and composites.                                                                           |
| first\_name          | string          | ✅ Yes    | varchar(255), not null                                                                                                                                                                                        | —              | Given (first) name component. Persisted alongside full\_name to enable name-part filtering and salutation formatting.                                                                                                                                                                                                    |
| last\_name           | string          | ✅ Yes    | varchar(255), not null                                                                                                                                                                                        | —              | Family (last) name component. Persisted alongside full\_name.                                                                                                                                                                                                                                                            |
| job\_title           | string          | ⚪ No     | varchar(100), nullable                                                                                                                                                                                        | —              | Professional role or title of the person at their primary company. Decorated with @Enrichable — the enrichment pipeline may populate or update this field automatically.                                                                                                                                                 |
| external\_person\_id | string          | ⚪ No     | nullable; partial unique index on (external\_person\_id, workspace\_pk) WHERE deleted\_at IS NULL — replaces original global unique constraint (Migration20260216120000). Uniqueness is scoped per workspace. | —              | Deduplication key from the originating connector (e.g. a CRM contact ID). Used by the reconciliation pipeline for find-or-create matching: if a sync supplies an external\_person\_id already known in the workspace, the existing record is updated rather than a duplicate created. NULL for manually created records. |
| created\_at          | Date, 🔒 system | ✅ Yes    | timestamptz, not null; set via onCreate lifecycle hook                                                                                                                                                        | —              | Timestamp of record creation. Set automatically by MikroORM on first persist; never updated.                                                                                                                                                                                                                             |
| updated\_at          | Date, 🔒 system | ⚪ No     | timestamptz, nullable; set via onCreate and onUpdate lifecycle hooks                                                                                                                                          | —              | Timestamp of last modification. Set on create and updated on every subsequent write by MikroORM.                                                                                                                                                                                                                         |
| deleted\_at          | Date            | ⚪ No     | timestamptz, nullable; null = active record                                                                                                                                                                   | —              | Soft-delete timestamp. When set, the record is treated as deleted across all queries and is excluded from the partial unique index on external\_person\_id. Never hard-deleted. All repositories must filter deleted\_at: null.                                                                                          |

### Relationships

| Name                          | Type                               | Required                  | Description                                                                                                                                                                                                                                                                                                         |
| ----------------------------- | ---------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| workspace                     | to-one (Workspace)                 | ⚪ No (nullable ManyToOne) | The workspace that owns this person record. Provides multi-tenant scope: all repository queries filter by workspace. Nullable to allow global catalog entries, though in practice all active People records have a workspace.                                                                                       |
| source\_workspace\_connector  | to-one (WorkspaceConnector)        | ⚪ No (nullable ManyToOne) | The WorkspaceConnector instance that created or last synced this record. NULL for manually created people. Used by the pipeline to track provenance and to surface the connector-logo-name composite in the data-views records page.                                                                                |
| media                         | to-one (Media)                     | ⚪ No (nullable ManyToOne) | Profile photo / avatar for this person. Linked via a direct ManyToOne to the shared Media entity. The overrides.yml marks media.url as editable with display\_type: image; write goes through add/delete join-entity mutations, not a scalar PATCH.                                                                 |
| emails                        | to-many (PersonEmail)              | ⚪ No                      | Email addresses linked to this person through the PersonEmail pivot entity. Each pivot record carries is\_primary, is\_verify, and label (work/personal/other). A partial unique index ensures at most one primary email per person (WHERE deleted\_at IS NULL AND is\_primary IS TRUE).                            |
| phones                        | to-many (PersonPhone)              | ⚪ No                      | Phone numbers linked through the PersonPhone pivot entity. Each pivot record carries is\_primary, is\_verify, and label (mobile/work/personal/other). A partial unique index enforces at most one primary phone per person. Decorated @Enrichable on the People side — enrichment pipeline may populate phone data. |
| locations                     | to-many (PersonLocation)           | ⚪ No                      | Geographic locations linked through the PersonLocation pivot entity. Each pivot record carries is\_primary, is\_legal, and label. A partial unique index enforces at most one primary location per person.                                                                                                          |
| web\_links                    | to-many (PersonWebLink)            | ⚪ No                      | Social profile or other web links associated with this person, linked through the PersonWebLink pivot entity. Decorated @Enrichable on the People side — enrichment pipeline may populate social links (e.g. LinkedIn).                                                                                             |
| companies                     | to-many (CompanyPerson)            | ⚪ No                      | Company associations for this person, modelled through the CompanyPerson pivot entity. Each pivot record carries a relationship\_type enum (contact / employee / owner / other, default: other). A person may be associated with multiple companies across workspaces.                                              |
| memberships                   | to-many (Membership)               | ⚪ No                      | Workspace membership records for this person. The Membership entity links People to a Workspace with role and status information for platform access control.                                                                                                                                                       |
| collect                       | to-many (Collect)                  | ⚪ No                      | Collect (document collection) records associated with this person. Links the People entity into the document collection pipeline.                                                                                                                                                                                   |
| workspace\_connectors         | to-many (WorkspaceConnector)       | ⚪ No                      | WorkspaceConnector records whose person field points to this People row. Used to associate a connector instance with the authenticated platform user who installed or owns it.                                                                                                                                      |
| people\_workspace\_connectors | to-many (PeopleWorkspaceConnector) | ⚪ No                      | Per-row provenance records linking this People entity to specific WorkspaceConnector instances that have synced it. Each PeopleWorkspaceConnector record carries a direction enum (inbound/outbound) and its own created\_at / updated\_at / deleted\_at. Analogous to CompanyWorkspaceConnector for Companies.     |

### System-computed

* person\_id is generated by PostgreSQL gen\_random\_uuid() as a column default; unique constraint peoples\_person\_id\_unique enforced at the DB level.
* created\_at is set via MikroORM onCreate lifecycle hook (new Date()); never updated after first persist.
* updated\_at is set on both onCreate and onUpdate via MikroORM lifecycle hooks; reflects the last modification timestamp.
* deleted\_at is null for active records; set to a timestamp on soft-delete. The partial unique index on (external\_person\_id, workspace\_pk) excludes rows where deleted\_at IS NOT NULL, allowing soft-deleted records to be re-created with the same external\_person\_id.
* external\_person\_id is the deduplication key for connector-driven sync: if a sync provides an external\_person\_id already present in the workspace (under the partial unique constraint), the reconciliation pipeline updates the existing record rather than creating a new one.
* sourceWorkspaceConnector is set by the sync pipeline at creation time to record which WorkspaceConnector produced this record; NULL for manually created people.
* The @Enrichable decorator on job\_title, phones, and webLinks marks these fields as candidates for AI enrichment via the enrichment pipeline (Cloud Tasks workers).
* Two partial indexes are maintained by migrations for the records-page query path: idx\_peoples\_workspace\_deleted (workspace\_pk WHERE deleted\_at IS NULL) and idx\_peoples\_workspace\_created\_active (workspace\_pk, created\_at DESC WHERE deleted\_at IS NULL) for default sort order.
* The composite\_avatar\_fullname composite field (source\_fields: person\_id, media.url, full\_name; display\_type: people\_avatar\_name) is materialized at query time by the data-views pipeline — it is not a persisted column.
* The sourceWorkspaceConnector.composite\_connector\_logo\_name composite (source\_fields: workspace\_connector\_id, connector.service\_id, connector.name; display\_type: connector\_logo\_name) is likewise a query-time composite on the people records root.

## Example

```json theme={null}
{
  "data": {
    "type": "people",
    "id": "a3f1c2d4-7e89-4b10-bcd2-1f234567890a",
    "attributes": {
      "person_id": "a3f1c2d4-7e89-4b10-bcd2-1f234567890a",
      "full_name": "Sophie Marotremy",
      "first_name": "Sophie",
      "last_name": "Marotremy",
      "job_title": "Head of Finance",
      "external_person_id": "crm_contact_0049281",
      "created_at": "2025-11-03T09:14:22.000Z",
      "updated_at": "2026-03-17T14:55:10.000Z",
      "deleted_at": null
    },
    "relationships": {
      "workspace": {
        "data": { "type": "workspace", "id": "f9e2a1b3-0000-4c2d-8888-aabbccddeeff" }
      },
      "source_workspace_connector": {
        "data": { "type": "workspace_connector", "id": "7a123456-dead-beef-cafe-000000000001" }
      },
      "media": {
        "data": { "type": "media", "id": "bb887766-5544-3322-1100-aabbccddeeff" }
      },
      "emails": {
        "data": [
          { "type": "person_email", "id": "11223344-aaaa-bbbb-cccc-000000000001" }
        ]
      },
      "phones": {
        "data": [
          { "type": "person_phone", "id": "55667788-aaaa-bbbb-cccc-000000000001" }
        ]
      },
      "locations": {
        "data": [
          { "type": "person_location", "id": "99aabbcc-aaaa-bbbb-cccc-000000000001" }
        ]
      },
      "web_links": {
        "data": [
          { "type": "person_web_link", "id": "ddeeff00-aaaa-bbbb-cccc-000000000001" }
        ]
      },
      "companies": {
        "data": [
          { "type": "company_person", "id": "fa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" }
        ]
      },
      "memberships": {
        "data": []
      }
    }
  }
}
```

<sub>Source: `apps/api/src/database/entities/People.ts` · domain: financial-graph · tier: Main</sub>
