Files
HiveGrid-Wireframes/hivegrid-cloud/docs/feature-decisions/crm-decision-report.md
T

16 KiB

CRM Decision Report

Decision

Decision: extract-plugin.

CRM is an optional CRM vertical/plugin candidate, not boilerplate Core and not an immediate-removal target. The retained core should provide auth, tenancy, admin/app shell, rows/entities runtime, transactional email, and stable extension points; CRM should own its product opinion around companies, contacts, opportunities, submissions, marketing subscriber state, CRM-specific routes, icons, commands, translations, and any Email Marketing coupling that depends on contacts as campaign recipients.

This slice does not change runtime code. It records the canonical decision and execution order for a later extraction/removal slice.

Scope

In scope for this decision:

  • Classify CRM as an optional plugin/vertical with action extract-plugin.
  • Identify current CRM surfaces and couplings that a future extraction/removal task must audit.
  • Preserve retained CRM code, data, and docs surfaces until a dedicated implementation slice moves or removes CRM. Default core route registration disables CRM route trees.
  • Define a sequential, atomic execution plan and verification plan for later work.

Out of scope for this decision:

  • Deleting CRM routes, modules, entity templates, translations, or command entries in this slice.
  • Refactoring Email Marketing, Rows/Entities, user sync, or sidebar code in this slice.
  • Deciding final packaging mechanics for a future plugin runtime beyond the extraction order and required checks.

Human-review notes that should not block execution:

  • Confirm whether Email Marketing remains a plugin that depends on CRM contacts, or whether it should introduce a separate audience/list abstraction before CRM extraction.
  • Confirm whether existing installations with CRM entities should receive a migration path, export instructions, or compatibility shim before CRM is disabled by default.
  • Confirm whether submission remains part of CRM or is split into a generic contact-form/audience plugin.

Current Code Surfaces

The current known CRM surfaces are retained plugin-candidate code surfaces. Route paths listed here are not active default core route registrations:

