Skip to main content

CRM data model and relationship rules

DealMeld uses company records as the canonical organization identity. Clients, contacts, projects, deals, competitors, notes, communications, rooms, and delivery work should link back to that company identity whenever the relationship is known.

Agents should read this page before writing CRM or delivery data. It explains how to avoid duplicate companies, how client numbers work, how multi-domain matching works, and how to merge duplicate records safely.

Canonical records​

RecordCanonical sourceRelationship rule
CompanycompaniesThe organization record. Search and match by normalized name, website, domain, and company identifiers.
Clientcompanies with client_idA delivery-facing company. /api/clients is a client view over canonical company records.
Contactcontacts plus contact_companiesA person can have a primary company_id and additional company associations.
ProjectprojectsDelivery work should point to the client/company through company_id; project contacts and partners should relate to that same company graph.
DealdealsSales opportunities should point to a company and may link contacts, rooms, communications, and project handoff data.
CompetitorcompetitorsCompetitor intelligence can point to a canonical company through company_id.
Notescompany_notes, contact notes, and activity historyNotes are timeline records. Company create/update accepts notes as convenience input and stores it as a note, not on the company row. API access key writes use the service-account label as the note author and do not require a human user id.

Client numbers​

Clients have both a display name and a client number:

  • client_name is an API alias for the canonical company name.
  • client_number is an API alias for companies.client_id.
  • client_id is retained for compatibility and sorting.
  • If both client_id and client_number are sent, they must describe the same client number.

Use the client number for external accounting, delivery, or project references. Use the company UUID for API relationships.

Company websites and email domains​

Companies can have more than one website or email domain. The legacy fields are still present, but agents should treat identifiers as the complete source.

Field or routePurpose
companies.websitePrimary public website compatibility field.
companies.domainPrimary company-owned email domain compatibility field.
identifiersFull normalized set of websites and email domains.
websitesIdentifier subset where identifier_type = website.
email_domainsIdentifier subset where identifier_type = email_domain.
GET /api/companies/{id}/identifiersRead every website and email domain for a company.
POST /api/companies/{id}/identifiersAdd a website or email domain.
PATCH /api/companies/{id}/identifiers/{identifierId}Update label, primary flag, value, source, or metadata.
DELETE /api/companies/{id}/identifiers/{identifierId}Remove an identifier from a company.

Identifier values are normalized before matching. For example, https://www.example.com/path and example.com resolve to the same website identity. Personal email domains are rejected for client creation.

Creating or linking clients​

Use POST /api/clients when an agent needs to create a client or promote an existing company into the client workspace.

{
"client_name": "Guide Energy Solutions",
"website": "https://www.guideenergysolutions.com/",
"domain": "guide-llc.com"
}

Client creation requires a client name and either a company-owned website or domain. If the external client number is already known during an import, send it as client_number or client_id on POST /api/companies or PATCH /api/companies/{id} before or after the client is promoted. The strict POST /api/clients payload auto-assigns a client number when the company does not already have one. DealMeld matches in this order:

  1. Exact identifier, companies.domain, or companies.website match.
  2. High-confidence normalized legal-name match.
  3. High-confidence domain-brand match, such as talyardbrewing.com to Talyard Brewing.

If exactly one company matches, DealMeld links that company, fills empty company fields from the request, assigns or reactivates the client number, and associates matching-domain contacts through contact_companies.

If multiple active companies match or scores are too close, the API returns 409 CONFLICT with candidate company IDs. Do not create another company in that case. Ask a human to choose the primary company or merge duplicates first.

Company and client creation do not require a contact. Create or link the company first, then add zero, one, or many contacts as the relationship becomes known. Contacts can be linked through their primary company_id or additional contact_companies associations.

employee_count accepts an exact number or a range string when the exact headcount is not known. Examples such as "1-5", "1 to 5", and "1,001-5,000" are normalized to a representative stored integer.

Merge behavior​

Use merge when two active records represent the same real company or contact. The ID in the path is the primary record that survives.

POST /api/companies/{primaryCompanyId}/merge
Content-Type: application/json

{
"duplicate_company_id": "381a0e33-461e-4991-836c-8eadecd2e5a6"
}

Company merge preserves primary values, fills empty primary fields from the duplicate, moves identifiers, contacts, notes, deals, projects, communications, competitors, custom fields, signing records, pulse records, and activity history, then soft-deletes the duplicate. Duplicate client_id, website, and domain values are released before reparenting so same-identifier duplicate records can merge without unique-field conflicts.

Contact merge uses the same primary-path rule:

POST /api/contacts/{primaryContactId}/merge
Content-Type: application/json

{
"duplicate_contact_id": "381a0e33-461e-4991-836c-8eadecd2e5a6"
}

Contact merge moves emails, phones, profiles, tags, company associations, notes, deals, projects, campaigns, communications, signing records, custom fields, and activity history into the primary contact before soft-deleting the duplicate.

Merge is a destructive CRM cleanup action even though records are soft-deleted. Agents should read both records first, choose the most complete or most trusted record as primary, then merge only after the user or automation policy allows it.

UI merge workflow​

The app exposes company and contact merge from detail pages:

  1. Open the company or contact detail page.
  2. Click Merge.
  3. Search for the duplicate by name, website, domain, client number, email, phone, title, or role.
  4. Choose whether the current record or the selected match remains primary.
  5. Confirm the merge.

Known UUIDs can be pasted into the search field as a fallback.

Global search behavior​

The top app search is a global search surface. It is not one monolithic API endpoint. The web app performs bounded, parallel reads against visible modules:

Result typeAPI read
CompaniesGET /api/companies?search={query}&pageSize=4
ClientsGET /api/clients?search={query}&pageSize=4
ContactsGET /api/contacts?search={query}&pageSize=4
DealsGET /api/deals?search={query}&pageSize=4
ProjectsGET /api/projects?search={query}&pageSize=4
RoomsGET /api/rooms?search={query}&pageSize=4
CommunicationsGET /api/communications?q={query}&pageSize=4
CampaignsGET /api/campaigns?search={query}&pageSize=4
CompetitorsGET /api/competitors?search={query}&pageSize=4

Agents should use the same pattern when they need cross-platform lookup: fan out to the relevant list endpoints, keep page sizes bounded, ignore modules that are hidden or return permission errors, then present grouped results.

Competitors should link to canonical companies when the competing organization is known. POST /api/competitors and PUT /api/competitors/{id} accept company_id. If omitted during create, DealMeld links the competitor to an active company in the same tenant only when there is exactly one case-insensitive same-name match. Competitor list, detail, comparison, and battlecard responses can include the linked company summary.

Agent write checklist​

Before creating companies, clients, projects, contacts, or competitors:

  1. Search by normalized domain, website, client number, and name.
  2. Prefer linking to an existing company over creating a new one.
  3. Add missing websites and email domains as company identifiers.
  4. Use POST /api/clients to promote an existing company into a client.
  5. Link projects, deals, contacts, notes, communications, rooms, and competitors to the company UUID.
  6. Merge obvious duplicates only after the primary record is chosen and the action is approved.

Relationship intelligence data model​

Relationship intelligence extends contacts with first-class relationship context and derived signals. The data model separates stored context from derived values.

Stored context (on the contacts row)​

These fields are human-authored or agent-proposed-then-human-confirmed. They are never inferred from timeline activity.

FieldClassificationDescription
relationship_intentrestricted_relationshipWhy this relationship matters (e.g., investor, advisor, partner).
desired_cadence_daysordinary_crmTarget days between meaningful touches.
preferred_channelordinary_crmPreferred communication channel.
interestsrestricted_relationshipTopics and areas of mutual interest.
context_notesprivate_user_noteFreeform notes visible only to users with explicit read permission.
steward_user_idordinary_crmThe user responsible for maintaining this relationship.

Derived signals (computed on read)​

These values are computed by RelationshipSummaryService from engagement timeline events. They are not stored and do not require backfill.

SignalSourceDescription
last_meaningful_touch_atTimeline eventsMost recent interaction qualifying as meaningful per touch-semantics rules.
next_scheduled_touch_atTimeline eventsNearest future scheduled communication.
unresolved_next_stepsTimeline eventsCount of open action items from timeline events.
recency_bandDerived from last_meaningful_touch_atfresh, stable, stale, or dormant.
interaction_countsAggregated timeline countsPer-channel interaction totals.

Structured outcomes (relationship_outcomes table)​

Typed records of what happened from relationship work. Stored as first-class rows with RLS policies, not derived.

FieldDescription
outcome_typeOne of 9 types: resource_shared, referral_given, referral_received, introduction_made, collaboration_started, collaboration_completed, co_created_artifact, research_insight, community_contribution.
directioninbound, outbound, or mutual.
evidenceBounded metadata categories — never full asset bodies.
confirmation_stateagent_suggested, human_confirmed, or corrected.
proof_asset_refsStable references (source URL, source system/ID, event type) to external proof assets. Full asset bodies remain external.

Governance and access​

ADR 002 defines a data-classification matrix for every relationship field. Restricted and private context must never appear in derived signals, summaries, citations, search results, dashboard aggregates, or telemetry logs unless the caller has explicit read permission for that field. Agent projections use the same permission redaction as human reads.

See the Relationship Intelligence Vocabulary guide (docs/RELATIONSHIP_INTELLIGENCE_VOCABULARY.md) for plain-language definitions of all relationship-intelligence terms.