Phone

Phone is a shared atomic resource representing a single telephone number stored in its canonical decomposed form: ITU-T country-calling-code, national-number string, and the derived E.164 number. It is linked to People and Company records through pivot entities (PersonPhone, CompanyPhone) that carry relationship-level metadata such as primary/verified status and a label. A Phone always belongs to a workspace via a nullable integer FK (workspace_pk) added in Migration20260119180000 and is soft-deletable. It is exposed as a records root named "phones" in the data-views pipeline with composite columns resolving linked companies and people.

NamingValue
ObjectPhone
Resource type (JSON:API type)phone
Collection / records rootphones
REST base/v1/phones
Entity classPhone

API operations

OperationMethod & pathStatus
ListGET /v1/phonesโœ… Implemented
List (nested)GET /v1/people/{id}/phones๐ŸŸก Planned
RetrieveGET /v1/phones/{id}โœ… Implemented
Create (nested)POST /v1/people/{id}/phonesโœ… Implemented
UpdatePATCH /v1/phones/{id}๐ŸŸก Planned
Delete (nested)DELETE /v1/people/{id}/phones/{subId}โœ… Implemented

Data model

Attributes

FieldTypeRequiredConstraintsAllowed valuesDescription
phone_idstring, UUID, ๐Ÿ”’ systemโœ… Yesunique; generated by gen_random_uuid() on INSERTโ€”Public stable identifier for the phone number. Used in all API responses and cross-resource references. The internal pk is never exposed.
country_codeintegerโœ… YesNOT NULLAny valid ITU-T calling code (e.g. 1, 33, 44, 49)The international dialing prefix without the leading '+'. For example France = 33, US = 1, UK = 44.
national_numberstringโœ… Yesvarchar(255); NOT NULLโ€”The national (subscriber) portion of the phone number, without the country code prefix and without leading zeros stripped. Stored as a string to preserve leading zeros for countries that require them.
e164_numberstringโœ… Yesvarchar(255); NOT NULLโ€”The fully-qualified E.164 representation of the number, including the leading '+' and country code. This is the canonical machine-readable form used for display, deduplication, and connector sync matching.
created_atdatetime, ๐Ÿ”’ systemโœ… YesNOT NULL; set once on INSERT via onCreate lifecycle hookโ€”Timestamp at which the Phone row was created. Immutable after creation.
updated_atdatetime, ๐Ÿ”’ systemโšช Nonullable; set on INSERT and updated on every UPDATE via onUpdate lifecycle hookโ€”Timestamp of the last modification to the Phone row. Null if the row has never been updated after creation.
deleted_atdatetimeโšช Nonullable; null = active recordโ€”Soft-delete timestamp. When set, the phone number is considered deleted and all queries must filter deleted_at IS NULL. Hard-deletes are not used.

Relationships

NameTypeRequiredDescription
workspaceto-one (workspace)โšช No (nullable)The workspace that owns this phone number. @ManyToOne(() => Workspace, { nullable: true }). The DB column is workspace_pk (int FK โ†’ core_api.workspaces.pk, ON DELETE SET NULL), added by Migration20260119180000. Provides tenant isolation for Hasura RLS filtering.
person_phonesto-many (person_phone)โ€”Pivot entities linking this phone to one or more People records. Each PersonPhone carries is_primary (with a partial-unique constraint: only one primary per person while deleted_at IS NULL), is_verify, label (PersonPhoneLabelEnum: mobile | work | personal | other), and created_at. Reverse-traversal index idx_person_phones_phone is on phone_pk.
company_phonesto-many (company_phone)โ€”Pivot entities linking this phone to one or more Company records. Each CompanyPhone carries is_primary (partial-unique: one primary per company while deleted_at IS NULL), is_verify, label (free-text string), and created_at. Bridge-table indexes: idx_company_phones_phone (phone_pk), idx_company_phones_company_deleted (company_pk, deleted_at).

System-computed

  • phone_id is generated by gen_random_uuid() at the database level on INSERT and carries a UNIQUE constraint. It is the public API identifier; the internal pk (serial int) is never exposed.
  • created_at is set via MikroORM onCreate lifecycle hook (new Date()) and is immutable thereafter.
  • updated_at is set on both INSERT (onCreate) and every UPDATE (onUpdate) via lifecycle hooks.
  • deleted_at is null by default. Setting it to a non-null timestamp performs a soft-delete. All reads must include a deleted_at IS NULL predicate. Hard-deletes are not used for Phone rows.
  • workspace FK column in the DB is workspace_pk (int, nullable FK โ†’ core_api.workspaces.pk, ON DELETE SET NULL), added by Migration20260119180000. Pre-migration rows have workspace_pk = NULL. The entity declares the relation nullable: true. There is no UUID workspace_id column on the phones table.
  • The PersonPhone pivot enforces a partial unique index (uniq_person_phones_primary_person) so that at most one PersonPhone per person_pk has is_primary = TRUE while deleted_at IS NULL.
  • The CompanyPhone pivot enforces a parallel partial unique index (uniq_company_phones_primary_company) so that at most one CompanyPhone per company_pk has is_primary = TRUE while deleted_at IS NULL.
  • In the data-views pipeline, the phones root exposes two composite columns: composite_companies_list (display_type: relation_list, source_fields: company_phones.company.company_id + company_phones.company.name) and composite_people_list (display_type: relation_list, source_fields: person_phones.people.person_id + person_phones.people.full_name). These are defined in composites.yml under the phones root.
  • On company and person records roots, composite_phones_list composites surface linked phone numbers (source_fields: {bridge}.phone.phone_id + {bridge}.phone.e164_number) for display in the records table.

Example

{
  "data": {
    "type": "phone",
    "id": "a3f7c291-84e2-4d55-9fc3-b8120e4a7301",
    "attributes": {
      "phone_id": "a3f7c291-84e2-4d55-9fc3-b8120e4a7301",
      "country_code": 33,
      "national_number": "612345678",
      "e164_number": "+33612345678",
      "created_at": "2025-09-14T10:22:00.000Z",
      "updated_at": "2026-01-07T08:05:00.000Z",
      "deleted_at": null
    },
    "relationships": {
      "workspace": {
        "data": { "type": "workspace", "id": "f1a2b3c4-0000-0000-0000-000000000001" }
      },
      "person_phones": {
        "data": [
          { "type": "person_phone", "id": "pivot-pk-91" }
        ]
      },
      "company_phones": {
        "data": []
      }
    }
  }
}
Source: apps/api/src/database/entities/Phone.ts ยท domain: financial-graph ยท tier: Supporting