PersonPhone
PersonPhone is a pivot entity that links a People record to a Phone record within a workspace, carrying metadata about the association. It implements the canonical pivot pattern with is_primary, is_verify, and label fields so a single person can hold multiple phone numbers under different categories. Records are written by the connector sync pipeline and by user-facing People mutations (add/remove phone); there is no standalone PATCH surface for individual person_phones rows. Soft-delete semantics via deleted_at are enforced, and partial unique indexes guarantee at most one primary phone per person at any time.
| Naming | Value |
|---|---|
| Object | PersonPhone |
Resource type (JSON:API type) | person_phone |
| Collection / records root | โ (not a records root) |
| REST base | /v1/person-phones |
| Entity class | PersonPhone |
Internal object. Not currently exposed on the public REST API. The operations below describe the intended contract.
API operations
| Operation | Method & path | Status |
|---|---|---|
| List | GET /v1/person-phones | ๐ก Planned |
| Retrieve | GET /v1/person-phones/{id} | ๐ก Planned |
| Create | POST /v1/person-phones | ๐ก Planned |
| Update | PATCH /v1/person-phones/{id} | ๐ก Planned |
| Delete | DELETE /v1/person-phones/{id} | ๐ก Planned |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|---|---|---|---|---|
| is_primary | boolean | โช No | DEFAULT false; partial unique index uniq_person_phones_primary_person enforces at most one row per person_pk WHERE deleted_at IS NULL AND is_primary IS TRUE | true / false | Marks this phone number as the person's primary contact number. At most one active PersonPhone per person may have is_primary = true at a time (enforced by partial unique index). |
| is_verify | boolean | โช No | DEFAULT false | true / false | Indicates whether the phone number has been verified (e.g., via OTP or connector confirmation). |
| label | ๐ system enum โ person_phone_label_enum (native PostgreSQL enum) | โ Yes | DEFAULT 'other'; stored as native enum core_api.person_phone_label_enum; values are the enum VALUE strings (lowercase) | mobile | work | personal | other | Categorises the nature or context of the phone number. Stored in a native PostgreSQL enum; the persisted value is the lowercase string (e.g. "mobile", not the TypeScript key MOBILE). |
| created_at | ๐ system datetime | โ Yes | Set automatically on INSERT via onCreate hook; timestamptz NOT NULL | โ | Timestamp when the pivot record was created. Set by the MikroORM onCreate lifecycle hook; never updated afterward. |
| deleted_at | ๐ system datetime | null | โช No | Nullable timestamptz; all active-record queries must filter deleted_at IS NULL | โ | Soft-delete timestamp. When set, the link between the person and phone is logically removed but the row is retained for audit. The partial indexes on person_pk and the primary-phone uniqueness constraint both scope themselves to deleted_at IS NULL. |
Relationships
| Name | Type | Required | Description |
|---|---|---|---|
| person | to-one (ManyToOne) | โ Yes | The People record this phone number is associated with. Foreign key person_pk โ core_api.people.pk. Indexed via partial index idx_person_phones_person (person_pk WHERE deleted_at IS NULL) for forward traversal, plus partial unique index for primary-phone uniqueness. |
| phone | to-one (ManyToOne) | โ Yes | The atomic Phone record containing the actual phone number value (e.g. e164_number). Foreign key phone_pk โ core_api.phones.pk. Indexed via idx_person_phones_phone (phone_pk) for reverse traversal (phones โ person_phones array relationship). |
System-computed
- pk โ auto-increment serial, internal join key only; never exposed in the public API
- created_at โ set on INSERT by MikroORM
onCreate: () => new Date()hook; noonUpdatehook exists on this entity (noupdated_atcolumn) - deleted_at โ soft-delete field; set to current timestamp when the phone association is removed via People mutation or connector sync; null on active records
- label default โ ORM default
PersonPhoneLabelEnum.OTHER('other') applied on entity construction; also enforced asDEFAULT 'other'in PostgreSQL via the native enum column - is_primary default โ ORM default
false; alsoDEFAULT falsein PostgreSQL - is_verify default โ ORM default
false; alsoDEFAULT falsein PostgreSQL - Partial unique index
uniq_person_phones_primary_personโ system-enforced constraint preventing more than one primary phone per person among non-deleted rows; managed at DB level, not ORM level
Example
{
"data": {
"type": "person_phone",
"attributes": {
"is_primary": true,
"is_verify": false,
"label": "mobile",
"created_at": "2025-11-14T09:23:17.000Z",
"deleted_at": null
},
"relationships": {
"person": {
"data": { "type": "people", "id": "d3a1c9e7-4f02-4b88-9c11-2e5f7a83bc40" }
},
"phone": {
"data": { "type": "phone", "id": "a7b2e451-0c3d-4e6f-8110-9d4f2c1b5e78" }
}
}
}
}apps/api/src/database/entities/PersonPhone.ts ยท domain: financial-graph ยท tier: Supporting