Contacts

People associated with buildings and projects — building owners, tenants, craftsmen, consultants, and Renewa employees. Contacts are synced bidirectionally with HubSpot Integration.

Contact as sole human identity (Human FK rule): every FK pointing at a human points at contacts.idreferences(users.id) is forbidden for identity, membership, attribution (createdBy, approvedBy, …), assignment, and audit columns. contacts is the durable identity; users is just one auth method keyed on contact_id. Spec: docs/superpowers/specs/2026-04-24-contact-as-sole-human-identity-design.md.

HubSpot mirror: contacts mirrors the HubSpot Contact object (objectTypeId 0-1, contactConfig in services/hubspot/sync-engine/entity-configs.ts). hubspotId is for sync round-tripping only — internal references join on contacts.id (UUID PK).

Source Files

LayerPath
Schemabackend/src/db/schema.ts (contacts)
Routesbackend/src/routes/contacts.ts
Servicebackend/src/services/contact-service.ts
Pagesfrontend/src/pages/ContactsPage.tsx, frontend/src/pages/ContactDetailPage.tsx
Queriesfrontend/src/lib/queries/contactQueries.ts

Database Tables

TablePurpose
contactsMain entity — personal/legal person data, address, HubSpot sync fields
contact_companiesM2M join table linking contacts to Companies with role metadata
building_rolesLinks contacts to Buildings with a role (via RBAC Authorization)
project_rolesLinks contacts to Projects with a role

Key Fields

FieldTypeNotes
typeenumDeprecated — contacts are always natural_person; legal persons live in Companies
firstName, lastNamevarcharPerson name (may be absent for HubSpot-only contacts)
titlevarchar(50)Dr., Prof., etc.
salutation, jobTitle, mobilePhonevarcharHubSpot-synced person fields (I#779)
email, phonevarcharContact info
street, houseNumber, postalCode, city, countryvarcharUnified address (I#1329)
lat, lng, placeIddouble/varcharGeocoded location
dateOfBirthtimestampFor natural persons
customerType, customerSegment, classification, companyFunction, decisionAuthorityvarcharRENEWA classification (HubSpot-synced)
lifecycleStage, linkedinUrlvarcharHubSpot lifecycle + social
parentContactId, reportsToContactId, hierarchyLeveluuid/intContact hierarchy (self-referencing, self-reference checks enforced)
claimedAt, claimedByUserIdtimestamp/uuidContact claiming — a user signing up with a matching email claims the contact and inherits its roles
departmentIduuid FKDepartment placement — single FK, populated by Entra profile sync (RNW-336); see Departments
locationIduuid FKOffice location, populated by Entra profile sync
hubspotIdvarchar(100)HubSpot CRM Contact ID (unique)
hubspotUserIdvarchar(100)HubSpot User ID (settings-side identity for Renewa employees with HubSpot access; distinct from hubspotId)
recordOwnerContactIduuid FKRead-only sync mirror of the HubSpot record owner — self-FK to contacts, resolved via hubspot_owners (PR#1767); sync layer is the sole writer
hubspotProperties, hubspotPipeline, hubspotPipelineStage, hubspotSchemaVersion, lastSyncedAtvariousRaw HubSpot property bag + sync metadata
archived, archivedAt, deletedAt, archiveReason, mergedIntoHubspotIdvariousHubSpot archive/merge status + privacy deletion

HubSpot Sync

Contacts sync bidirectionally with HubSpot CRM contacts:

  • hubspotId maps to the HubSpot record
  • hubspotProperties stores the full HubSpot property set
  • lastSyncedAt tracks sync freshness
  • Sync managed via HubSpot Integration admin routes (backend/src/routes/admin/hubspot-sync.ts)

Relationships

Contact *──* Companies (via contact_companies)
Contact *──* Buildings (via building_roles)
Contact *──* Projects (via project_roles)
Contact 1──0..1 Users (user.contactId -- one auth method)
Contact *──0..1 Department (contacts.departmentId, Entra-synced)
Contact <── attribution/assignment FKs from across the schema (Human FK rule)

Every user has a mandatory contactId reference; the contact is the durable identity and the source of truth for personal information. “Is this person an active user?” is a predicate (users WHERE contact_id = ? AND is_active), not a separate identity.

HubSpot-Tracked Relationships

Roles HubSpot tracks against contacts (customer, owner, invoice_recipient, landlord, resident, architect, main_contact, …) are not FK columns on the target tables — they are polymorphic rows in hubspot_associations, read via getAssociationsByRenewaName(role, fromEntityId, fromEntityType, toEntityType?); the internal_name → role mapping lives in RENEWA_ROLE_BINDINGS and is reapplied every sync. Spec: docs/superpowers/specs/2026-05-08-hubspot-association-infrastructure-design.md.

Frontend Components

The contacts list page supports search, filtering, and pagination. The detail page shows contact info, associated Companies, Buildings, and Projects.

Features

  • Sole human identity — all human FKs across the schema point at contacts.id (Human FK rule); even workflow_transition_log.actorId was migrated (PR#1931)
  • Natural persons only — the type enum is deprecated; legal persons live in Companies
  • HubSpot bidirectional sync via HubSpot Integration
  • Contact claiming — signup with a matching email claims the contact and inherits its roles
  • Role assignments on Buildings and Projects via RBAC Authorization
  • Company associations with role and date metadata (primary flag, start/end dates)
  • Entra-fed employee data — department and office location FKs are populated by the hourly Entra sync (see Departments)
  • User linkage — every system user is backed by a contact record

Companies | Buildings | Projects | Users | HubSpot Integration | RBAC Authorization | Leads | Database Architecture