CRM
Sales plus mail sync, sequences, segments, assignment, enrichment, forecasts, lead scoring and goals, with a pipeline read set.
Is the sales stack with the follow-up and planning blocks around it. Its parts are contacts, pipelines, deals, quotes, goals, activities, mail sync, sequences, segments, assignment, enrichment, forecasts, lead scoring and the queue. It shares closeWon and closeLost with sales.
better-supabase add crmexport default defineConfig({ blocks: { crm: {} } });add lists crm in blocks, writes the modules of every part and wires the client in lib/blocks.ts. Import the manifest from better-supabase/blocks/crm when you build the set with createBlocks.
import { crmBlock } from "better-supabase/blocks/crm";Parts
Installing crm installs these blocks. Each stays an ordinary block in the config and keeps its own client.
contactspipelinesdealsquotesgoalsactivitiesmail-syncsequencessegmentsassignmentenrichmentforecastslead-scoringqueue
Helpers and read set
closeWon and closeLost work as on sales: the deal moves first, then the quote.
crmReadSets.dashboard is a named read set for the pipeline page. It calls deals.board for one pipeline, so the page loads with one round trip. Build it once after better-supabase gen has written the block reads:
import { defineCompositeReadSet } from "better-supabase/blocks";
import { crmReadSets } from "better-supabase/blocks/crm";
import { betterSupabase } from "@/lib/supabase/server";
import { reads } from "@/lib/supabase/blocks.generated";
export const crmDashboard = defineCompositeReadSet(
betterSupabase,
crmReadSets.dashboard,
reads,
);Pass the result to db.$many(crmDashboard, { pipelineId }). defineCompositeReadSet throws a TypeError that names the block read when it is missing, for example when deals is not installed.
route: Pick an owner for a record. It reads the segments the record is in, then runs the assignment queues with the segment names under the segments field, so a queue territory can match a segment. Extra fields are merged in. The helper returns the segments and the pick (or undefined when no queue matched), and writing the owner onto the record stays with the app, as with assignment.assign.
const routed = await blocks.crm.route({
organizationId,
subject: { type: "contact", id: contactId },
fields: { country: "NL" },
});attainment: How the running targets stand. It reads goals.progress for the day (today by default), sums progress and goal per metric so amounts and counts never mix, and lists the targets whose attainment is below the share of their period that has passed.
const { byMetric, pace, behind } = (
await blocks.crm.attainment({ organizationId })
).orThrow();Example
The Next.js example builds the helpers over the clients it already installs, in apps/examples/nextjs/src/features/composites/composites.ts, and calls them from the server actions in composite-actions.ts. routeContact runs route and salesAttainment runs attainment; closeDealWon is the call below:
const closed = await crmOf(db).closeWon({ dealId, quote: true });closeDealWon is that call behind a server action with the deals.write permission. The example keeps its pipeline board on its own pipelineOverview read set, which calls the same deals.board read as crmReadSets.dashboard.
Remove
better-supabase remove crm
better-supabase remove crm --with-partsThe first command replaces crm in blocks with its parts. The second removes the parts too, except any part another block still needs (remove).
Last updated on