Skip to content

New reading notes on system design, software architecture, and AI engineering. Explore reading

← Projects

Mar 31, 2026 · 6 min read · 0 views

The Smart Cart

Multi-vendor e-commerce marketplace

The Smart Cart is a next-generation multivendor e-commerce platform built on the MERN stack. It empowers sellers with independent product catalogs, pricing flexibility, and streamlined order management, while giving buyers a fast, secure, and engaging shopping experience. From intuitive store management to real-time vendor communication and smooth checkout, every feature is designed to blend scalability, simplicity, and innovation in one platform.

Timeline
2026-01 - 2026-03
Origin
Built from a tutorial, then extended
Role
Solo build. Followed a MERN roadmap tutorial for the core, then extended it.

Overview

The Smart Cart is a modern multivendor e-commerce platform built to handle the complexity of online retail while maintaining simplicity for users. Developed with the MERN stack, it empowers vendors to create independent stores with complete control over product catalogs, pricing, and order fulfillment. At the same time, buyers enjoy a seamless shopping journey with advanced filtering, wishlists, secure payments and responsive design across all devices.

Sellers get their own shop with control over catalog, pricing, and fulfillment. Buyers get one catalog, one cart, and one checkout across every seller, with three ways to pay and a direct line to whoever is shipping the order. Buyer, seller, and administrator each see three different surfaces of the same data.

Ten Mongoose models, thirty-plus REST endpoints, and a separate Socket.IO service for messaging.

Demo

A recorded walkthrough of The Smart Cart running, covering the main flows end to end.

Problem

Problems to solve

  • Seller storefront control

    Small sellers need a storefront they can control without building and maintaining a separate marketplace presence.

  • Fragmented buyer journey

    Shopping across several small sellers can mean separate catalogs, carts, checkouts, and accounts.

  • Limited buyer-seller contact

    Buyers need a direct line to the seller handling an order rather than relying on an indirect email thread.

  • Coordinating independent sellers

    Sellers need control over pricing, stock, and fulfillment while the marketplace still presents one coherent shopping experience.

The project brings seller control and a shared buyer journey into one marketplace without claiming production outcomes.

Goals

Goals for the build

  • Seller autonomy

    Independent shops with real control over products, orders, inventory, and earnings, through a dashboard that belongs to the seller rather than to the platform.

  • One buyer experience

    One catalog, one cart, one checkout across many sellers, with search, wishlists, and order tracking that do not fragment by vendor.

  • Direct communication

    Chat between buyer and seller, so a question about an order does not become an email thread nobody answers.

  • Payment choice

    Card, PayPal, and cash on delivery. Cash matters in this market and omitting it would exclude a large share of buyers.

  • Vendor visibility

    Vendors need visibility into their own orders and performance through an analytics surface tied to their store.

  • Fulfillment control

    Each seller needs control over products, inventory, pricing, and fulfillment within an independent shop.

The goals describe the marketplace relationships the build was intended to support, not measured business outcomes.

Tech stack

Frontend

ReactRedux Toolkit
CostA lot of boilerplate for slices that only ever hold three fields.

Routing

React Router
CostNo server rendering, so product pages are invisible to a crawler. The wrong trade for a storefront, and the first thing I would change.

Styling

Tailwind CSS
CostDense markup that reads poorly in review.

Backend

Node.jsExpress
CostExpress supplies nothing by default. Validation, error handling, and security headers are all hand-rolled, and the rate limiting never got written.

Database

MongoDBMongoose
CostNo transactions across documents, which matters most at checkout. See the first challenge below.

Real-time

Socket.IO
CostRequires a long-lived process, so it deploys as a separate service from the API.

Payments

StripePayPaland cash on delivery
CostThree payment paths is three times the surface area where money can be recorded wrongly.

Media

Cloudinary
CostA third-party dependency sitting in the product-creation path.

Architecture

BrowserReact clientRedux ToolkitDeployed servicesExpress APIRESTSocket.IO serviceWebSocketMongoDB10 modelsThird partyStripePayPalCloudinarySMTP relayRESTws
Three deployed services

User interfacesCustomer interfacediscovery, cart, checkoutSeller dashboardproducts, orders, messagingAdmin panelusers, sellers, oversightFrontend stateReact + Viterendered interfacesRedux Toolkituser, product, cart stateCommunicationHTTP / RESTCORS middlewarecredentials enabledJWT cookie authprotected requestsExpress APIExpress serverAPI gatewayUser and seller routesauthentication, profilesProduct and order routescatalog, checkout, trackingMessage routesconversations, messagesPayment and media routesStripe, CloudinaryCoupon, event, withdrawal routesData and servicesMongoDB + Mongoosedocument storageCloudinaryimagesStripepaymentsSMTP relay + Socket.IOemail and real-time messaging
Application flow from interface to integrations

CartPayment pathStripetokenizedPayPaltokenizedCash on deliveryunpaid stateOrder writtenNo transaction spans theseStockBalancesDeliveredsettles cashcash only
Checkout across three payment paths

Technologies I work with day to day, from the frontend through the backend to deployment.

Features

Buyer

6 features

Search and filtering across the whole catalog

Wishlists and a cart that survives a refresh

Checkout through Stripe, PayPal, or cash on delivery

Coupon codes applied at the cart level

Order tracking, refund requests, and purchase history

Reviews tied to a completed order for verified purchases

Seller

6 features

Dashboard for product creation, editing, and stock management

Product images uploaded and managed through Cloudinary

Order fulfillment with status transitions and seller-side refund approval

Promotional events with countdown timers

Seller-specific coupon codes

Earnings tracking and withdrawal requests through a configured payout method

Platform

6 features

Role-based access enforced in API middleware

Admin routes protected from unauthorized client-side access

Analytics dashboard covering orders, users, products, and active events

MongoDB aggregation pipelines returning chart-ready analytics data

Transactional emails through an SMTP relay

Email notifications for account activation and order events

API design

Users and sellers

Buyers and sellers are separate models with separate auth routes, not one user table with a role flag. That was the tutorial's shape and I kept it. It duplicates registration, activation, and profile handling almost exactly. The clearest structural thing I would undo.

  • Create UserPOST/api/user/register
  • ActivatePOST/api/user/activate

    email activation token

  • Login as UserPOST/api/user/login
  • Update AddressesPUT/api/user/update-addresses
  • DeleteDELETE/api/user/delete-address/:id
  • Create SellerPOST/api/seller/register
  • Update Withdraw MethodPUT/api/seller/update-withdraw-method
  • All SellersGET/api/seller/admin/all-sellers

    admin only

Orders and refunds

Refunds are two separate routes rather than one with a role check, because the buyer's action is a request and the seller's is a decision. Collapsing them would have meant a single handler branching on who called it, and that branch is where an authorisation mistake would eventually live.

  • CreatePOST/api/order/create
  • GetGET/api/order/user/:id
  • GetGET/api/order/seller/:id
  • PutPUT/api/order/seller/update-status/:id
  • PutPUT/api/order/user/order-refund/:id

    buyer requests

  • PutPUT/api/order/seller/order-refund/:id

    seller decides

Products and reviews

Reviews are a PUT onto the product rather than their own collection. Embedding keeps the read path to one query, which is the right call for a product page but it means a review cannot be moderated or paginated independently, and at any real volume that becomes the wrong call.

  • AddPOST/api/product/add
  • ListGET/api/product/all
  • GetGET/api/product/seller/:id
  • AddPUT/api/product/review/add

Data model

Ten collections. The interesting decisions are all about where to embed and where to reference.

AccountsUsercustomer accountShopseller accountAdmin roleplatform oversightCatalog and commerceProductseller-owned catalogEventpromotional inventoryCouponseller discountsOrdercart and payment snapshotSeller operationsWithdraw requestseller payout requestVisitoranalytics recordReviewembedded in ProductShipping addressembedded in OrderMessagingConversationbuyer and seller membersMessagetext and imagesSocket.IO servicereal-time deliveryMongoDBshared document storeownsadministers
Ten collections and their main relationships

Productembedded reviews, referenced seller

Reviews live inside the product document because they are always read with it and never on their own. One query renders a product page.

The cost is real: reviews cannot be paginated or moderated independently, and a product with thousands of them would carry all of them on every read.

Orderdenormalised cart snapshot

An order copies the cart contents at the moment of purchase rather than referencing live products. This is the one denormalisation I would defend without hesitation: a price change or a deleted product must not rewrite what someone already bought.

Sellerseparate root collection

Sellers are their own collection rather than users with a role. It duplicates most of the auth surface, and it is the structural decision I would reverse.

Conversation and Messagereferenced, split

Conversations hold the participants, messages reference the conversation. Splitting them keeps a long thread from growing one document past the point where it is cheap to read.

CouponCode, Event, Withdraw, Visitorreferenced

All small, all scoped to a seller, all queried independently. Nothing interesting here, which is the point. Most collections should be boring.

Key flows

Customer registersReact clientCreate activation tokenuser controllerSend email + upload avatarSMTP + CloudinaryActivate accountUser.create in MongoDBLoginpassword comparisonBrowse and filter productsRedux Toolkit stateAdd to cart or wishliststock validationMessage sellerSocket.IO
Customer registration and shopping

Register shopseller clientCheck email and upload avatarMongoDB + CloudinarySend activation emailSMTP relayActivate shopShop.create in MongoDBLogin to shoppassword comparison + cookieManage products and eventsseller routesProcess orders and update shoporders, balance, profileHandle buyer messagesSocket.IO
Seller onboarding and fulfillment

Admin loginrole checkedView all usersUser.find sorted by dateView all sellersShop.find sorted by dateDelete user when requiredprotected DELETE routeDelete seller when requiredprotected DELETE routePersist and refresh panelMongoDB
Administrator oversight

Challenges

Checkout writes to several documents with no transaction

Problem

What:

Placing an order writes an order, decrements stock on every product in the cart, and updates the seller's balance. MongoDB gave me no transaction across those writes.

Why:

If the process dies between the order write and the stock decrement, the buyer has an order for something the catalog still thinks is in stock. Money and inventory disagree, and nothing in the system notices.

How it was solved

I ordered the writes so the least damaging failure is the likeliest one: the order is written first, then stock, then balances. A crash after the order leaves stock briefly overstated, which a seller can correct; the reverse would silently take payment for nothing.

This is mitigation, not a fix. The correct answer is a transaction, and the honest version of this section is that I chose an ordering rather than solving the problem.

Three payment paths, three ways to mishandle money

Problem

What:

Supporting Stripe, PayPal, and cash on delivery without widening the surface where a payment can be recorded incorrectly.

Why:

Cash on delivery is a meaningful share of e-commerce in this market, so omitting it was not an option. But it behaves nothing like the other two: there is no confirmation event, and the money arrives days after the order.

How it was solved

Card and wallet payments are tokenized through their SDKs, so no card data reaches the application. Cash orders are created in an unpaid state and only settle when the seller marks delivery.

Treating cash as a first-class state rather than a special case is what kept it from leaking conditionals through the order code.

Analytics that recompute on every dashboard load

Problem

What:

Sellers needed sales, order, and revenue figures. With no budget for a scheduled job or a warehouse, every figure is aggregated live.

Why:

A seller without numbers cannot tell a good week from a bad one, and has no basis for pricing or restocking.

How it was solved

MongoDB aggregation pipelines return chart-ready shapes directly, so the client does no reshaping. Grouping and projection happen in the database, and only the summary crosses the wire.

The honest caveat: this is fine at the data volume it has ever held, and I have not tested it beyond that. At real scale these become a precomputed job, not a live query.

Practices

  • Authorization in middleware, not in the interface. role checks sit on the route. Hiding a button is not access control.
  • Payment data never touches the server. tokenized through the provider SDKs, so card details are out of scope entirely.
  • Query shaping in the database. indexes, pagination, and projections, so a list endpoint returns a page rather than a collection.
  • Global error middleware. one place that turns a thrown error into a response, instead of a try/catch in every controller.
  • Secrets in environment variables. nothing committed, and the example file lists names without values.
  • Real-time separate from persistence. Socket.IO handles live delivery; MongoDB stores the source of truth.

Two things are missing that ought to be here: there is no rate limiting on the auth routes, and no automated tests. Both are absent because I ran out of time, which is a reason and not a defence.

Metrics

15k+
Lines of code

counted

60+
React components

counted

30+
API endpoints

counted

10
Mongoose models

counted

Lessons

01

The most useful thing this project taught me was where a tutorial stops being enough. The core flows were guided; the multi-vendor payout logic, the aggregation queries behind the analytics, and the deployment of two separate services were not, and that is where I actually learned.

02

Data modelling decisions are expensive to reverse. The order schema went through three revisions because I modelled it around the checkout flow rather than around what sellers needed to query later. The aggregation pipelines behind the analytics dashboard would have been considerably simpler if I had started from the reporting requirements.

03

Separating buyers and sellers into two collections cost more than it saved. Registration, activation, password handling, and profile updates are all written twice, and every future feature that touches both has to be written twice too. One collection with a role field would have been less code and fewer places to get authorisation wrong.

Related

Open to full-time remote roles and freelance work.