Case Study
FarmFlow
Architected a full-stack B2B marketplace to connect farm suppliers and buyers, featuring role-based dashboards, product catalog management, order lifecycle tracking, an AI demand-forecasting engine, and Razorpay payments with Socket.IO real-time updates.
AI-Powered B2B Marketplace
FarmFlow doesn't just connect suppliers and buyers — it actively forecasts demand using 30-day sales history, identifies low-stock risks, recommends reorder quantities, and provides personalized product recommendations to buyers based on their order history.
Core Feature Overview
🥕
Supplier Analytics
Real-time revenue dashboards built with MongoDB aggregation pipelines — 6-month trend charts, VIP/Regular/New buyer segmentation, and order volume metrics.
Complete Project Structure
root
farm-flow-prod/
backend/
src/
config/
db.js— Mongoose Atlas connection
passport.js— Google OAuth 2.0 strategy
redis.js— Redis client for caching
socket.js— Socket.IO initialization export
controllers/
analytics.controller.js— Revenue, segmentation, forecasting
auth.controller.js— JWT, Google OAuth, refresh tokens
cart.controller.js— Add, remove, update cart items
notification.controller.js— Mark-read, get all notifications
order.controller.js— Order lifecycle (place → deliver)
payment.controller.js— Razorpay create + verify endpoints
product.controller.js— CRUD + image upload via Cloudinary
user.controller.js— Profile, RBAC, account management
middleware/
auth.middleware.js— Bearer JWT verify + attach user
rbac.middleware.js— Role-based route guard (supplier/buyer/admin)
rateLimiter.middleware.js— express-rate-limit per endpoint
upload.middleware.js— Multer + Cloudinary storage engine
validate.middleware.js— Joi validation pipe
error.middleware.js— Centralized error handler
models/
User.model.js— Roles: supplier, buyer, admin with business info
Product.model.js— Catalog with stock, pricing, category
Order.model.js— Order with items, pricing, status machine
Payment.model.js— Razorpay transaction records
Cart.model.js— Per-user cart with item array
Category.model.js— Product categories
Notification.model.js— User notification feed
Discount.model.js— Discount rules
Wishlist.model.js— Buyer product wishlists
services/
ai.service.js— Demand forecasting + smart recommendations
analytics.service.js— Aggregation-based revenue analytics
auth.service.js— JWT pair generation and rotation
payment.service.js— Razorpay order + signature verification
email.service.js— Nodemailer transports
pdf.service.js— Invoice PDF generation
socket/
index.js— JWT-auth middleware + room join logic
handlers/
notification.handler.js— Real-time push to user rooms
utils/— ApiError, ApiResponse, logger, helpers
server.js
frontend/
src/
features/— Redux Toolkit slices (auth, cart, orders)
store/— Redux store config with RTK Query
docker-compose.yml— Local dev orchestrator
Order Lifecycle Flow
1
Buyer Browses Catalog
Buyers browse the marketplace product catalog filtered by category, price range, and supplier. Products have stock levels with `lowStockThreshold` for supplier alerting.
2
Cart & Checkout
Items are added to a per-user `Cart` model. On checkout, an `Order` document is created with status `pending`, containing full pricing breakdown (subtotal, taxes, total in paise for Razorpay).
3
Razorpay Order Creation
The payment controller calls `paymentService.createRazorpayOrder(order)`. A fresh `Razorpay` instance is initialized per request (to handle Render environment variable injection issues). The returned `razorpayOrderId` is stored on the order.
4
HMAC Signature Verification
After the buyer completes checkout, the frontend sends `razorpayOrderId`, `razorpayPaymentId`, and `razorpaySignature`. The backend verifies by computing `HMAC-SHA256(razorpayOrderId|razorpayPaymentId)` and comparing the digest.
5
Payment Record & Order Update
On success, a `Payment` document is created with gateway metadata (method, bank, VPA, card network). The order status advances to `confirmed` and inventory stock counts decrement.
6
Real-Time Notification Push
A Socket.IO event fires to the supplier's personal room (`supplier:{id}`) notifying them of a new order. The buyer's room receives an order-confirmed event simultaneously.
Socket.IO Authentication & Room System
javascript
// backend/src/socket/index.js
import { Server } from 'socket.io';
import jwt from 'jsonwebtoken';
import User from '../models/User.model.js';
export const initializeSocket = (server) => {
const io = new Server(server, {
cors: { origin: process.env.CLIENT_URL, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], credentials: true },
});
// JWT Authentication Middleware on Socket handshake
io.use(async (socket, next) => {
const token = socket.handshake.auth?.token ||
socket.handshake.headers?.authorization?.split(' ')[1] ||
socket.handshake.query?.token;
if (!token) return next(new Error('Authentication token required'));
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const user = await User.findById(decoded.sub).select('-password');
if (!user || !user.isActive) return next(new Error('User not found or deactivated'));
socket.user = user;
next();
});
io.on('connection', (socket) => {
socket.on('join:room', (room) => {
// Security: only allow joining own personal rooms or the global marketplace
const isAllowed =
room === 'marketplace' ||
room === `supplier:${socket.user._id}` ||
room === `buyer:${socket.user._id}`;
if (isAllowed) socket.join(room);
});
socket.on('disconnect', () => {
console.log(`Socket disconnected: ${socket.id}`);
});
});
return io;
};Razorpay Payment Service
javascript
// backend/src/services/payment.service.js
import Razorpay from 'razorpay';
import crypto from 'crypto';
// Strip invisible chars from Render environment variables
const normalizeEnvValue = (value) =>
typeof value === 'string' ? value.replace(/['"\r\n]/g, '').trim() : '';
class PaymentService {
async createRazorpayOrder(order) {
const keyId = normalizeEnvValue(process.env.RAZORPAY_KEY_ID);
const keySecret = normalizeEnvValue(process.env.RAZORPAY_KEY_SECRET);
// Fresh instance per call — prevents Render cold-start env issues
const razorpayInstance = new Razorpay({ key_id: keyId, key_secret: keySecret });
const razorpayOrder = await razorpayInstance.orders.create({
amount: Math.round(order.pricing.total * 100), // in paise
currency: 'INR',
receipt: order.orderNumber,
notes: { orderId: order._id.toString(), orderNumber: order.orderNumber },
});
order.payment.razorpayOrderId = razorpayOrder.id;
await order.save();
return { razorpayOrderId: razorpayOrder.id, amount: razorpayOrder.amount, keyId };
}
verifyPaymentSignature(razorpayOrderId, razorpayPaymentId, signature) {
const body = `${razorpayOrderId}|${razorpayPaymentId}`;
const keySecret = normalizeEnvValue(process.env.RAZORPAY_KEY_SECRET);
const expectedSignature = crypto
.createHmac('sha256', Buffer.from(keySecret, 'utf8'))
.update(Buffer.from(body, 'utf8'))
.digest('hex');
return expectedSignature === signature;
}
}AI Demand Forecasting Engine
The AI service uses real sales history to compute reorder recommendations. No external ML library — a pure Node.js heuristic approach:
javascript
// backend/src/services/ai.service.js
class AIService {
async predictDemand(supplierId) {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// Aggregate delivered orders for the last 30 days per product
const salesHistory = await Order.aggregate([
{ $match: { supplier: new mongoose.Types.ObjectId(supplierId), status: 'delivered', createdAt: { $gte: thirtyDaysAgo } } },
{ $unwind: '$items' },
{ $group: {
_id: '$items.product',
totalQuantity: { $sum: '$items.quantity' },
orderCount: { $sum: 1 },
}},
]);
return Promise.all(salesHistory.map(async (item) => {
const product = await Product.findById(item._id).select('name unit stock');
const weeklyAverage = item.totalQuantity / 4;
const predictedNextWeek = weeklyAverage * 1.15; // 15% safety buffer
const daysOfStockLeft = weeklyAverage > 0
? (product.stock.quantity / weeklyAverage) * 7
: 999;
return {
productId: product._id,
name: product.name,
currentStock: product.stock.quantity,
averageWeeklySales: Math.round(weeklyAverage * 10) / 10,
predictedNextWeekDemand: Math.round(predictedNextWeek * 10) / 10,
daysOfStockRemaining: Math.round(daysOfStockLeft),
recommendation: daysOfStockLeft < 7 ? 'REORDER_NOW' : daysOfStockLeft < 14 ? 'MONITOR' : 'OK',
};
}));
}
async getSmartRecommendations(buyerId) {
// Find buyer's past orders to extract purchased categories
const pastOrders = await Order.find({ buyer: buyerId }).sort('-createdAt').limit(5).lean();
const orderedProductIds = new Set(pastOrders.flatMap(o => o.items.map(i => i.product.toString())));
const products = await Product.find({ _id: { $in: [...orderedProductIds] } });
const orderedCategoryIds = new Set(products.map(p => p.category.toString()));
// Recommend from same categories, excluding already purchased
return Product.find({
isActive: true,
'stock.quantity': { $gt: 0 },
category: { $in: [...orderedCategoryIds] },
_id: { $nin: [...orderedProductIds] },
}).sort('-salesCount').limit(6).populate('category', 'name');
}
}Customer Segmentation Analytics
javascript
async analyzeCustomerSpending(supplierId) {
const orders = await Order.aggregate([
{ $match: { supplier: new mongoose.Types.ObjectId(supplierId), status: 'delivered' } },
{ $group: {
_id: '$buyer',
totalSpent: { $sum: '$pricing.total' },
orderCount: { $sum: 1 },
averageOrderValue: { $avg: '$pricing.total' },
}},
{ $sort: { totalSpent: -1 } },
]);
return Promise.all(orders.map(async (ord) => {
const buyer = await User.findById(ord._id).select('firstName lastName email businessInfo');
return {
name: `${buyer.firstName} ${buyer.lastName}`,
totalSpent: ord.totalSpent,
orderCount: ord.orderCount,
// Segment buyers: VIP (>50k), Regular (>15k), New/Low Value
segment: ord.totalSpent > 50000 ? 'VIP' : ord.totalSpent > 15000 ? 'Regular' : 'New/Low value',
};
}));
}Security Layers
All API routes are protected with Helmet for HTTP headers, MongoDB Sanitize against NoSQL injection, HPP protection against parameter pollution, and express-rate-limit on authentication and payment endpoints.