npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/crm

v0.9.0

Published

Engine-factory CRM package — leads, accounts, contacts, opportunities, pipelines, campaigns, subscriptions, activities, notes, email engagement. PACKAGE_RULES compliant; built on Mongoose + mongokit + arc primitives.

Readme

@classytic/crm

Engine-factory CRM package for the Classytic commerce stack — leads, accounts, contacts, opportunities, pipelines, campaigns, double-opt-in subscriptions, polymorphic activities + notes, email-engagement events, and conversation/message threads. Built on **Mongoose + @classytic/mongokit

  • @classytic/repo-core + @classytic/primitives**, PACKAGE_RULES compliant.

Architecture (0.4.0+): this is an engine-factory mongokit package, NOT the pure-TS port-based design earlier docs described. createCrm({ connection }) returns Mongoose models, Repository subclasses (domain verbs live on the repos), and an EventTransport. The ports / adapters / in-memory-repo layer is gone.

  • Repositories ARE the API. Each repo extends Repository<TDoc> from mongokit and inherits CRUD, pagination, query parsing, aggregation, and transactions. Domain verbs (qualify, convert, win, subscribe, confirm, merge, …) live on the same class.
  • Atomic state transitions. Every lifecycle move goes through assertAndClaim (state-machine legality check + repo.claim() CAS). Concurrent qualifiers / stage-moves / activations can't both win; race-losses surface as typed transition errors.
  • Index-backed dedup. Lead email, campaign name, and live (contact, campaign) subscriptions are guarded by partial-unique indexes; the residual E11000 is caught via isDuplicateKeyError and re-thrown as the same typed domain error the pre-check emits — no check-then-save TOCTOU window.
  • Multi-tenant by default. multiTenantPlugin is wired on every repo with required: true; tenant scope is injected at POLICY priority and cannot be forgotten. A read with no organizationId throws.
  • Event-driven extension plane. Every lifecycle change publishes a crm:<resource>.<verb> event through an arc-compatible EventTransport. Automation (outreach, AI follow-up, analytics) subscribes with globs; none of it lives in this package.

Install

npm install @classytic/crm

Peers: @classytic/mongokit >=3.16.0, @classytic/repo-core >=0.6.0, @classytic/primitives >=0.7.1, mongoose >=9.4.1, zod >=4.0.0.

Quick start

import mongoose from 'mongoose';
import { createCrm, LeadService } from '@classytic/crm';

const crm = await createCrm({
  connection: mongoose.connection,
  tenantFieldType: 'objectId', // matches Better Auth orgs; 'string' for UUID hosts
});

const ctx = { organizationId: orgId };

// Pipeline setup.
const pipeline = await crm.repositories.pipeline.create(
  {
    name: 'New Business',
    stages: [
      { name: 'Prospecting', sequence: 1, defaultProbability: 0.1 },
      { name: 'Discovery',   sequence: 2, defaultProbability: 0.3 },
      { name: 'Proposal',    sequence: 3, defaultProbability: 0.6 },
      { name: 'Negotiation', sequence: 4, defaultProbability: 0.8 },
    ],
  },
  ctx,
);

// Inbound lead — auto-scores + emits crm:lead.created.
const lead = await crm.repositories.lead.createLead(
  {
    email: '[email protected]',
    firstName: 'Alex',
    lastName: 'Rivera',
    companyName: 'Acme',
    jobTitle: 'Director',
    source: 'web-form',
  },
  ctx,
);
const leadId = String(lead._id);

// Qualify, then convert — spawns Account + Contact + Opportunity.
await crm.repositories.lead.markContacted(leadId, {}, ctx);
await crm.repositories.lead.qualify(leadId, {}, ctx);

const leads = LeadService.fromEngine(crm);
const { opportunityId } = await leads.convert(
  leadId,
  { pipelineId: String(pipeline._id), amount: 120_000, currency: 'USD' },
  ctx,
);

// Move through the pipeline (atomic CAS on stageId).
const stages = pipeline.stages;
await crm.repositories.opportunity.moveToStage(opportunityId, { stageId: String(stages[1]._id) }, ctx);
await crm.repositories.opportunity.moveToStage(opportunityId, { stageId: String(stages[2]._id) }, ctx);

// Close as won — terminal, immutable.
await crm.repositories.opportunity.win(opportunityId, { amount: 125_000 }, ctx);

// React to events from anywhere.
crm.events.subscribe('crm:opportunity.*', (e) => console.log(e.type));

Atomic conversion under a transaction

LeadService.convert() writes across four repositories (Account, Contact, Opportunity, Lead). Wrap the call in connection.transaction() and thread the session through ctx.session so all four writes commit or roll back together (requires a replica set):

await mongoose.connection.transaction(async (session) => {
  await leads.convert(leadId, { pipelineId, amount: 120_000 }, { ...ctx, session });
});

State machines

Lead:         new → contacted → qualified → converted
              new/contacted/qualified → disqualified | nurturing
              nurturing → contacted | qualified

Opportunity:  open → won | lost | abandoned   (terminal; stage moves while open)

Campaign:     draft → active ⇄ paused → completed → archived

Subscription: pending → confirmed → unsubscribed | bounced | complained

Activity:     planned → in_progress → completed
              planned/in_progress → cancelled

Every transition is CAS-guarded and appends to the record's statusHistory. Invalid transitions throw typed errors; *_TRANSITIONS maps are the canonical source of truth.

Lead scoring, forecast, conversion analytics

import { composeScorers, forecast, pipelineConversion } from '@classytic/crm';

// Custom scorer (replaces defaultLeadScorer at repo construction):
const scorer = composeScorers(
  (l) => (l.email ? 20 : 0),
  (l) => (l.source === 'referral' ? 30 : 0),
  (l) => (/ceo|cto|founder/i.test(l.jobTitle ?? '') ? 40 : 0),
);

// Pure analytics over opportunity lists (no I/O):
const open = await crm.repositories.opportunity.findAll({ status: 'open' }, ctx);
const rows = forecast(open); // ISO-month buckets: committed / weighted / count
const conv = pipelineConversion(open, pipeline.stages.map((s) => String(s._id)));

Events catalog (rule 18)

Every CRM_EVENTS constant has a Zod definition in crmEventDefinitions, exported from the package root and the ./events subpath:

import { crmEventDefinitions, CRM_EVENTS } from '@classytic/crm/events';

for (const def of crmEventDefinitions) registry.register(def); // arc EventRegistry

Hosts wire these into arc's EventRegistry for publish-time validation and OpenAPI introspection. Subscribe glob-style with any arc transport (Memory, Redis, Kafka, BullMQ):

crm.events.subscribe('crm:lead.*',          (e) => routeLead(e));
crm.events.subscribe('crm:opportunity.won', (e) => createOrder(e));

Event families: crm:lead.*, crm:account.*, crm:contact.*, crm:opportunity.*, crm:pipeline.*, crm:campaign.*, crm:subscription.*, crm:activity.*, crm:note.*, crm:email_event.*, crm:conversation.*, crm:message.*.

Production boot

// Build indexes from a deploy script, not on cold boot (rule 35):
const crm = await createCrm({ connection, autoIndex: false });
// once per deploy:
await crm.syncIndexes();

The engine asserts the backend's required capabilities at boot (upsert, getOrCreate, duplicateKeyError, and transactions unless allowNonTransactional: true) so a misconfigured deployment fails loud instead of mid-convert.

What this package does NOT do

  • No HTTP routes / auth / RBAC — the host (arc) owns those.
  • No calendar / inbox integration — bridge at the host.
  • No outreach sending — subscribe to crm:activity.planned and call your own provider, then activities.complete(id).
  • No custom-field validation — metadata is Record<string, unknown>.
  • Cross-entity re-pointing on contact merge is the host's job (subscribe to crm:contact.merged).

Tests

npm run test:unit          # pure: scoring, forecast, state machines, event-catalog no-drift
npm test                   # unit + integration (mongodb-memory-server replica set)

Integration coverage includes the lead → opportunity → customer scenario, opportunity pipeline, subscription DOI, conversation/message threads, agentic verbs, multi-tenant isolation, and invariants.test.ts (tenant probe + lead/subscription dedup races + atomic owner reassignment).

License

MIT