Skip to content

CRM Plugin #76

@olliethedev

Description

@olliethedev

Overview

Add a CRM (Customer Relationship Management) plugin that provides contact management, deal pipelines, and activity tracking. Built to compose naturally with existing plugins — pipelines reuse the Kanban plugin's board/column/card model, and lead capture integrates with the Form Builder plugin.

The goal is a lightweight but genuinely useful CRM, not a full Salesforce replacement.


Core Features

Contacts

  • Contact CRUD (name, email, phone, company, title, notes, tags)
  • Company/organisation records linked to contacts
  • Custom fields per contact (key-value pairs)
  • Activity timeline per contact (notes, calls, emails logged)
  • Search and filter contacts by any field or tag

Deals / Pipeline

  • Deal CRUD (title, value, currency, close date, associated contact/company)
  • Pipeline stages backed by Kanban plugin (boards = pipelines, columns = stages, cards = deals)
  • Drag-and-drop stage progression (inherits from Kanban plugin)
  • Multiple pipelines (Sales, Onboarding, Support, etc.)

Activities

  • Log notes, calls, and emails against a contact or deal
  • Activity types: note | call | email | meeting
  • Due dates and follow-up reminders (stored in DB; notification delivery via lifecycle hook)

Integrations

  • Form Builder lead capture → auto-create contact (onAfterSubmit hook)
  • AI Chat: query contacts, log notes, summarize deal history via natural language

Schema

import { createDbPlugin } from "@btst/stack/plugins/api"

export const crmSchema = createDbPlugin("crm", {
  contact: {
    modelName: "contact",
    fields: {
      firstName:   { type: "string",  required: true },
      lastName:    { type: "string",  required: false },
      email:       { type: "string",  required: false },
      phone:       { type: "string",  required: false },
      company:     { type: "string",  required: false },
      title:       { type: "string",  required: false },
      tags:        { type: "string",  required: false }, // JSON array
      customFields:{ type: "string",  required: false }, // JSON blob
      notes:       { type: "string",  required: false },
      createdAt:   { type: "date",    defaultValue: () => new Date() },
      updatedAt:   { type: "date",    defaultValue: () => new Date() },
    },
  },
  deal: {
    modelName: "deal",
    fields: {
      title:       { type: "string",  required: true },
      value:       { type: "number",  required: false },
      currency:    { type: "string",  defaultValue: "USD" },
      stage:       { type: "string",  required: true },   // column key in Kanban
      pipelineId:  { type: "string",  required: true },   // board id in Kanban
      contactId:   { type: "string",  required: false },
      closeDate:   { type: "date",    required: false },
      status:      { type: "string",  defaultValue: "open" }, // "open" | "won" | "lost"
      createdAt:   { type: "date",    defaultValue: () => new Date() },
      updatedAt:   { type: "date",    defaultValue: () => new Date() },
    },
  },
  activity: {
    modelName: "activity",
    fields: {
      type:        { type: "string",  required: true }, // "note" | "call" | "email" | "meeting"
      body:        { type: "string",  required: true },
      contactId:   { type: "string",  required: false },
      dealId:      { type: "string",  required: false },
      dueAt:       { type: "date",    required: false },
      completedAt: { type: "date",    required: false },
      createdAt:   { type: "date",    defaultValue: () => new Date() },
    },
  },
})

Plugin Structure

