-
Notifications
You must be signed in to change notification settings - Fork 20
Description
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 (
onAfterSubmithook) - 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 fieldForm 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
- Setup —
crmBackendPlugin+crmClientPlugin - Kanban composition — how pipelines map to Kanban boards
- Form Builder lead capture —
onAfterSubmithook pattern - AI Chat integration — contact context +
logActivitytool - Schema reference —
AutoTypeTablefor all config + hooks - Routes — table of route keys, paths, descriptions
Related Issues
- Job Board Plugin #58 Job Board Plugin (similar domain plugin concept)
- Calendar Booking Plugin #40 Calendar Booking Plugin (scheduling complement to CRM)
- Analytics Plugin #74 Analytics Plugin
- Newsletter / Marketing Emails Plugin #75 Newsletter / Marketing Emails Plugin