Guides · August 1, 2023 · 9 min read
Building a Multi-Vendor Marketplace on Medusa v2
Medusa v2's module architecture makes vendor accounts, split orders, and seller-scoped fulfillment tractable without forking core commerce logic. Here is a working blueprint for structuring a marketplace on Medusa v2.
By Polo Themes
A multi-vendor marketplace on Medusa v2 is built from three moving pieces: a custom Seller module that owns vendor identity and onboarding, a cart-splitting workflow that turns one customer checkout into multiple seller-scoped orders, and a fulfillment and payout layer that tracks which seller owes which shipment and which commission. None of this ships in Medusa core out of the box, but v2's modular architecture — modules, workflows, and the query/link system — gives you the primitives to build it cleanly instead of forking the monolith. This guide walks through the architecture end to end, with the workflow steps and data-model decisions that actually matter.
If you have only built single-vendor Medusa stores, marketplace work will initially feel like it should be "just add a vendor_id column." It is not. The moment you have more than one seller, nearly every core concept — order, fulfillment, payment capture, product ownership, even customer support — needs a seller dimension threaded through it. The good news is that Medusa v2 was designed with exactly this kind of extension in mind: modules are isolated, linkable, and composable, and the workflow engine gives you transactional, resumable multi-step processes instead of a pile of ad hoc service calls. This tutorial assumes familiarity with basic Medusa v2 module and workflow authoring; if you need a primer on those first, treat this as the next step after "hello world module."
Why Medusa v2's Architecture Fits Marketplaces
Medusa v1 was a single Express monolith with a shared Postgres schema and tightly coupled services — extending core order or fulfillment logic meant either forking the package or writing fragile subscriber hacks. Medusa v2 rebuilt the internals around modules: independently versioned, independently migrated units of domain logic, each owning its own database tables, with a module link system that lets you associate rows across modules without either module knowing the other exists. A marketplace's Seller module can link to Medusa's core Product module, Order module, and Fulfillment module without editing a single line of Medusa's own source. That is the difference between "customizing Medusa" and "patching Medusa," and it is the reason v2 is a credible foundation for marketplace commerce in a way v1 never was.
The second piece is the workflow engine. Medusa v2 workflows are built from discrete, individually testable steps with automatic compensation (rollback) logic — if step four of an eight-step "split cart into per-seller orders" workflow fails, the engine unwinds steps one through three using their declared compensating actions. For a marketplace checkout, where you might be creating three orders, three payment captures, and three fulfillment records in a single customer transaction, that rollback guarantee is not a nice-to-have, it is the difference between a consistent ledger and a support queue full of half-created orders.
Step 1: Model the Seller Module
Start with a dedicated Seller module rather than bolting seller fields onto Customer or Product. A Seller module typically needs its own data model for the seller entity itself (business name, payout details, status: pending/active/suspended, commission rate), a join concept linking sellers to the users who administer them, and a way to represent a seller's storefront presentation (slug, logo, bio) if you are building seller storefronts. Keep this module ignorant of orders, products, and fulfillment — those relationships are expressed later through module links, not foreign keys inside the Seller module's own schema. This separation is what lets you later swap out or version the Seller module independently.
Commission rate deserves special care. Store it as an integer in basis points (e.g. 1500 for 15%), never as a float percentage — floating point commission math compounds rounding errors across thousands of orders in ways that are miserable to reconcile later. Decide up front whether commission is seller-level (one rate per seller), category-level, or product-level, because retrofitting a more granular commission model after payouts have already run is a real migration headache, not a config change.
Step 2: Link Sellers to Products
With the Seller module in place, use Medusa's module link API to associate each product with exactly one seller (the common "single-seller-per-listing" marketplace model — the alternative, multiple sellers competing on one listing like a Buy Box, is a materially harder problem and worth avoiding for a v1). A module link is a first-class Medusa concept: it creates a join table Medusa manages for you, and it lets you query "give me this product's seller" or "give me all products for this seller" through the standard query.graph API alongside core product data, without touching the Product module's own tables.
Once that link exists, admin-side product creation needs a seller-scoping check: a seller's admin user should only be able to create, edit, or view products linked to their own seller record. This is the first place marketplace work touches Medusa's auth layer — you will typically extend the actor/auth-identity model so a logged-in admin user carries a seller_id claim, then enforce it in a custom middleware on the relevant admin API routes rather than trusting client-supplied seller IDs.
Step 3: Split the Cart at Checkout
This is the workflow that defines whether your marketplace actually behaves like one. A customer adds items from three different sellers to a single cart and checks out once, with one payment. Medusa needs to end up with three separate orders — one per seller — each with its own fulfillment status, its own line items, and its own eventual payout record, while the customer experiences a single checkout and a single charge.
- Group cart items by seller. Before payment, walk the cart's line items, resolve each product's linked seller via query.graph, and group into per-seller item sets. Reject or flag carts where a seller cannot fulfill (e.g. inactive seller status).
- Capture one payment, allocate internally. Run a single payment authorization and capture against the customer's chosen payment method for the cart total. Do not create N separate payment intents — that multiplies card-decline risk and hurts conversion. Allocation across sellers happens in your own ledger, not at the payment-provider level.
- Create N orders from one workflow run. Use a custom workflow (composed from Medusa's createOrderWorkflow building blocks, invoked once per seller group) so each seller gets a real Medusa order object with correct totals, tax, and shipping apportioned to their share of the cart.
- Write a payout ledger entry per seller order. For each created order, write a pending payout record: gross amount, commission deducted, net owed, referencing the order id. This ledger is what your payout job reads later — never compute payouts by re-querying orders at payout time.
- Emit seller-scoped events. Fire a subscriber event per created order so each seller's notification (new order email, webhook to their dashboard) fires independently, rather than one combined notification that leaks other sellers' data.
Shipping is the detail that trips up most first attempts. If a customer orders from three sellers who ship from three different warehouses, you either need per-seller shipping options presented at checkout (more accurate, more UI complexity) or a blended shipping calculation you absorb the variance on (simpler, but you eat the margin risk). Medusa v2's fulfillment module supports multiple fulfillment providers per order, which is the mechanism you would extend to let each seller-scoped order carry its own shipping method independent of the others in the same original cart.
Step 4: Fulfillment and Order Status Per Seller
Because checkout produces one order per seller, downstream fulfillment is close to Medusa's default behavior — each seller's admin marks their own order fulfilled, generates their own shipping label, and their own order status progresses independently. This is one of the biggest wins of the split-at-checkout approach over trying to retrofit multi-seller status onto a single combined order: you get Medusa's existing fulfillment state machine for free, per seller, instead of inventing a parallel sub-order status system bolted onto one order object.
Where you do need custom work is the customer-facing view. A customer who placed one checkout now has three orders in the system; your storefront's "order history" and "order confirmation" pages need to either group those by an original-cart/checkout-group identifier (recommended — store a shared checkout_group_id across the sibling orders at creation time) or explicitly present them as separate shipments from separate sellers, which is the honest and expected pattern on any real multi-vendor marketplace.
Step 5: Payouts and Commission
Payout is a scheduled job, not a checkout-time action — you do not want to be wiring money to a seller's bank account synchronously inside the customer's checkout request. A typical pattern: a recurring workflow (Medusa v2 supports scheduled workflows natively) queries pending payout ledger entries older than your return/dispute window, batches them by seller, and hands the batch to a payments-out provider (Stripe Connect transfers, PayPal Payouts, or a manual bank-transfer export, depending on scale and region).
Keep the payout ledger append-only. Never edit a payout row in place once it has been marked paid — if a refund happens after payout, write a new negative adjustment entry rather than mutating history. This is standard accounting hygiene, and it is what saves you when a seller disputes a payout six months later and you need to reconstruct exactly what happened and when.
Where This Fits With Headless and AI-Assisted Frontends
Everything above is backend and data-model work, deliberately independent of whatever renders the storefront. That separation is the point of a headless commerce engine: Medusa owns sellers, orders, and payouts, while the frontend — increasingly a Next.js storefront, sometimes assembled quickly with AI design-to-code tooling — just needs a clean set of storefront and vendor-dashboard APIs to call. A seller-facing dashboard (order queue, payout history, product management scoped to that seller) is a natural second frontend against the same Medusa backend, and structuring your modules and query patterns cleanly, as described above, is what makes that second frontend cheap to build later instead of requiring backend rework.
For teams assembling the customer-facing storefront quickly, our Figma UI kits give you production-ready screens and components to design or hand off from, and our ecommerce Figma bundle specifically covers the catalog, cart, and checkout patterns a marketplace frontend needs — product listing with seller attribution, multi-seller cart states, and order confirmation layouts. Polo Themes today ships Figma kits and Shopify OS 2.0 themes; a fully custom multi-vendor build like the one described here sits outside Shopify's native data model and is why teams pursuing marketplace commerce typically look toward a headless, Medusa-style backend instead.
Common Pitfalls
- Treating commission as a float percentage. Use integer basis points and round consistently (always round down on the seller's net, never up) to avoid cumulative reconciliation drift.
- One order for the whole cart. Combining sellers into a single order object looks simpler on day one and becomes unworkable the first time one seller ships and another is backordered.
- Trusting client-supplied seller IDs in admin API calls. Always resolve the acting seller from the authenticated actor, never from a request body field.
- Synchronous payouts inside checkout. Payout is a batched, auditable, asynchronous process — coupling it to checkout latency and failure modes is a reliability risk with no upside.
- Skipping the checkout-group identifier. Without a shared id linking sibling orders from one checkout, customer support and the storefront order-history page both become harder to build correctly after the fact.
Frequently Asked Questions
Does Medusa v2 have built-in multi-vendor support?
Not natively. Medusa v2 does not ship a Seller module or cart-splitting workflow out of the box, but its module and workflow architecture is specifically designed to let you add this kind of domain cleanly through module links rather than forking core code, which is why it is a common foundation for marketplace builds.
Should one customer checkout create one order or many orders?
Create one order per seller, linked by a shared checkout-group identifier. This lets each seller use Medusa's normal, independent fulfillment and order-status flow, and avoids inventing a parallel sub-order status system on a single combined order.
How should commission be calculated and stored?
Store commission rates as integers in basis points, not floating-point percentages, and decide early whether the rate is seller-level, category-level, or product-level — changing the granularity after payouts have already run is a difficult migration.
Can I run payouts automatically at checkout time?
No — payouts should be a scheduled, batched, asynchronous workflow that reads from an append-only payout ledger, run after your return or dispute window has passed, not a synchronous step inside the customer's checkout request.