Skip to main content

Blueprint for a Mini-CRM on the MERN stack

A small Mini-CRM on the MERN stack (MongoDB, Express, React, Node.js): one place for leads, a Kanban sales pipeline, and basic reports — instead of the spreadsheets and scattered notes of self-employed sellers, small businesses, and startups.

Overview​

GoalUsers
Centralized lead managementSelf-employed salespeople
Track pipeline progressSmall and micro businesses
Higher productivitySmall sales teams
Basic reportsEarly-stage startups
Easy to use, no training required

Value: replace the spreadsheet with one system, see the whole sales process so opportunities are not missed, keep the history of each customer conversation, and decide from the numbers — at a low cost.

Sales pipeline​

Each lead moves through stages, shown as Kanban columns; drag a card to another column to change its stage.

A lead card shows the name, company, and expected value; open the card for the detail and the note history.

Data model​

CollectionMain fieldsNotes
usersemail, passwordHash, rolePassword hashed with bcrypt or Argon2
leadsname, company, value, stage, ownerIdstage is a fixed enum; value is stored as an integer (VND), not a float
activitiesleadId, type (note, call, email), content, createdAtHistory of interaction with the customer
stage_changesleadId, from, to, changedAtStored on every stage change — input for conversion rate and time to close

Index { ownerId: 1, stage: 1 } for the Kanban query.

API​

MethodEndpointDescription
POST/api/auth/loginSign in, return a JWT
GET/api/leads?stage=&q=The user's leads, with filter and search
POST/api/leadsCreate a lead
PATCH/api/leads/:idUpdate a lead, change its stage (also write stage_changes)
DELETE/api/leads/:idDelete a lead
POST/api/leads/:id/activitiesAdd a note
GET/api/reports/pipelineCount and total value by stage
Check ownership

Every query by :id must also filter on ownerId (Lead.findOne({ _id: id, ownerId: req.user.id })). Filtering only on _id lets a user change the id in the URL and read someone else's lead (an IDOR bug).

Flows​

Authentication​

Store the JWT in an HttpOnly, Secure, SameSite cookie instead of localStorage, so a malicious script cannot read it.

Create a lead​

  1. The user enters the lead on a form.
  2. The frontend sends POST /api/leads (the cookie is attached automatically).
  3. The backend verifies the token, validates the data, sets ownerId = req.user.id, and saves it to MongoDB.
  4. The frontend adds the new lead to the "New" column on the Kanban board.

Reporting dashboard​

The numbers for the pipeline chart come from one aggregation:

reports.controller.js
const pipeline = await Lead.aggregate([
{ $match: { ownerId: req.user._id } }, // or the whole team, if this user is a manager
{ $group: { _id: "$stage", count: { $sum: 1 }, totalValue: { $sum: "$value" } } },
]);

From stage_changes you can also compute: win rate, conversion rate between stages, and average time to close a deal.

Further development​

FeatureDescription
Email and calendarSend email and schedule meetings inside the CRM
Task managementCreate and assign tasks tied to each lead
Finer-grained permissionsAdmin, Manager, and Sales roles; a manager sees the whole team's leads
Richer reportsAdd filters for time range, salesperson, and chart type
AutomationAutomatically send an email or create a task when a lead changes stage
Docker and CI/CDPackage the app and deploy it automatically — see Ubuntu server setup