src/plugins/crm/
├── db.ts
├── types.ts
├── schemas.ts
├── query-keys.ts
├── client.css
├── style.css
├── api/
│   ├── plugin.ts               # defineBackendPlugin — contact, deal, activity endpoints
│   ├── getters.ts              # listContacts, getDeal, getContactActivities, etc.
│   ├── mutations.ts            # createContact, logActivity, moveDeal
│   ├── query-key-defs.ts
│   ├── serializers.ts
│   └── index.ts
└── client/
    ├── plugin.tsx              # defineClientPlugin — all CRM routes
    ├── overrides.ts            # CrmPluginOverrides
    ├── index.ts
    ├── hooks/
    │   ├── use-crm.tsx         # useContacts, useContact, useDeals, usePipeline
    │   └── index.tsx
    └── components/
        └── pages/
            ├── contacts-page.tsx / .internal.tsx       # Contact list + search
            ├── contact-detail-page.tsx / .internal.tsx # Contact detail + activity timeline
            ├── edit-contact-page.tsx / .internal.tsx   # Create / edit contact
            ├── pipeline-page.tsx / .internal.tsx       # Kanban-backed deal pipeline
            └── deal-detail-page.tsx / .internal.tsx    # Deal detail + linked contact

Routes

Route Path Description
contacts /crm/contacts Contact list with search + filter
contactDetail /crm/contacts/:id Contact detail + activity timeline
editContact /crm/contacts/:id/edit Edit contact
newContact /crm/contacts/new Create contact
pipeline /crm/pipeline/:pipelineId Deal pipeline (Kanban view)
dealDetail /crm/deals/:id Deal detail

Kanban Plugin Composition

Deal pipelines are rendered using the Kanban plugin's board component in a CRM-aware wrapper rather than reimplementing drag-and-drop:

// Each pipeline is a Kanban board with CRM-specific card content
// Stages map to Kanban columns; deals map to Kanban cards
// Moving a card (deal) calls crm.moveDeal() to update the stage field

Form Builder Integration

Auto-create a contact when a lead capture form is submitted:

formBuilderBackendPlugin({
  onAfterSubmit: async (submission, ctx) => {
    if (submission.formId === LEAD_FORM_ID) {
      await myStack.api.crm.createContact({
        firstName: submission.data.name,
        email: submission.data.email,
      })
    }
  },
})

AI Chat Integration

Contact detail and deal pages register AI context for natural language queries:

useRegisterPageAIContext({
  routeName: "crm-contact-detail",
  pageDescription: `Contact: ${contact.firstName} ${contact.lastName}\nCompany: ${contact.company}\n\nRecent activities:\n${activities.map(a => `- ${a.type}: ${a.body}`).join("\n")}`,
  suggestions: ["Summarize this contact's history", "Draft a follow-up email", "Log a call note"],
  clientTools: {
    logActivity: async ({ type, body }) => {
      await fetch(`/api/data/crm/contacts/${contactId}/activities`, { method: "POST", body: JSON.stringify({ type, body }) })
      return { success: true }
    },
  },
})

Backend API Surface

const contacts = await myStack.api.crm.listContacts({ tag: "enterprise" })
const contact  = await myStack.api.crm.getContactById("contact-id")
const deals    = await myStack.api.crm.listDeals({ pipelineId: "sales", status: "open" })

SSG Support

CRM is auth-gated and per-user by nature — prefetchForRoute is not applicable. Use dynamic = "force-dynamic" on all CRM pages.


Non-Goals (v1)

  • Email sending / sequences (use Newsletter plugin)
  • Calendar integration / meeting scheduler (use Calendar Booking plugin)
  • Lead scoring
  • Reporting / revenue forecasting dashboards
  • Multi-user role permissions within the CRM

Plugin Configuration Options

Option Type Description
apiBaseURL string Base URL for API calls
apiBasePath string API route prefix
siteBasePath string Mount path
queryClient QueryClient Shared React Query client
hooks CrmPluginHooks onBeforeCreateContact, onAfterCreateContact, etc.

Documentation

Add docs/content/docs/plugins/crm.mdx covering:

  • Overview — contacts, deals, activities; composition with Kanban + Form Builder
  • SetupcrmBackendPlugin + crmClientPlugin
  • Kanban composition — how pipelines map to Kanban boards
  • Form Builder lead captureonAfterSubmit hook pattern
  • AI Chat integration — contact context + logActivity tool
  • Schema referenceAutoTypeTable for all config + hooks
  • Routes — table of route keys, paths, descriptions

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions