Asia/Karachi
BlogOctober 4, 2026

How to Build a Multi-Vendor E-Commerce Platform

The roles, data model and order flow to plan before you write code
Fawaz Israr
How to Build a Multi-Vendor E-Commerce Platform
To build a multi-vendor ecommerce platform, start by separating three kinds of users (customers, vendors and admins), then give every product a vendor and split every customer order into one sub-order per vendor. Payments, stock and payouts all hang off that split. If the data model gets it right early, the rest of the system is mostly ordinary CRUD with good permissions. I'm a final-year Computer Science student at KFUEIT, and this is how I approach it, based on designing GoCart, my AI-powered multi-vendor e-commerce final year project. The details below are general, so adapt them to your own stack. A single-store shop has an owner and customers. A marketplace has three groups, and each one needs its own screens and its own rules.
  • Customers browse, build a cart, pay and track orders. They never see another customer's data.
  • Vendors manage their own products, stock, incoming orders and earnings. They must never see another vendor's data.
  • Admins approve vendors, set commission, handle disputes and see everything.
Write these down as user stories before touching the database. GoCart has these same three groups, and I documented the project in a 40+ page Software Design Document. If you need a template for that kind of document, see Software Design Document for a Final Year Project. The simplest mental model: a vendor is a business, a user is a login. One user can own a vendor, and a vendor can later have several staff users. If you merge the two into one table you will regret it the first time a shop wants a second person to manage orders. Products belong to a vendor, not to the platform. Every product row carries a vendorId, and every query that a vendor triggers filters on it. Categories, on the other hand, usually belong to the platform so customers get one consistent catalogue to browse. Here is a trimmed sketch of the core relations in Prisma (a full walkthrough with indexes and delete rules is in Prisma Schema Design for an E-Commerce Database):
Prisma
model Vendor {
  id         String    @id @default(cuid())
  name       String
  ownerId    String    @unique
  owner      User      @relation(fields: [ownerId], references: [id])
  commission Int       @default(1000) // basis points: 1000 = 10%
  products   Product[]
  subOrders  SubOrder[]
}

model Product {
  id       String @id @default(cuid())
  vendorId String
  vendor   Vendor @relation(fields: [vendorId], references: [id])
  title    String

  @@index([vendorId])
}
Storing commission in basis points as an integer avoids floating point surprises when you later calculate each vendor's share. A customer checks out once, but their cart may contain items from four different shops. Each shop packs, ships and gets paid separately, and each can fail separately: one vendor runs out of stock while the other three are fine. So model it in two levels. The Order is what the customer sees: one address, one payment, one total. Each SubOrder belongs to one vendor and holds that vendor's items, its own shipping status and its own subtotal.
A shop owner at a counter serving a customer, the kind of vendor a multi-vendor marketplace is built for
The customer-facing status is then derived from the sub-orders. If two of three have shipped, the order is "partially shipped". Do not store a second copy of that status that can drift out of sync, compute it when you read. A reasonable lifecycle for a sub-order is PENDING, CONFIRMED, SHIPPED, DELIVERED, with CANCELLED and REFUNDED as exits. Put it in an enum so invalid states cannot be written. The customer pays the platform once. The platform then owes each vendor their subtotal minus commission, usually after the order is delivered and any return window has passed. Keep a ledger instead of a running balance. Every sale, refund and payout is a row with an amount and a reference to the sub-order. A vendor's balance is the sum of their rows, which means you can always explain a number to a vendor who disputes it. If you take payments with Stripe, Stripe Connect is the product designed for splitting money between a platform and sellers. For the customer-side checkout flow, Stripe Checkout in a MERN Stack App covers the basics. A student project can also keep payouts as recorded ledger entries that an admin marks as paid, which is enough to prove the model works. Authentication answers who you are. Authorization answers what you may touch, and in a marketplace the second one is where the bugs live. Give each user a role (CUSTOMER, VENDOR, ADMIN) and check it on the server for every route, not just in the UI. Hiding a button is cosmetic. Then add an ownership check on top: a vendor with the right role can still only edit their own products.
Ts
// inside a route handler, after you have the session
const product = await prisma.product.findFirst({
  where: { id: productId, vendorId: session.vendorId },
});

if (!product) {
  return new Response("Not found", { status: 404 });
}
Returning 404 instead of 403 for someone else's product avoids confirming that it exists. The full pattern for Next.js is in Role-Based Access Control in Next.js with Prisma. OWASP's page on broken access control is worth reading once, because forgotten ownership checks are the classic marketplace vulnerability. Stock belongs to the thing that is actually sold, which is usually a variant (size, colour) rather than the product. Each variant has a quantity owned by its vendor. Decrement stock when the order is created, inside the same database transaction that creates the order, and use a conditional update so two simultaneous buyers cannot both take the last item. Do not read the stock, check it in application code and then write it back. That read-then-write gap is where overselling happens. If a payment fails or an order is cancelled, put the quantity back. Decide early whether stock is reserved at checkout start or only at payment success. Reserving at checkout start is friendlier for scarce items but needs an expiry job to release abandoned carts. Anything slow, retryable or not needed for the HTTP response should leave the request. Typical examples: order confirmation emails, vendor notifications, releasing expired stock reservations, generating payout summaries and syncing search indexes. An event-driven approach works well here. The checkout code emits an event such as order/created, and separate functions react to it. Inngest is one option for this, and it is what I used in GoCart to automate background processes. A queue like BullMQ or a cron job can do the same job, so pick whichever fits your hosting. The benefit is isolation. If the email provider is down, the order still exists and the email retries later. Marketplaces get big quickly, so order the work by risk. A checklist that works for a first version:
  1. Users, roles and sessions, with the server-side checks in place.
  2. Vendor onboarding and an admin approval step.
  3. Products and variants owned by vendors, with a vendor dashboard.
  4. Customer catalogue, cart and checkout creating one Order and its SubOrders in a transaction.
  5. Stock decrement, order statuses and vendor order screens.
  6. Background jobs for emails and notifications.
  7. Commission ledger and payouts.
  8. Extras such as reviews, coupons and recommendations.
You can see how these pieces come together in the GoCart case study, where the AI features are still in development. Get steps 1 to 5 working end to end before adding anything clever like that. Cover photo from Unsplash.
Share this post: