Mercur is a Medusa plugin. Every rule here is either a Medusa framework requirement or a Mercur convention that keeps the marketplace layer consistent. When Medusa’s docs and this guide agree, follow both; when in doubt, mirror an existing module, workflow, or route in
packages/core.The layered architecture
Every feature in Mercur flows through the same four layers, top to bottom. Data and requests move down; results move back up. A layer may only talk to the layer directly beneath it.Module
Owns one domain’s data. Thin CRUD only — no orchestration, no cross-module calls.
Workflow
Orchestrates a business operation across modules, step by step, with automatic rollback (compensation) on failure.
API Route
A thin HTTP adapter: validate input, run a workflow (or query for reads), shape the response.
Frontend
Admin/Vendor panels and storefront. Talks to the API only through the typed SDK — never raw
fetch.- Testability — business logic lives in workflows, which can be run in isolation without an HTTP request.
- Reusability — a workflow can be called from a route, a subscriber, or a scheduled job.
- Upgrade safety — modules stay thin, so Medusa framework upgrades rarely touch your logic.
- Rollback — because mutations are workflow steps, a failure halfway through automatically undoes the earlier steps.
The non-negotiables
These are hard rules. Breaking one produces code that looks like it works but silently violates the architecture — no rollback, broken upgrades, or data written outside a workflow. Two more that follow from the above:- Modules are thin. A module service is CRUD plus small, self-contained helpers. If a method touches more than one module’s data, it belongs in a workflow, not the service.
- One mutation per step. Each workflow step performs a single mutation and defines how to compensate it. This is what makes rollback reliable.
Logic-placement cheat sheet
When you’re about to write a piece of logic, find the concern in this table before you decide where the code goes. The “Never put it in” column is the part people get wrong.| Concern | Put it in | Never put it in |
|---|---|---|
| Input shape / type validation | API route (Zod schema on the request) | Module service, workflow |
| Ownership / scoping (e.g. this seller may only see its own orders) | API middleware (filters) + workflow guard | The frontend alone |
| Business orchestration (multi-step, multi-module operations) | Workflow (steps + compensation) | API route handler, module service |
| A single data mutation | Workflow step (module service call inside it) | API route, subscriber, job |
| Cross-module reads | Query (query.graph) | Direct service-to-service imports |
| Cross-module relationships | Module link (defineLink) | A foreign key inside one module’s model |
| Reacting to something that happened (emails, sync, indexing) | Subscriber (listens to an event, runs a workflow) | Inline inside the route that caused it |
| Periodic / time-based work (polling, daily settlement) | Scheduled job (runs a workflow) | A subscriber, a route |
| Emitting domain events | Workflow step (emitEventStep) | Module service, subscriber |
| Response shaping / field selection | API route queryConfig (fields) | The module service |
| Presentation, composition, UX | Frontend (panels / storefront) | The API or any backend layer |
How to read the rest of this section
Each page that follows drills into one layer and its rules:Modules
Thin CRUD, naming, and what must never live in a service.
Module links
defineLink, link direction, and filtering by links.Workflows
Composition constraints, steps, compensation, and the query engine.
API routes
Thin adapters, Zod validation, middlewares as filters,
queryConfig.Subscribers & jobs
Event-driven side effects and scheduled work done safely.