Design principles
Everything below follows from four decisions.- You run it. MIT-licensed, PostgreSQL, Node. No vendor in the request path, no GMV fee, no proprietary runtime. Self-host, or deploy to any cloud that runs a Node process and a database.
- Extension over configuration. Instead of hundreds of settings, the platform exposes typed extension zones: workflow hooks, API routes, modules, custom fields, pages, and widgets. When a zone isn’t enough, the source is yours to change.
- Marketplace on top of commerce, not instead of it. Products, carts, orders, payments, and fulfillment are handled by Medusa, a mature commerce engine. Mercur adds only what multi-vendor requires: sellers, offers, order splitting, commissions, payouts, and governance.
- Governance is structural. Role scoping, the product change pipeline, and exact-precision money arithmetic are properties of the architecture, not features you enable.
The layers
Mercur is a single deployable backend with three API surfaces, plus two panel applications that are ordinary clients of those APIs.Data
One PostgreSQL database. Every domain owns its own tables and never reaches into another’s; relationships across domains are declared explicitly as links (see Links). That isolation is what makes a domain replaceable, and what keeps a schema change local instead of platform-wide. Redis backs the event bus, the workflow engine, and caching in production. In development both fall back to in-memory implementations, so a laptop needs nothing but Postgres.Domain
The domain layer is where marketplace behaviour lives: seller accounts and members, offers against a shared product catalog, commission rules and their resolution, payout accounts and transfers, order groups, the product change pipeline. It calls into the commerce domain for anything commerce already does. Two things about this layer matter to an architect:- It is not a wrapper. Mercur does not proxy or re-implement commerce endpoints. Marketplace concepts are first-class records with their own lifecycle, joined to commerce records through links.
- Your own domains sit beside it. A module you write, a module Mercur ships, and a module Medusa ships are the same kind of object, registered the same way, with the same access to the container, the event bus, and the workflow engine. There is no privileged inner ring.
API
Three HTTP surfaces, one per audience, separated because their authorization models differ, not because their data does.
A route is a thin adapter: middleware authenticates and scopes, a Zod validator
checks the payload, and the handler runs a workflow for writes or the query
engine for reads. Business logic does not live in routes, which is why adding
one is cheap and overriding one is safe. See the
API conventions.
Clients
The admin panel and the vendor portal are separate React applications, not a templated back office. They talk to the backend through@mercurjs/client, a
typed fetch wrapper generated from the real route definitions — including routes
you add — so a backend change that breaks a caller fails at tsc, not in
production.
The storefront is deliberately not shipped as a fixed application. Anything that
speaks HTTP consumes the Store API; a reference Next.js storefront is available
to start from.
The building blocks
Four primitives compose the domain layer. You use the same four to extend it.Modules
A module owns one domain: its data models, its service, its migrations. Modules never import each other. That constraint is what lets you swap Mercur’s commission logic for your own, or drop a module you don’t use, without a refactor rippling outward. Modules →Links
A link declares a relationship between records in two modules without either module knowing about the other. The product–seller link, for example, is what allowlists which sellers may sell a given product — expressed as a link rather than a foreign key, so neither the product model nor the seller model is modified. Your modules link to built-in entities exactly the same way. Links →Workflows
A workflow is a multi-step operation with automatic compensation: if step five fails, steps one through four roll back. Anything that crosses domains or must not half-happen is a workflow — seller approval, payout transfer, and above all cart completion, which validates the cart, splits it by seller, creates an order each, allocates payment, and computes commission lines as one atomic unit. Workflows also expose hooks, which is the primary backend extension zone. Workflows →Events and subscribers
Workflows emit events; subscribers react asynchronously. Notifications, webhooks to your systems, search indexing, and payout side effects all hang off this, which keeps the transactional path short and lets you add behaviour without touching the workflow that triggered it. Subscribers and jobs →Extension zones
This is the part that determines what a build actually costs. Mercur exposes six zones. Each one is typed, each one is additive, and none of them require forking or patching platform code.1. Workflow hooks — change what happens
The default way to change platform behaviour. A hook is a declared point inside an existing workflow where you register your own step. Your step runs inside the original transaction and participates in its rollback, so you are not reimplementing the flow to add one rule.src/workflows/hooks/seller-approved.ts
2. API routes — add endpoints
Drop aroute.ts under src/api/** and the endpoint exists. Routes nest under
the built-in ones, so a new resource can hang off an existing entity rather than
living off to the side.
3. Modules and links — add domains
When the data has its own lifecycle — subscriptions, RMAs, contracts, quotas — it is a module, not a field. You define the models and service, link the module to the built-in entities it relates to, and it becomes queryable in the same graph as everything else. This is how substantial vertical features get built, and it is the same mechanism Mercur’s own domains use.4. Custom fields — add data to built-in entities
For one plain property on an existing record —is_featured on a product,
tier on a customer — you declare the field in configuration and the storage,
link, and migration are generated for you.
medusa-config.ts
5. Panel pages and widgets — change what teams see
The panels are extended by file convention. A file’s folder decides what it does, and there is no manifest to maintain.
A widget targets a slot on an existing page, which is how you put a payout
summary on the order detail screen without owning that screen:
apps/vendor/src/widgets/payout-summary.tsx
extension-targets.d.ts, so a mistargeted extension fails type-checking instead
of silently not rendering.
Panel extensions →
6. Blocks — install features as source
Larger features ship as blocks. The CLI copies the source into your project rather than adding a dependency, so a block is yours from the moment it lands: readable, debuggable, and modifiable without waiting on an upstream release. Updates are explicit —diff against the registry and take what you want.
Choosing a zone
Governance
Multi-vendor means untrusted writers, so the guarantees operators need are built into the layers rather than layered on.- Scoping before handling. Vendor requests are narrowed to the caller’s seller in middleware, ahead of any handler. A route cannot accidentally leak across sellers, including a route you add.
- An immutable change pipeline. Product edits are recorded as
ProductChangeentries — who changed what, who approved it, when. Low-risk edits auto-confirm; the rest queue for review. The audit trail is the mechanism, not a log written beside it. - Exact money. Commission and payout arithmetic uses arbitrary-precision numbers throughout, so splitting a payment across sellers stays exact.
- Agents inside the same guardrails. The typed client, exposed workflows,
llms.txt, and the MCP server give AI agents structured contracts — subject to the same roles and the same review pipeline as human users.
How a multi-vendor order flows
One cart, several sellers. This is the path that touches every layer.- The customer adds items from multiple sellers to a single cart (Store API).
- Cart completion starts the split-order workflow.
- Items are grouped by seller and an order is created per seller, all under one order group.
- Commission lines are computed per order from the matching rules.
- Payment is allocated proportionally across the seller orders.
- Each seller’s share is credited to its payout account, net of commission.
- Events fire: notifications, webhooks, indexing, and any hook you registered.
- Sellers fulfill their own orders in the vendor portal; the operator sees all of it in the admin panel.
Stack
Mercur’s own code is distributed as
@mercurjs/core, a Medusa plugin registered
by withMercur() in your medusa-config.ts. That packaging matters when you
upgrade; it does not change any of the above.
Next steps
Platform modules
Data models, workflows, and events for each marketplace domain.
Panel extensions
Pages, widgets, custom fields, and navigation.
Extend a workflow
Inject a step into an existing flow through a hook.
API reference
Authentication, seller scoping, and the three API surfaces.