BTST

CRM

Contacts, companies, Kanban deal pipelines, and activity timelines

Full-stackDependentReleased · Preview

Best for

Teams adding a lightweight sales or relationship workflow to an existing React application.

Track contacts, deal pipelines, and follow-ups inside your application.

BTST supplies

  • Contact and company records, custom fields, tags, and activity timelines
  • Deal pipelines backed by Kanban boards, columns, and cards
  • Follow-up reminder hooks, Form Builder lead capture, and AI page tools

You supply

  • A database adapter with isolated transaction support and the Kanban plugin
  • Authenticated identities and CRM authorization rules
  • A reminder delivery hook and recurring job when reminders are enabled
  • Form Builder and AI Chat registration when those integrations are used

You own and customize

Contacts, companies, deals, and activities stay in your database. Your application owns authentication, authorization, and reminder delivery.

Compatibility and dependencies

Maintained: Next.js 15+ App Router, React Router v7, TanStack Start.

Requires: Kanban plugin; A BTST database adapter.

External services: None required.

From registration to result

A semantic workflow, not a setup shortcut

  1. 1Capture a contact

    Create a contact with tags and custom fields, or connect a lead capture form.

  2. 2Progress a deal

    Move deals through Kanban stages and keep their linked contacts in view.

  3. 3Follow up

    Log interactions, set due dates, and deliver reminders through a lifecycle hook.

The CRM plugin manages contacts and companies, tracks deals through multiple pipelines, and records notes, calls, emails, and meetings. Pipelines reuse Kanban boards, columns, and cards. Form Builder and AI Chat are optional integrations.

Setup

Register CRM and Kanban under their canonical keys. CRM reuses Kanban's board, column, and task model for pipelines and deals.

lib/stack.ts
import { createBackendStack } from "@btst/stack/api"
import { crmBackendPlugin } from "@btst/stack/plugins/crm/api"
import { kanbanBackendPlugin } from "@btst/stack/plugins/kanban/api"

export const myStack = createBackendStack({
  adapter,
  auth: serverAuth,
  basePath: "/api/data",
  plugins: {
    kanban: kanbanBackendPlugin(),
    crm: crmBackendPlugin(),
  },
})

Create the client stack with the same shared API and site paths used by your application:

lib/stack-client.tsx
import { createClientStack } from "@btst/stack/client"
import { crmClientPlugin } from "@btst/stack/plugins/crm/client"
import { kanbanClientPlugin } from "@btst/stack/plugins/kanban/client"

export function getStackClient(queryClient) {
  return createClientStack({
    queryClient,
    api: { baseURL: "https://example.com", basePath: "/api/data" },
    site: { baseURL: "https://example.com", basePath: "/pages" },
    plugins: {
      kanban: kanbanClientPlugin(),
      crm: crmClientPlugin(),
    },
  })
}
globals.css
@import "@btst/stack/plugins/kanban/css";
@import "@btst/stack/plugins/crm/css";

btst init --plugins crm automatically includes Kanban. Supply your database adapter, authentication provider, and authorization rules before using the private CRM routes.

Authorization

Register crmPermissions from @btst/stack/plugins/crm/permissions in your shared authorization contract. CRM request operations require explicit grants; unauthenticated or unconfigured requests cannot read CRM data.

CRM requires configured stack authorization at setup. Reading pipelines also requires crm.deal.read, because pipeline responses include their deal metadata.

Permissions cover contact, company, deal, pipeline, activity, and reminder. For example, an application can restrict CRM management to admins:

crm.contact.read.when(({ identity }) => identity?.role === "admin")
crm.contact.create.when(({ identity }) => identity?.role === "admin")
crm.deal.move.when(({ identity }) => identity?.role === "admin")

Apply equivalent rules for the remaining operations your application enables. CRM records are workspace-wide: the plugin does not ship owner fields or row isolation between users or tenants. Grant access only to the intended workspace or CRM administrators, and use a separate data boundary when tenants must be isolated. CRM does not provide a multi-user role management interface. Trusted jobs deliberately bypass request authorization.

Contacts and companies

Contacts support first and last name, email, phone, company, title, notes, tags, and string custom fields. Link a contact to a company record with companyId. Companies have a name, domain, phone, and notes.

