Research Objective
Understand Z360's current contact model, identify CRM parity gaps, benchmark adjacent CRM systems, and produce an implementation-ready path that improves customer data richness without breaking the product root.
Z360 CRM investigation packet | May 18, 2026
A code-backed audit of how contacts are created and connected today, what HubSpot, GoHighLevel, and monday CRM teach us, and the minimum Z360 should build next without destabilizing the rest of the product.
Understand Z360's current contact model, identify CRM parity gaps, benchmark adjacent CRM systems, and produce an implementation-ready path that improves customer data richness without breaking the product root.
Build a pragmatic CRM Profile v2, not a full CRM platform rewrite. Preserve the current Contact ID and conversation history model. Extend identity and profile data around it.
Contact creation is not one flow. Manual create, public forms, inquiry forms, imports, widget registration, agent actions, and MCP tools all create or match contacts. A schema change without a shared resolver will multiply duplicates and history gaps.
| Bucket | What goes in | Why it matters | Decision |
|---|---|---|---|
| MVP now | Company, owner, multiple contact methods, scoped contact fields, resolver service, delete safety. | Closes the most visible CRM parity gap while protecting inbox and inquiry flows. | Do it in the next sprint. |
| Later | Association labels, advanced lifecycle workflows, imports, merge UI, dedupe review queue. | Valuable, but not required for the first credible CRM upgrade. | Design for it, do not ship yet. |
| Not now | Full custom objects, monday-style board builder, calculation fields, custom relationship types. | High complexity, high support burden, and not aligned with Z360's small-business automation wedge. | Explicitly defer. |
| Label | What this report means by it | Examples |
|---|---|---|
| Observed | Directly verified from local migrations, models, observers, controllers, viewmodels, frontend files, or official competitor documentation. | Contact fields, identifier uniqueness, creation paths, public CRM object/property docs. |
| Inferred | Reasoned from code relationships and product behavior, but not verified through a live Z360 browser session in this run. | Customer-visible profile limitations, sidebar dependency patterns, impact of richer contact methods. |
| Assumed | Product direction this report assumes based on the request and Z360 positioning. | Small-business CRM should stay simpler than HubSpot and more structured than monday boards. |
| Unknown / needs validation | Needs runtime QA, customer workflow review, or migration rehearsal before implementation is considered production-ready. | Exact contact detail UI state, production duplicate rate, deletion recovery expectations, import volume. |
The product has two practical roots: `contacts` for CRM profile context and `identifiers` for communication identity.
No first-class company/account, owner, title, address, lifecycle, role, or structured profile sections.
The identifier table can technically hold multiple records, but the profile UI and validation are still built around one email and one phone.
Normal contact delete should preserve identifiers by nulling `contact_id`, but MCP delete explicitly deletes identifiers first, which can cascade conversations.
Intake fields live on `InquiryField`; they are not a clean reusable contact profile customization model.
Controllers, services, observers, widget auth, imports, and MCP tools each encode contact matching behavior.
Database columns are nullable, but most create flows require both email and phone, limiting call-first and chat-first enrichment.
| Finding | Evidence | Interpretation |
|---|---|---|
| Contact fields are basic direct columns. | `app/Models/Contact.php:33-41`, `database/migrations/2025_09_17_000010_add_email_phone_to_contacts_table.php:14-20` | The current CRM profile is thin by design. |
| Contact has conversations through identifiers. | `app/Models/Contact.php:88-97` | Identity redesign must preserve identifier continuity. |
| Sidebar data is mostly indirect. | `app/ViewModels/Contacts.php:267-427` | Tickets, notes, reminders, attachments, and threads are retrieved through conversation messages. |
| Inquiry creation auto-creates contacts. | `app/Observers/InquiryObserver.php:37-85` | Any contact matching change immediately affects lead creation and outreach. |
| Public forms auto-create contacts and conversations. | `app/Services/FormsSubmissionService.php:23-64` | Form behavior must move to the same resolver as manual and inquiry flows. |
The useful pattern is not "copy everything." It is choosing the right amount of CRM structure for Z360's automation product.
Strong object model: contacts, companies, deals, tickets, activities, associations, properties, and custom objects.
Lesson: use associations and stable property definitions, but avoid enterprise object sprawl for MVP.
Contact-centric CRM with opportunities, tags, owner, DND, conversations, workflows, and object-scoped custom fields.
Lesson: separate contact fields from opportunity/inquiry fields and use fields for automation.
Board-first CRM where contacts, accounts, deals, leads, and activities are configurable boards linked by columns.
Lesson: the UX is flexible, but Z360 should keep a canonical model under any customization layer.
| Capability | HubSpot | GoHighLevel | monday CRM | Z360 implication |
|---|---|---|---|---|
| Company/account relationship | First-class Company object with associations. | Company/business support, lighter than HubSpot. | Accounts board linked to Contacts. | Add companies and a contact-company pivot now. |
| Custom fields | Typed object properties and groups. | Contact vs opportunity fields, folder grouping. | Columns on configurable boards. | Use object-scoped definitions and typed values. Do not use arbitrary JSON only. |
| Multiple contact methods | Primary email plus additional emails; phone as properties. | Primary email/phone in public docs, DND per channel. | Email/phone columns, flexible board fields. | Extend identifiers into visible contact methods with labels and primary flags. |
| Owner/assignee | `hubspot_owner_id` and custom user fields. | Contact owner and opportunity owner can be decoupled. | Owner columns on boards. | Add contact owner without replacing inquiry assignment logic. |
| Workflow substrate | Properties and associations drive automation. | Fields, tags, DND, opportunities drive workflows. | Columns and automations drive workflows. | Custom fields must be queryable and automatable, not only UI metadata. |
Canonical contact plus company, owner, contact methods, typed fields, visible profile sections, field groups, and a unified timeline.
Custom objects, association labels, merge queues, field calculations, deep board-style schema configuration, and import mapping UI.
Making every form answer a permanent contact field, duplicating opportunity data onto contacts, and letting integrations bypass one resolver.
Keep Contact as the durable anchor. Make identity and profile data richer around it.
Introduce one shared contact resolver for manual create, forms, inquiries, imports, widget registration, agent actions, and MCP tools.
Done when no write path uses a one-off `firstOrCreate(email, phone)` rule.
Expose identifiers as contact methods with type, label, primary, source, DND, blocked, verified, and conversation count.
Done when users can add work email, personal email, mobile phone, office phone, and choose the primary values.
Add company records plus a pivot for role/title and primary relationship. Keep old free-text company out of Contact.
Done when one company can have many contacts and one contact can later support multiple companies.
Add an owner field to Contact separate from creator and separate from inquiry assignment.
Done when customers can assign accountability for a contact without reassigning every ticket or inquiry.
Create contact field definitions and typed values. Support sections, ordering, field type, options, required flag, and visibility.
Done when org admins can create controlled profile fields that appear on contact create/edit and detail.
Add identity events and align delete behavior so contact deletion never silently cascades conversations through identifier deletion.
Done when all delete paths preserve communication history unless explicitly purged.
| Persona | What changes after implementation | Business outcome |
|---|---|---|
| Z360 customer user | Can store richer contact profiles, associate people with companies, track multiple contact methods, and see a clearer history. | Fewer workarounds, better follow-up, more CRM trust. |
| Sales or front desk teammate | Can assign contact ownership and find contacts by company, email, phone, tag, or custom profile field. | Less duplicate work and clearer accountability. |
| Business owner | Can standardize what their team captures for customers without forcing every lead form answer into the same global schema. | Better data quality for automation, reporting, and customer experience. |
| Z360 engineering | Gets a shared resolver, explicit identity events, safer deletion, and predictable profile extension points. | Less brittle product root and lower incident risk. |
| AI / automation layer | Can read structured profile facts, company context, preferred contact methods, DND, and lifecycle fields. | More useful AI actions and better personalization with less hallucinated context. |
Company, owner, and contact methods are not just fields. They drive permissions, dedupe, routing, search, automation, and history.
Keep them as primary caches for compatibility, but make identifiers the richer source for multiple methods and channel state.
Z360 wins by automating customer communication for small businesses. A heavy custom-object platform slows the core roadmap.
| Input case | Resolver behavior | Audit requirement |
|---|---|---|
| Exact email match in same organization | Attach to existing contact, update primary cache only when the incoming source is trusted or user-confirmed. | Log source and whether any primary value changed. |
| Exact phone match in same organization | Attach to existing contact after normalization. Preserve DND and blocked state from the matched identifier. | Log normalized value, source, and previous owner/contact link. |
| Same email, different phone | Use existing contact and add the phone as a new contact method unless blocked by an explicit conflict rule. | Log method_added and primary decision. |
| Same phone, different email | Use existing contact and add the email as a new contact method. Do not overwrite name from low-trust sources. | Log method_added and source confidence. |
| Email and phone match different contacts | Do not auto-merge. Create a conflict event and attach the new activity to the highest-confidence identifier. | Log conflict_needs_review with both contact IDs. |
| Visitor-only chat session | Create or reuse an unclaimed visitor identifier, then link it when email or phone becomes available. | Log visitor_claimed when upgraded to a known contact. |
| Import row or agent/MCP write | Use the same resolver as product flows. Never bypass tenant uniqueness or identifier audit. | Log actor, import batch or tool name, and source payload hash when available. |
Current artifacts are observed from the repo. Proposed artifacts are implementation targets for CRM Profile v2.
| Status | Artifact | Purpose | Notes |
|---|---|---|---|
| Current | `contacts` | Root CRM profile row. | Name, email, phone, avatar, favorite, creator, org. |
| Current | `identifiers` | Communication identity for email, phone, visitor ID. | Already has DND, blocked, metadata, contact link. |
| Current | `conversations`, `messages` | Inbox threads and message history. | Conversation belongs to identifier. Message can polymorphically compose work objects. |
| Current | `inquiries`, `inquiry_fields` | Lead pipeline records and intake answers. | Contact ID is direct. Custom intake answers are inquiry-specific. |
| Current | `tickets`, `notes`, `reminders`, `attachments`, `activities`, `tags`, `taggables` | Associated operational context. | Mostly reached through conversations and composition messages. |
| Proposed | `add_crm_profile_columns_to_contacts` | Add `owner_id`, `lifecycle_stage`, `source`, `deleted_at` if approved. | Keep `creator_id` as audit. Use `owner_id` for CRM accountability. |
| Proposed | `extend_identifiers_for_contact_methods` | Add `label`, `is_primary`, `normalized_value`, `source`, `verified_at`, `last_seen_at`. | Prefer evolving identifiers over duplicating contact methods in a second table. |
| Proposed | `companies`, `company_contact` | First-class company/account association. | Supports one company to many contacts and future many-to-many contact roles. |
| Proposed | `contact_field_definitions`, `contact_field_values` | Typed, scoped, ordered custom contact profile fields. | Do not store all field values only in JSON if search and automation matter. |
| Safety | `contact_identity_events` | Audit contact method links, unlinks, merges, splits, and primary changes. | Required before merge tooling or broad imports. |
| Safety | `contact_delete_events` or activity expansion | Record who deleted what and what was preserved. | Resolve MCP delete inconsistency before shipping richer contact operations. |
2026_XX_XX_000001_add_owner_and_lifecycle_to_contacts.php - contacts.owner_id nullable foreign key to users, null on delete - contacts.lifecycle_stage nullable string or enum-backed string - contacts.source nullable string - contacts.deleted_at nullable timestamp if soft delete is approved 2026_XX_XX_000002_extend_identifiers_for_contact_methods.php - identifiers.label nullable string - identifiers.is_primary boolean default false - identifiers.normalized_value nullable string - identifiers.source nullable string - identifiers.verified_at nullable timestamp - identifiers.last_seen_at nullable timestamp - partial unique guard for one primary per contact and type
2026_XX_XX_000003_create_companies_table.php - id - organization_id foreign key - name required - domain nullable - website nullable - phone nullable - address fields nullable or metadata json - owner_id nullable foreign key to users - timestamps - softDeletes optional 2026_XX_XX_000004_create_company_contact_table.php - id - organization_id foreign key - company_id foreign key cascade - contact_id foreign key cascade or restrict based on delete policy - role nullable string - title nullable string - is_primary boolean default false - started_at, ended_at nullable - timestamps
2026_XX_XX_000005_create_contact_field_definitions_table.php - id - organization_id foreign key - key stable string - label string - type string: text, textarea, number, date, select, multi_select, boolean, url - group_name nullable string - options json nullable - required boolean default false - show_on_create boolean default false - sort_order integer default 0 - archived_at nullable timestamp - timestamps - unique organization_id + key 2026_XX_XX_000006_create_contact_field_values_table.php - id - organization_id foreign key - contact_id foreign key - field_definition_id foreign key - value_text nullable - value_number nullable - value_date nullable - value_json nullable - timestamps - unique contact_id + field_definition_id
2026_XX_XX_000007_create_contact_identity_events_table.php - id - organization_id foreign key - contact_id nullable foreign key - identifier_id nullable foreign key - event_type string: linked, unlinked, primary_changed, merged, split, deleted - actor_id nullable foreign key to users - before json nullable - after json nullable - reason nullable text - created_at timestamp 2026_XX_XX_000008_create_contact_merge_events_table.php - id - organization_id foreign key - survivor_contact_id foreign key - merged_contact_id nullable - actor_id nullable foreign key - summary json - created_at timestamp
Create identifier rows from existing `contacts.email` and `contacts.phone` where missing. Mark existing values as primary. Keep the old contact columns as compatibility caches.
Preserve tenant scoping. Add normalized lookup indexes for `organization_id`, `type`, and `normalized_value`. Add unique guards for one primary email and one primary phone per contact.
Roll back additive fields first, but do not drop identity audit tables until the migration rehearsal confirms no production recovery depends on them.
| Phase | Goal | Done when | Validation |
|---|---|---|---|
| 0 | Safety cleanup and shared resolver design. | All write paths call one resolver. MCP delete no longer cascades conversations unintentionally. | Targeted feature tests for manual, form, inquiry, widget, MCP create/delete. |
| 1 | Add contact owner, contact methods, and primary sync. | User can add multiple phones/emails and choose primary values. Existing API remains compatible. | Migration tests, observer/service tests, contact create/update tests. |
| 2 | Add company association. | User can create/link company from contact and filter/search by company. | Model tests, contact page tests, access isolation tests. |
| 3 | Add scoped contact custom fields. | Admin can define fields. Contact create/edit/detail renders fields. Values are searchable where configured. | Request validation, UI smoke, persistence tests, permission tests. |
| 4 | Profile UX and automation integration. | Contact detail page shows profile sections, company, methods, custom fields, timeline, and related work. | Browser QA, responsive checks, key workflow smoke tests. |
| Operation | Recommended behavior | Why |
|---|---|---|
| Delete contact | Soft-delete the contact or detach identifiers. Preserve conversations, messages, inquiries, tickets, notes, reminders, and attachments unless an admin chooses explicit purge. | Contact is the root of customer history. Accidental hard deletion creates product-wide data loss. |
| Delete identifier | Unlink or archive by default. Hard delete only when no conversation history depends on it or when a purge workflow is explicit. | Conversations currently hang off identifiers, so identifier deletion can remove more than a contact method. |
| Merge contacts | Pick one survivor contact. Move identifiers, company links, field values, tags, inquiries, activities, and relationship pivots with a merge event. | Merge needs reversibility and audit before it becomes a customer-facing operation. |
| Retention/export | Keep a clear distinction between CRM cleanup, privacy deletion, and legal retention. Add export before destructive purge. | Small-business users need simple cleanup, but Z360 still needs defensible data handling. |
Goal: Implement Z360 CRM Profile v2 without breaking current contact, inquiry, inbox, widget, forms, MCP, and agent workflows. Constraints: - Preserve existing contact IDs and conversation history. - Keep contacts.email and contacts.phone as compatibility primary caches. - Treat identifiers as communication identity and future visible contact methods. - Use Laravel migrations, models, observers, requests, services, and focused Pest tests. - Do not add full custom objects or board-builder functionality. Build: 1. Add shared ContactResolver service. 2. Route manual, form, inquiry, widget, import, agent, and MCP create/update through the resolver. 3. Fix contact delete behavior first, especially MCP DeleteContact identifier deletion. 4. Extend identifiers with contact-method metadata and primary flags. 5. Add contact owner field. 6. Add companies and company_contact pivot. 7. Add contact field definitions and values. 8. Update Contacts UI with profile sections, methods, company, owner, and custom fields. 9. Update search/filter surface for company, owner, and primary contact methods. 10. Add audit events for identity links and deletes. Evidence required: - Migration tests pass. - ContactCreation, ContactUpdate, ContactDeletion targeted tests pass. - New tests cover manual create, public form create, inquiry create, widget register, MCP create/update/delete. - Browser smoke confirms contact create/edit/detail on desktop and mobile widths.
Deleting identifiers can cascade conversations. Any delete or merge work must explicitly preserve or intentionally purge conversation history.
If phone, email, visitor ID, form data, and imports use different resolver rules, customers will see duplicate or split histories.
Admins need typed definitions, groups, and visibility controls. Freeform JSON will become unsearchable and hard to automate.
Current observer side effects are already important. Move cross-object creation decisions into an explicit service and keep observers for model lifecycle sync.