focalcrm / core
Headless CRM Core Engine for Focal (Contacts, Companies, Custom Properties, Associations, Timeline)
v0.2.3
2026-10-02 00:53 UTC
Requires
- php: ^8.3
- laravel/framework: ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^3.8 || ^4.0 || ^5.0
- pestphp/pest-plugin-laravel: ^3.0 || ^4.0 || ^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
This is a read-only split of the focalcrm/focal monorepo. Please open issues and pull requests there.
The headless CRM foundation engine for the Focal RevOps platform. Manages contacts, companies, extensible custom properties (EAV), polymorphic associations, activity audit timelines, list segmentation, and lifecycle stage state transitions.
Architecture & Capabilities
+-------------------------------------------------------------------------+
| FOCAL CORE |
| |
| +--------------------+ +--------------------+ +-----------------+ |
| | Contacts | | Companies | | Custom Objects | |
| +--------------------+ +--------------------+ +-----------------+ |
| \ / / |
| v v v |
| +---------------------------------------------------------+ |
| | Polymorphic Association Engine | |
| | (1:1, 1:N, M:N with directional cardinalities) | |
| +---------------------------------------------------------+ |
| | | | |
| v v v |
| +--------------------+ +--------------------+ +-----------------+ |
| | Custom Properties | | Activity Timelines | | Lifecycle State | |
| | (EAV + Audit Log) | | (Calls, Meetings) | | Machine & Health| |
| +--------------------+ +--------------------+ +-----------------+ |
+-------------------------------------------------------------------------+
Core Features
- Extensible EAV Properties: Dynamically define and validate custom properties (
text,number,date,datetime,boolean,select,multiselect,json) across any CRM record, complete with full historical audit trails (PropertyHistory). - Polymorphic Association Engine: Connect any record to any other record (
Contact,Company,Deal,Ticket, or custom object) with directional types and enforced cardinality (OneToOne,OneToMany,ManyToMany). - Unified Activity Timeline: Log calls, notes, emails, meetings, tasks, WhatsApp, SMS, and LinkedIn messages with polymorphic subject attachment.
- Strict Lifecycle State Machine: Enforce deterministic progression (
Subscriber->Lead->MQL->SQL->Opportunity->Customer->Evangelist), with full transition tracking and velocity metrics. - Automated Association & Domain Extraction: Automatically match inbound contacts to companies using corporate email domains while filtering out 4,000+ public freemail providers (
gmail.com,yahoo.com, etc.). - Deduplication & Record Merging: Identify contact/company duplicates via exact or fuzzy matchers, and safely merge secondary records into a primary survivor while repointing associations, activities, and property histories.
- Dynamic & Static Segmentation: Filter and maintain active CRM audience lists (
CrmList) using declarative condition criteria. - Customer Health Scoring: Calculate multidimensional health scores (
Healthy,At Risk,Critical) derived from activity recency, open support tickets, and deal interactions.
Installation
composer require focalcrm/core
Publish migrations and configuration (optional):
php artisan vendor:publish --tag=focal-core-migrations php artisan vendor:publish --tag=focal-core-config
Run migrations:
php artisan migrate
Quick Start & Code Examples
1. Creating Contacts & Automatic Company Association
use Focal\Core\Actions\CreateContactAction; use Focal\Core\Enums\LifecycleStage; $contact = app(CreateContactAction::class)->execute([ 'first_name' => 'Jane', 'last_name' => 'Doe', 'email' => 'jane@acme.corp', 'lifecycle_stage' => LifecycleStage::Lead, 'custom_properties' => [ 'job_title' => 'VP of Engineering', 'annual_cloud_budget' => 150000, ], ]); // If Acme Corp exists with domain "acme.corp", Jane is automatically associated.
2. Polymorphic Associations
use Focal\Core\Actions\AssociateRecordsAction; use Focal\Core\Models\AssociationType; use Focal\Core\Enums\AssociationCardinality; $type = AssociationType::firstOrCreate([ 'name' => 'billing_contact', 'label' => 'Billing Contact', 'cardinality' => AssociationCardinality::OneToOne, ]); app(AssociateRecordsAction::class)->execute( from: $company, to: $contact, type: $type );
3. Lifecycle Stage State Transitions
use Focal\Core\Actions\TransitionLifecycleStageAction; use Focal\Core\Enums\LifecycleStage; // Transitions validate against allowed paths and record a LifecycleStageTransition audit entry app(TransitionLifecycleStageAction::class)->execute( record: $contact, newStage: LifecycleStage::Mql, actorId: auth()->id() );
4. Logging Timeline Activities
use Focal\Core\Actions\LogActivityAction; use Focal\Core\Enums\ActivityType; app(LogActivityAction::class)->execute([ 'type' => ActivityType::Call, 'subject' => 'Q4 Contract Review', 'body' => 'Client agreed to proceed with annual enterprise tier.', 'subjectable' => $contact, 'author_id' => auth()->id(), 'metadata' => [ 'call_duration_seconds' => 940, 'recording_url' => 'https://storage.acme.corp/calls/rec_9281.mp3', ], ]);
5. Deduplication & Record Merging
use Focal\Core\Actions\FindDuplicateContactsAction; use Focal\Core\Actions\MergeContactsAction; // Discover duplicates by email or normalized name $duplicates = app(FindDuplicateContactsAction::class)->execute($contact); if ($duplicates->isNotEmpty()) { // Merges secondary into primary, re-parenting associations, logs, and properties app(MergeContactsAction::class)->execute( primary: $contact, secondary: $duplicates->first(), actorId: auth()->id() ); }
Data Models & Schema Reference
| Model | Table | Responsibility |
|---|---|---|
Contact |
contacts |
Individual persons, emails, phones, lifecycle stages, and custom properties. |
Company |
companies |
Organizations, verified corporate domains, employee sizes, and health scores. |
PropertyDefinition |
property_definitions |
Dynamic property registry specifying data types, validation rules, and target entities. |
PropertyHistory |
property_histories |
Point-in-time EAV change logs with actor attribution. |
Association |
associations |
Polymorphic graph connecting any two CRM records. |
AssociationType |
association_types |
Relationship taxonomy defining cardinality and directional semantics. |
Activity |
activities |
Time-stamped touchpoints (calls, meetings, notes, tasks) attached to entities. |
CrmList |
crm_lists |
Static or dynamic segmentation rules and filter definitions. |
LifecycleStageTransition |
lifecycle_stage_transitions |
Immutable history of record lifecycle stage movements. |
CustomObjectDefinition |
custom_object_definitions |
Schema definitions for custom user-created entities. |
CustomObjectRecord |
custom_object_records |
Concrete instances of user-defined entities. |
Testing
vendor/bin/pest packages/core/tests --compact