Z360 CRM investigation packet | May 18, 2026

Contact CRM model redesign report

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.

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.

Ready for sprint planning
Root object Contact anchors inquiries, form submissions, widget registration, conversations, activities, tags, and agent memory cleanup.
Current profile depth Name, one email, one phone, avatar, tags, favorite, creator.
Hidden strength Identifiers already model email, phone, visitor ID, DND, blocked state, and conversation linkage.
Highest risk Delete and identity behavior are inconsistent across manual, observer, form, inquiry, widget, and MCP paths.

Executive Recommendation

Immediate direction

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 owner Company association Multiple emails Multiple phones Typed custom fields Identity audit

Main constraint

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.

Recommended Build Boundary

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.

Evidence Confidence

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.

Current Z360 Contact System

The product has two practical roots: `contacts` for CRM profile context and `identifiers` for communication identity.

Code-backed visualization
Screenshot note: the local Docker services were not running during this research, so this artifact uses code-backed visual maps instead of live app screenshots. `make ps` returned no active containers.

organizations

  • owns contacts
  • tenant boundary

contacts

  • name
  • email cache
  • phone cache
  • avatar, tags

identifiers

  • email, phone, visitor
  • DND, blocked
  • links to contact

conversations

  • through identifier
  • phone, email, chat, forms
  • messages and attachments

work objects

  • inquiries
  • tickets
  • notes
  • reminders

Creation Paths

Manual contact
UICreate/edit modal with avatar, first, last, email, phone, tags.
Controller`ContactsController::store` writes direct columns.
ObserverSyncs email and phone identifiers.
ResultContact plus identifiers plus activity log.
Inquiry / lead
Form/API/importPasses transient name, email, phone, fields.
Observer`Contact::firstOrCreate(email, phone)`.
ConversationStarts outreach thread from pipeline channel.
ResultInquiry owns pipeline state, contact owns identity.
Public forms
SubmissionValidated name, email, phone, message.
Service`FormsSubmissionService` matches contact.
ConversationCreates forms conversation and first message.
ResultTagged inbound contact and thread.
Widget / chat
AnonymousVisitor identifier can exist without contact.
RegistrationCreates or updates contact by email and phone.
LinkVisitor identifier links to contact.
ResultChat history becomes contact history.

Current Limitations

Profile is too shallow

No first-class company/account, owner, title, address, lifecycle, role, or structured profile sections.

Single visible email and phone

The identifier table can technically hold multiple records, but the profile UI and validation are still built around one email and one phone.

Deletion is risky

Normal contact delete should preserve identifiers by nulling `contact_id`, but MCP delete explicitly deletes identifiers first, which can cascade conversations.

Custom fields are inquiry-scoped

Intake fields live on `InquiryField`; they are not a clean reusable contact profile customization model.

Creation logic is scattered

Controllers, services, observers, widget auth, imports, and MCP tools each encode contact matching behavior.

Unknown contact state is awkward

Database columns are nullable, but most create flows require both email and phone, limiting call-first and chat-first enrichment.

Evidence Highlights

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.

Competitor Benchmarks

The useful pattern is not "copy everything." It is choosing the right amount of CRM structure for Z360's automation product.

HubSpot

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.

GoHighLevel

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.

monday CRM

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.

Pattern Comparison

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.

What To Copy, Defer, Avoid

Copy now

Canonical contact plus company, owner, contact methods, typed fields, visible profile sections, field groups, and a unified timeline.

Defer

Custom objects, association labels, merge queues, field calculations, deep board-style schema configuration, and import mapping UI.

Avoid

Making every form answer a permanent contact field, duplicating opportunity data onto contacts, and letting integrations bypass one resolver.

Proposed Z360 CRM Profile v2

Keep Contact as the durable anchor. Make identity and profile data richer around it.

Recommended

contacts

  • stable ID
  • name
  • primary email cache
  • primary phone cache
  • owner_id

identifiers v2

  • multiple methods
  • label
  • is_primary
  • source
  • verified_at

companies

  • name
  • domain
  • phone
  • website

company_contact

  • role/title
  • relationship
  • is_primary

custom fields

  • definition
  • typed value
  • section/group
  • visibility

Immediate Build Scope

1. Contact identity service

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.

2. Multiple contact methods

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.

3. Company association

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.

4. Contact owner

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.

5. Scoped custom fields

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.

6. Delete and merge safety

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.

Product Impact

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.

Design Tradeoffs

Why not only custom fields?

Company, owner, and contact methods are not just fields. They drive permissions, dedupe, routing, search, automation, and history.

Why keep email/phone on contacts?

Keep them as primary caches for compatibility, but make identifiers the richer source for multiple methods and channel state.

Why not full CRM objects?

Z360 wins by automating customer communication for small businesses. A heavy custom-object platform slows the core roadmap.

ContactResolver Decision Matrix

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.

SQL Artifact List

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.

Proposed Migration Shape

Phase 0: safety and compatibility
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
Phase 1: company/account model
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
Phase 2: contact custom fields
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
Phase 3: audit and merge readiness
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

Backfill, Index, Rollback Notes

Backfill

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.

Indexes

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.

Rollback

Roll back additive fields first, but do not drop identity audit tables until the migration rehearsal confirms no production recovery depends on them.

Migration invariant: no rollout should delete, rewrite, or reassign existing `contact_id`, `identifier_id`, `conversation_id`, or `inquiry_id` values without a reversible audit event.

Implementation Roadmap

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.

Delete / Merge / Retention Policy

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.

Recommended PR Sequence

PR 1 Resolver and delete safety tests. No visible CRM expansion until the root behavior is stable.
PR 2 Identifier/contact-method extensions, primary sync, and compatibility cache behavior.
PR 3 Company and owner model, backend API/query updates, focused UI exposure.
PR 4 Custom field definitions/values, contact create/edit/detail UI, browser QA.

Agent-Ready Task Packet

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.

Risk Register

Conversation loss

Deleting identifiers can cascade conversations. Any delete or merge work must explicitly preserve or intentionally purge conversation history.

Duplicate contacts

If phone, email, visitor ID, form data, and imports use different resolver rules, customers will see duplicate or split histories.

Custom field sprawl

Admins need typed definitions, groups, and visibility controls. Freeform JSON will become unsearchable and hard to automate.

Observer overload

Current observer side effects are already important. Move cross-object creation decisions into an explicit service and keep observers for model lifecycle sync.

Research And Source Notes

Local Z360 evidence

  • `app/Models/Contact.php`, `app/Observers/ContactObserver.php`, `app/Http/Controllers/ContactsController.php`
  • `app/ViewModels/Contacts.php`, `resources/js/pages/contacts/index.tsx`, `resources/js/pages/contacts/contact.tsx`
  • `app/Observers/InquiryObserver.php`, `app/Services/FormsSubmissionService.php`, `app/Http/Controllers/Widget/WidgetAuthController.php`
  • `database/migrations/2025_07_29_112539_create_contacts_table.php`, `2025_07_30_214113_implement_new_contact_system.php`, `2025_10_27_122244_change_identifiers_contact_id_to_null_on_delete.php`

Delegation Used

Z360 audit agent Mapped schema, creation paths, relationships, UX, SQL artifacts, and risks.
HubSpot benchmark Researched objects, associations, properties, dedupe, and customization model.
GoHighLevel benchmark Researched contact-centric CRM, opportunities, custom fields, DND, workflows.
monday benchmark Researched board-based CRM, contacts, accounts, deals, Connect Boards, API schema.