const crm = myStack.forRequest(request).operations.crm
const contact = await crm.createContact({
  firstName: "Avery",
  lastName: "Chen",
  email: "avery@example.com",
  tags: ["enterprise"],
  customFields: { region: "North America" },
})
const contacts = await crm.listContacts({ tag: "enterprise" })
const detail = await crm.getContactById({ id: contact.id })

Contact search includes ordinary fields, tags, and custom fields. List results use { items, total }.

Contact and deal relation fields search the server beyond the initial results while retaining the selected contact or company when the search changes.

Kanban composition

A pipeline is a Kanban board, a stage is one of its columns, and a deal links to a Kanban task. CRM projects a deal's title, stage, and pipeline from that task, so moving the same card through Kanban keeps CRM reads consistent.

const crm = myStack.trusted.crm
const pipeline = await crm.createPipeline({
  name: "Sales",
  slug: "sales",
})
const deal = await crm.createDeal({
  title: "Annual support plan",
  pipelineId: pipeline.id,
  stage: pipeline.columns[0].id,
  value: 12000,
  currency: "USD",
  contactId: contact.id,
})
await crm.moveDeal({ id: deal.id, stage: pipeline.columns[1].id })

Create additional pipelines for onboarding, support, or other workflows. Deal records also support company links, close dates, and open, won, or lost status. The pipeline view reuses Kanban's drag-and-drop interaction. Its CRM-aware wrapper uses KanbanBoard's optional renderColumn callback to provide deal links and values while the shared board owns drag-and-drop behavior.

Activities and reminders

Log a note, call, email, or meeting against a contact, a deal, or both:

await myStack.trusted.crm.logActivity({
  type: "call",
  body: "Reviewed the support requirements; send the proposal next week.",
  contactId: contact.id,
  dueAt: "2026-12-01T10:00:00.000Z",
})

Set completedAt when the follow-up is complete. Reminder dates are stored in the database. Configure delivery through onActivityReminder and call sendDueReminders from your scheduler:

crmBackendPlugin({
  hooks: {
    onActivityReminder: async (activity) => {
      await deliverReminder(activity)
    },
  },
})

// In your application's recurring server job:
await myStack.trusted.crm.sendDueReminders({})

Successful deliveries are marked so later runs do not resend the same reminder. CRM does not schedule the recurring job or send email itself.

Form Builder lead capture

Use Form Builder's onAfterSubmission lifecycle hook. Check the form identity and validate its saved JSON data before creating a contact:

import { z } from "zod"
import { formBuilderBackendPlugin } from "@btst/stack/plugins/form-builder/api"

const leadSchema = z.object({
  name: z.string().trim().min(1),
  email: z.email(),
})

formBuilderBackendPlugin({
  hooks: {
    onAfterSubmission: async (submission, form) => {
      if (form.slug !== "sales-enquiry") return
      const lead = leadSchema.parse(JSON.parse(submission.data))
      await myStack.trusted.crm.createContact({
        firstName: lead.name,
        email: lead.email,
        tags: ["website-lead"],
      })
    },
  },
})

The trusted operation is appropriate here because the application selected the form and validated the lead. For a request-driven CRM action, use myStack.forRequest(request).operations.crm instead.

AI Chat integration

CRM contact and deal pages register page descriptions and recent activities with AI Chat. Suggested prompts summarize history and draft follow-ups. The registered client tools query contacts and log activities through the same CRM API and authorization checks as the visible interface.

Mount PageAIContextProvider above the application's StackProvider instances and enable AI Chat as described in AI Chat. CRM does not require an AI provider for its ordinary contact, deal, or activity screens.

Routes

Paths below are relative to the client stack's site base path.

Route keyPathPurpose
contacts/crm/contactsSearch and filter contacts
contactDetail/crm/contacts/:idContact details and activity timeline
editContact/crm/contacts/:id/editEdit contact fields
newContact/crm/contacts/newCreate a contact
companies/crm/companiesManage company records
pipelines/crm/pipelinesCreate and choose pipelines
pipeline/crm/pipeline/:pipelineIdKanban deal pipeline
dealDetail/crm/deals/:idDeal details and activities

CRM data is private and request-dependent. Do not statically generate CRM pages. Next.js catch-all pages serving CRM should use export const dynamic = "force-dynamic".

Configuration and schema reference

Shared API URLs, site paths, and the QueryClient belong on createClientStack, not plugin overrides. The client config exposes plugin-specific page components, loader hooks, and the request identity partition used for SSR hydration.

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type