A marketplace is a normal store plus two hard problems: one customer order becomes several vendor orders, and money has to reach people who are not you. Everything else — vendor onboarding, dashboards, commission rules — is ordinary application work.
Get the two hard problems right and the rest follows.
The vendor module
import { model } from "@medusajs/framework/utils"
export const Vendor = model.define("vendor", {
id: model.id().primaryKey(),
name: model.text(),
handle: model.text().unique(),
status: model.enum(["pending", "active", "suspended"]).default("pending"),
commission_rate: model.number().default(15), // percent
payout_account_id: model.text().nullable(), // e.g. Stripe Connect account
payout_schedule: model.enum(["daily", "weekly", "monthly"]).default("weekly"),
admins: model.hasMany(() => VendorAdmin),
})
export const VendorAdmin = model.define("vendor_admin", {
id: model.id().primaryKey(),
email: model.text().unique(),
vendor: model.belongsTo(() => Vendor, { mappedBy: "admins" }),
})Link vendors to products so every product has an owner, and to orders so every vendor order has a payee. Vendor admins are separate from customers and from store staff — three distinct actor types with three different permission sets.
Splitting the order
The core mechanic. When a cart completes, group its items by vendor and create a child order per vendor, all inside the completion workflow:
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
type Input = { order_id: string }
export const splitOrderByVendorStep = createStep(
"split-order-by-vendor",
async ({ order_id }: Input, { container }) => {
const query = container.resolve("query")
const marketplace = container.resolve("marketplace")
const { data: [order] } = await query.graph({
entity: "order",
fields: ["id", "items.*", "items.variant.product.vendor.*", "currency_code"],
filters: { id: order_id },
})
// One bucket per vendor represented in the cart.
const byVendor = new Map<string, typeof order.items>()
for (const item of order.items) {
const vendorId = item.variant?.product?.vendor?.id
if (!vendorId) continue
byVendor.set(vendorId, [...(byVendor.get(vendorId) ?? []), item])
}
const vendorOrders = await marketplace.createVendorOrders(
[...byVendor.entries()].map(([vendorId, items]) => ({
vendor_id: vendorId,
parent_order_id: order_id,
items: items.map((i) => ({ line_item_id: i.id, quantity: i.quantity })),
subtotal: items.reduce((sum, i) => sum + Number(i.total), 0),
})),
)
return new StepResponse(vendorOrders, vendorOrders.map((o) => o.id))
},
async (ids: string[] | undefined, { container }) => {
if (!ids?.length) return
await container.resolve("marketplace").deleteVendorOrders(ids)
}
)The customer keeps one order and one payment. Vendors each get a fulfilment-scoped order. Compensation removes the vendor orders if a later step fails, so a rejected checkout does not leave three vendors with phantom work.
Money
The part where mistakes are regulatory rather than merely embarrassing.
Do not collect money and pay vendors from your own account unless you have taken advice about money-transmission licensing in every jurisdiction you operate in. Use a platform payment product built for it — Stripe Connect, Adyen for Platforms, or a regional equivalent — where the processor handles the split and the compliance.
With Stripe Connect the shape is:
- Vendors onboard through Connect and get an account id.
- At payment, the charge specifies transfers to each vendor account minus commission.
- Stripe handles payouts on the vendor's schedule.
- Refunds reverse the corresponding transfer.
Commission is computed at split time and stored on the vendor order, so a later rate change does not rewrite history.
Vendor access
Vendors need a scoped API and dashboard. Two rules, and they are non-negotiable:
Scope every query by vendor id, in middleware, not in each handler. One forgotten filter leaks another vendor's orders, and that is a breach rather than a bug.
import { defineMiddlewares, authenticate } from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/vendor/*",
middlewares: [authenticate("vendor", ["session", "bearer"])],
},
],
})Give vendors only what they need: their orders, their products, their payouts, their fulfilment actions. Not customer contact details beyond what shipping requires, and never store-wide analytics.
Returns and refunds
The part that is always underestimated.
- Who authorises a return? The vendor, the platform, or either — decide and encode it.
- Who pays return shipping? Depends on reason code. Encode that too.
- How is the refund split? The vendor's share reverses; whether your commission also reverses is a policy decision with real revenue impact.
- What if the vendor has already been paid out? Then you are clawing back from a future payout, and that needs a negative balance concept from day one.
Design the return flow at the same time as the order flow. Retrofitting clawbacks into a payout system that assumed money only moves outward is genuinely painful.
Effort
| Capability | Typical effort |
|---|---|
| Vendor module and onboarding | 2 weeks |
| Order splitting | 1–2 weeks |
| Connect integration and payouts | 2–3 weeks |
| Vendor dashboard | 3–4 weeks |
| Returns and clawbacks | 2 weeks |
| Commission rules and reporting | 1–2 weeks |
Eleven to fifteen weeks for a functioning marketplace. The vendor dashboard is consistently the largest single piece and consistently underestimated.
Vendor onboarding
The unglamorous work that determines whether vendors ever list anything.
Stage the requirements. Do not ask for tax details and bank information before a vendor has seen the dashboard. Let them sign up, explore, and add products in draft; require payout details only before their first listing goes live.
Automate identity and payout onboarding. Stripe Connect's hosted onboarding handles KYC and bank details and keeps that data off your systems entirely, which is both faster and a smaller compliance surface.
Make the first product easy. A guided flow for one product beats a bulk CSV import as a first experience. Offer the import to vendors who have already listed something.
Review before publishing. A status field on the vendor and on products, with an approval step, prevents your storefront from becoming a surprise. Automate what you can — image quality, required fields, prohibited categories — and review the rest.
Commission models
| Model | Works for | Watch |
|---|---|---|
| Flat percentage | Most marketplaces | Simple, may not fit all categories |
| Per-category rate | Mixed margin categories | More rules to maintain |
| Tiered by volume | Rewarding large vendors | Recalculating retroactively |
| Fixed fee plus percentage | Low-value items | Feels punitive on cheap goods |
| Subscription plus lower rate | Committed vendors | Two billing systems |
Store the rate that applied on the vendor order, not just on the vendor. When you change commission next year, historical orders must keep their original rate — otherwise every past payout report changes retroactively, which is a reconciliation nightmare and, depending on your vendor agreement, a contractual problem.
Marketplaces are among the most demanding things we build. Ask for a scope before committing to a date.
Frequently asked questions
Can Medusa be used to build a marketplace?
Yes, and its module system suits it well — vendors, vendor orders and payouts become custom modules linked to core commerce entities, with order splitting handled in the completion workflow. Expect roughly three months for a functioning marketplace.
How do I split an order between vendors in Medusa?
Group line items by the vendor that owns each product and create a vendor order per group inside the completion workflow. The customer keeps a single order and payment; vendors receive fulfilment-scoped orders.
How should marketplace payouts be handled?
Through a platform payments product such as Stripe Connect, which splits the charge and handles payouts and compliance. Collecting funds and paying vendors from your own account can constitute money transmission and needs legal advice first.
How do vendors access their own data?
Through a scoped API and dashboard, with vendor filtering applied in middleware rather than in individual handlers. A single missing filter exposes another vendor's data, so scope it once at the boundary.
How do refunds work in a marketplace?
The refund reverses the customer payment and the corresponding vendor transfer. If the vendor has already been paid out you need a clawback against future payouts, which means designing negative balances into the payout model from the start.
How long does it take to build a marketplace on Medusa?
Eleven to fifteen weeks for vendor management, order splitting, payouts, a vendor dashboard and returns. The vendor dashboard is usually the largest piece and the one most often underestimated.
Building Subscriptions on Medusa: Billing Cycles, Dunning and Churn
Recurring billing is a scheduling problem with money attached. The data model, the renewal workflow, and the failure handling that determines involuntary churn.
B2B Commerce on Medusa: Companies, Approvals, Quotes and Net Terms
The four capabilities that separate B2B from DTC, modelled as Medusa modules and workflows — with the data model that makes approvals work.
Testing Medusa: What to Test, What to Skip, and How
Commerce code moves money, so untested checkout logic is a liability. A pragmatic test strategy — what earns its keep, and what is theatre.



