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
| Record | Canonical source | Relationship rule |
|---|---|---|
| Company | companies | The organization record. Search and match by normalized name, website, domain, and company identifiers. |
| Client | companies with client_id | A delivery-facing company. /api/clients is a client view over canonical company records. |
| Contact | contacts plus contact_companies | A person can have a primary company_id and additional company associations. |
| Project | projects | Delivery work should point to the client/company through company_id; project contacts and partners should relate to that same company graph. |
| Deal | deals | Sales opportunities should point to a company and may link contacts, rooms, communications, and project handoff data. |
| Competitor | competitors | Competitor intelligence can point to a canonical company through company_id. |
| Notes | company_notes, contact notes, and activity history | Notes 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_nameis an API alias for the canonical companyname.client_numberis an API alias forcompanies.client_id.client_idis retained for compatibility and sorting.- If both
client_idandclient_numberare 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 route | Purpose |
|---|---|
companies.website | Primary public website compatibility field. |
companies.domain | Primary company-owned email domain compatibility field. |
identifiers | Full normalized set of websites and email domains. |
websites | Identifier subset where identifier_type = website. |
email_domains | Identifier subset where identifier_type = email_domain. |
GET /api/companies/{id}/identifiers | Read every website and email domain for a company. |
POST /api/companies/{id}/identifiers | Add 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:
- Exact identifier,
companies.domain, orcompanies.websitematch. - High-confidence normalized legal-name match.
- High-confidence domain-brand match, such as
talyardbrewing.comtoTalyard 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:
- Open the company or contact detail page.
- Click Merge.
- Search for the duplicate by name, website, domain, client number, email, phone, title, or role.
- Choose whether the current record or the selected match remains primary.
- 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 type | API read |
|---|---|
| Companies | GET /api/companies?search={query}&pageSize=4 |
| Clients | GET /api/clients?search={query}&pageSize=4 |
| Contacts | GET /api/contacts?search={query}&pageSize=4 |
| Deals | GET /api/deals?search={query}&pageSize=4 |
| Projects | GET /api/projects?search={query}&pageSize=4 |
| Rooms | GET /api/rooms?search={query}&pageSize=4 |
| Communications | GET /api/communications?q={query}&pageSize=4 |
| Campaigns | GET /api/campaigns?search={query}&pageSize=4 |
| Competitors | GET /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.
Competitor company links
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:
- Search by normalized domain, website, client number, and name.
- Prefer linking to an existing company over creating a new one.
- Add missing websites and email domains as company identifiers.
- Use
POST /api/clientsto promote an existing company into a client. - Link projects, deals, contacts, notes, communications, rooms, and competitors to the company UUID.
- 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.
| Field | Classification | Description |
|---|---|---|
relationship_intent | restricted_relationship | Why this relationship matters (e.g., investor, advisor, partner). |
desired_cadence_days | ordinary_crm | Target days between meaningful touches. |
preferred_channel | ordinary_crm | Preferred communication channel. |
interests | restricted_relationship | Topics and areas of mutual interest. |
context_notes | private_user_note | Freeform notes visible only to users with explicit read permission. |
steward_user_id | ordinary_crm | The 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.
| Signal | Source | Description |
|---|---|---|
last_meaningful_touch_at | Timeline events | Most recent interaction qualifying as meaningful per touch-semantics rules. |
next_scheduled_touch_at | Timeline events | Nearest future scheduled communication. |
unresolved_next_steps | Timeline events | Count of open action items from timeline events. |
recency_band | Derived from last_meaningful_touch_at | fresh, stable, stale, or dormant. |
interaction_counts | Aggregated timeline counts | Per-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.
| Field | Description |
|---|---|
outcome_type | One of 9 types: resource_shared, referral_given, referral_received, introduction_made, collaboration_started, collaboration_completed, co_created_artifact, research_insight, community_contribution. |
direction | inbound, outbound, or mutual. |
evidence | Bounded metadata categories — never full asset bodies. |
confirmation_state | agent_suggested, human_confirmed, or corrected. |
proof_asset_refs | Stable 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.