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.id—references(users.id)is forbidden for identity, membership, attribution (createdBy,approvedBy, …), assignment, and audit columns.contactsis the durable identity; users is just one auth method keyed oncontact_id. Spec:docs/superpowers/specs/2026-04-24-contact-as-sole-human-identity-design.md.
HubSpot mirror:
contactsmirrors the HubSpot Contact object (objectTypeId0-1,contactConfiginservices/hubspot/sync-engine/entity-configs.ts).hubspotIdis for sync round-tripping only — internal references join oncontacts.id(UUID PK).
Source Files
| Layer | Path |
|---|---|
| Schema | backend/src/db/schema.ts (contacts) |
| Routes | backend/src/routes/contacts.ts |
| Service | backend/src/services/contact-service.ts |
| Pages | frontend/src/pages/ContactsPage.tsx, frontend/src/pages/ContactDetailPage.tsx |
| Queries | frontend/src/lib/queries/contactQueries.ts |
Database Tables
| Table | Purpose |
|---|---|
contacts | Main entity — personal/legal person data, address, HubSpot sync fields |
contact_companies | M2M join table linking contacts to Companies with role metadata |
building_roles | Links contacts to Buildings with a role (via RBAC Authorization) |
project_roles | Links contacts to Projects with a role |
Key Fields
| Field | Type | Notes |
|---|---|---|
type | enum | Deprecated — contacts are always natural_person; legal persons live in Companies |
firstName, lastName | varchar | Person name (may be absent for HubSpot-only contacts) |
title | varchar(50) | Dr., Prof., etc. |
salutation, jobTitle, mobilePhone | varchar | HubSpot-synced person fields (I#779) |
email, phone | varchar | Contact info |
street, houseNumber, postalCode, city, country | varchar | Unified address (I#1329) |
lat, lng, placeId | double/varchar | Geocoded location |
dateOfBirth | timestamp | For natural persons |
customerType, customerSegment, classification, companyFunction, decisionAuthority | varchar | RENEWA classification (HubSpot-synced) |
lifecycleStage, linkedinUrl | varchar | HubSpot lifecycle + social |
parentContactId, reportsToContactId, hierarchyLevel | uuid/int | Contact hierarchy (self-referencing, self-reference checks enforced) |
claimedAt, claimedByUserId | timestamp/uuid | Contact claiming — a user signing up with a matching email claims the contact and inherits its roles |
departmentId | uuid FK | Department placement — single FK, populated by Entra profile sync (RNW-336); see Departments |
locationId | uuid FK | Office location, populated by Entra profile sync |
hubspotId | varchar(100) | HubSpot CRM Contact ID (unique) |
hubspotUserId | varchar(100) | HubSpot User ID (settings-side identity for Renewa employees with HubSpot access; distinct from hubspotId) |
recordOwnerContactId | uuid FK | Read-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, lastSyncedAt | various | Raw HubSpot property bag + sync metadata |
archived, archivedAt, deletedAt, archiveReason, mergedIntoHubspotId | various | HubSpot archive/merge status + privacy deletion |
HubSpot Sync
Contacts sync bidirectionally with HubSpot CRM contacts:
hubspotIdmaps to the HubSpot recordhubspotPropertiesstores the full HubSpot property setlastSyncedAttracks 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); evenworkflow_transition_log.actorIdwas migrated (PR#1931) - Natural persons only — the
typeenum 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
Related Pages
Companies | Buildings | Projects | Users | HubSpot Integration | RBAC Authorization | Leads | Database Architecture