Surface Current role Future handling
apps/web/app/modules/crm CRM module root; currently includes apps/web/app/modules/crm/services/CrmService.ts. Move to plugin package/module or delete only after dependents are removed.
apps/web/app/modules/crm/services/CrmService.ts Validates CRM entity presence and reads/writes company/contact/opportunity/submission rows; handles contact form, newsletter fallback, ConvertKit fallback, user sync helpers, and CRM summaries. Plugin-owned service; extraction must replace core callers with an interface, feature flag, or no-op path.
apps/web/app/routes/admin/crm.tsx Admin CRM layout route; validates CRM entities and builds CRM icon navigation including Overview, entity routes, and Sync. Plugin admin route shell.
apps/web/app/routes/admin/crm/* Admin CRM nested routes for overview/entity/sync flows. The current filesystem contains apps/web/app/routes/admin/crm as the route directory. Move/delete atomically with admin CRM layout and route registration.
apps/web/app/routes/app.$tenant/crm.tsx Tenant CRM layout route; validates tenant CRM entities and builds tenant CRM icon navigation. Plugin tenant route shell.
apps/web/app/routes/app.$tenant/crm/* Tenant CRM nested routes for overview/entity flows. The current filesystem contains apps/web/app/routes/app.$tenant/crm as the route directory. Move/delete atomically with tenant CRM layout and route registration.
apps/web/app/modules/templates/defaultEntityTemplates.ts Defines CRM_ENTITIES_TEMPLATE for company, opportunity, contact, and submission plus relationships. Decide whether the template is plugin-owned, installable as optional seed data, or removed from default template loading.
apps/web/app/utils/api/.server/EntitiesApi.ts Provides CRM-specific entity route generation for admin CRM and tenant CRM paths. Remove CRM branches from core or make route generation plugin-aware.
apps/web/app/services/command-registry/commandRegistry.ts Owns command palette actions. CRM command entries are currently absent from the generic registry. Keep CRM commands out of core or register them through plugin command contribution.
apps/web/app/modules/emailMarketing/services/EmailMarketingService.ts Imports CrmService and ContactDto; campaign previews/sends require contact rows, marketingSubscriber, and contact variable replacement. Must be audited before CRM extraction; Email Marketing either depends on CRM plugin or receives its own audience abstraction.
apps/web/app/utils/db/users.db.server.ts Imports CrmService and ContactStatus; user creation/update paths can create CRM contacts and log CRM sync. Move CRM sync to plugin event subscriber or guard behind optional plugin availability.
apps/web/public/locales/en/translations.json Contains crm.title and crm.settings.* strings. Move/remove CRM translations with plugin UI.
apps/web/public/locales/de/translations.json Contains crm.title and crm.settings.* strings. Move/remove CRM translations with plugin UI.
docs/feature-manifests/plugin-candidates.json Lists CRM as plugin candidate with code areas apps/web/app/modules/crm, apps/web/app/routes/admin/crm*, and apps/web/app/routes/app.$tenant/crm*; pruning action is extract-plugin. Keep consistent with this decision report and later split into a CRM-specific plugin manifest if extraction begins.

Additional evidence from current inventory:

  • docs/feature-inventory.md classifies CRM as Optional/Plugin because it is a strong product opinion coupled to default entities and Email Marketing.
  • The filesystem still retains apps/web/app/routes/admin/crm, apps/web/app/routes/admin/crm.tsx, apps/web/app/routes/app.$tenant/crm, and apps/web/app/routes/app.$tenant/crm.tsx, while default core route discovery excludes those plugin trees.
  • apps/web/app/modules/crm/services/CrmService.ts depends on Rows/Entities APIs, row relationships, row values, notifications, cache helpers, and default entity names.

Couplings

Rows and default entities

CRM is coupled to the generic rows/entities runtime through entity names and slugs rather than isolated CRM tables. CrmService.validate() requires company, opportunity, and contact, plus submission for admin/global CRM. The service uses rows APIs and row relationship APIs for contact, company, opportunity, and submission behavior. A future extraction must preserve rows/entities as core infrastructure while ensuring CRM entity templates are optional plugin seed data.

Email Marketing

apps/web/app/modules/emailMarketing/services/EmailMarketingService.ts directly imports CrmService and ContactDto. It uses CRM contacts as campaign recipients, blocks sends to contacts without marketingSubscriber, replaces email variables from contact fields, and loads contacts from contact entity views. This is the most important downstream dependency: CRM cannot be removed from core unless Email Marketing is either extracted together with CRM, made dependent on the CRM plugin, or refactored to a separate audience/contact-list abstraction.

User lifecycle sync

apps/web/app/utils/db/users.db.server.ts imports CrmService and ContactStatus and can create CRM contact rows for users. The future extraction must move this behavior into plugin-owned lifecycle hooks or guard it so core user persistence no longer imports CRM.

Route and entity routing helpers

apps/web/app/utils/api/.server/EntitiesApi.ts contains route-generation branches for admin CRM and tenant CRM paths. These branches make CRM route paths part of shared entity routing. A future extraction must either remove those branches or replace them with plugin-provided route builders.

Navigation and commands

apps/web/app/services/command-registry/commandRegistry.ts is the current command-registration surface. CRM command entries are not registered in the generic registry; the admin/app CRM layout routes still build CRM-specific local navigation with CRM icons. A future extraction must keep command registration plugin-owned and remove route/sidebar visibility unless the CRM plugin is installed.

Translations and icons

CRM strings live in shared locale files under crm.*. CRM route layouts import CRM-specific icon components from ~/components/ui/icons/crm/*. A future extraction must move translations and icon usage with the plugin or delete them when CRM is removed.

Manifest and documentation

docs/feature-manifests/plugin-candidates.json already classifies CRM as a plugin candidate. This report becomes the human-readable canonical decision; T02 should verify that report references remain resolvable and consistent with the manifest.

Schema and Template Impact

CRM does not appear as a dedicated Prisma model surface in the reviewed task inputs. Its data model is represented through the rows/entities runtime and CRM_ENTITIES_TEMPLATE in apps/web/app/modules/templates/defaultEntityTemplates.ts:

  • company with slug companies, type AdminOnly, properties such as name, address, phone, source, and logo.
  • opportunity with slug opportunities, type AdminOnly, properties such as name, value, and expectedCloseDate.
  • contact with slug contacts, type AdminOnly, properties such as firstName, lastName, email, jobTitle, status, and marketingSubscriber.
  • submission with slug submissions, type AdminOnly, properties such as users and message.
  • Relationships: company to contact, company to opportunity, and contact to submission.

Future extraction decision for templates must be explicit and atomic:

  1. If CRM becomes an installable plugin, move CRM_ENTITIES_TEMPLATE into plugin seed/install code and keep core template loading independent from CRM.
  2. If CRM is removed for minimal boilerplate builds, remove default availability of company/contact/opportunity/submission templates and document migration/export for existing installations.
  3. If Email Marketing still needs contacts, do not remove contact/marketingSubscriber until Email Marketing owns or imports an audience template.

Sidebar Command Docs Impact

Sidebar and route navigation impact:

  • Admin CRM route code is retained at apps/web/app/routes/admin/crm.tsx and nested apps/web/app/routes/admin/crm/* routes, but is disabled in default core route registration.
  • Tenant CRM route code is retained at apps/web/app/routes/app.$tenant/crm.tsx and nested apps/web/app/routes/app.$tenant/crm/* routes, but is disabled in default core route registration.
  • CRM layout routes build their own icon subnavigation for Overview, opportunities, companies, contacts, submissions, and, in admin, Sync.
  • Any global AdminSidebar/AppSidebar references discovered in a future implementation slice must be removed or made plugin-aware. Current sidebar tests already assert some optional admin routes, including canonical admin-host CRM paths, are not present in the admin sidebar serialization.

Command impact:

  • apps/web/app/services/command-registry/commandRegistry.ts is the command registry surface and should remain free of CRM commands unless a CRM plugin contributes them.
  • Future extraction must remove this core command or move it to plugin command registration.

Docs and i18n impact:

  • apps/web/public/locales/en/translations.json and apps/web/public/locales/de/translations.json contain CRM translations and should move with plugin UI.
  • docs/feature-manifests/plugin-candidates.json must remain aligned with this report; if CRM receives a dedicated plugin manifest later, plugin-candidates.json should point to it or retain a consistent summary entry.

Execution Plan

The future CRM extraction/removal work must run in this sequence, with each step atomic and independently verifiable:

  1. Decision/report: land this extract-plugin decision report and the T02 verifier before runtime changes.
  2. Email Marketing dependency check: inspect apps/web/app/modules/emailMarketing/services/EmailMarketingService.ts, Email Marketing routes, campaign recipient storage, and tests to choose one of three paths: extract Email Marketing together with CRM, declare Email Marketing dependent on CRM plugin, or introduce a non-CRM audience abstraction.
  3. Route/module extraction or deletion: move or remove apps/web/app/modules/crm, apps/web/app/routes/admin/crm.tsx, apps/web/app/routes/admin/crm/*, apps/web/app/routes/app.$tenant/crm.tsx, and apps/web/app/routes/app.$tenant/crm/* in one implementation slice after dependencies are guarded.
  4. Default entity template decision: move CRM_ENTITIES_TEMPLATE from apps/web/app/modules/templates/defaultEntityTemplates.ts into plugin install/seed code, or remove it from default loading only after Email Marketing/user sync no longer requires it.
  5. Command/sidebar/i18n update: remove or plugin-register CRM command palette entries, CRM route/sidebar links, CRM route helpers in apps/web/app/utils/api/.server/EntitiesApi.ts, CRM locale keys in apps/web/public/locales/en/translations.json and apps/web/public/locales/de/translations.json, and CRM docs/manifest entries.
  6. Tests/static scans: add or update static scans and focused tests so core no longer imports ~/modules/crm, CRM routes are absent when the plugin is disabled, Email Marketing behavior is explicit, and docs/manifests stay consistent.

Verification Plan

T02 must add a local static verifier, expected at scripts/verify-crm-decision-report.mjs, that fails when this decision contract drifts. The verifier must check:

  • Required report sections exist: Decision, Scope, Current Code Surfaces, Couplings, Schema and Template Impact, Sidebar Command Docs Impact, Execution Plan, Verification Plan, Risks.
  • The report contains the exact decision token extract-plugin.
  • No TODO, TBD, or unresolved placeholder text exists in the report.
  • Required referenced paths are present in the report and resolvable where they are concrete current paths: apps/web/app/modules/crm, apps/web/app/routes/admin/crm.tsx, apps/web/app/routes/admin/crm/*, apps/web/app/routes/app.$tenant/crm.tsx, apps/web/app/routes/app.$tenant/crm/*, apps/web/app/modules/templates/defaultEntityTemplates.ts, apps/web/app/utils/api/.server/EntitiesApi.ts, apps/web/app/services/command-registry/commandRegistry.ts, apps/web/app/modules/emailMarketing/services/EmailMarketingService.ts, apps/web/app/utils/db/users.db.server.ts, apps/web/public/locales/en/translations.json, apps/web/public/locales/de/translations.json, docs/feature-manifests/plugin-candidates.json.
  • Glob-style references such as apps/web/app/routes/admin/crm/* and apps/web/app/routes/app.$tenant/crm/* resolve to existing route directories or files.
  • docs/feature-manifests/plugin-candidates.json still contains a CRM component and pruning.action remains extract-plugin unless a later migration intentionally updates both manifest and report.

Manual verification for a future runtime extraction should cover:

  • Core app and admin shells start without CRM installed.
  • Email Marketing either works with its declared CRM dependency or fails with a clear disabled-plugin message.
  • User creation/update no longer imports or calls CRM from core when CRM is disabled.
  • Existing CRM installations have a documented export/migration path if runtime deletion is chosen instead of plugin packaging.

Risks

  • Incomplete surface map can cause later removal to miss imports; this report therefore names all known coupling files from the slice research and the task inputs.
  • Email Marketing has direct CRM service and contact DTO coupling, so removing CRM first would break campaign preview/send flows.
  • User lifecycle CRM sync creates hidden coupling outside CRM routes and must be moved or guarded before extraction.
  • CRM entities are generic rows/entities templates, so deleting only route/module code can leave orphaned templates or data.
  • Translation, command, and route-helper leftovers can keep CRM visible or navigable after the module is removed.
  • Existing tenants may rely on CRM rows and relationships; plugin extraction should include compatibility and migration notes rather than silent data loss.