Skip to main content
This section is a practical guide for developing on Mercur, written for both human developers and AI coding agents. It captures the conventions the codebase already follows so that new code reads as if it belongs, stays testable, and survives upgrades of the underlying Medusa framework.
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.
Why this shape matters:
  • 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.
All mutations go through a workflow. Never write to the database directly from an API route, a subscriber, or a scheduled job. Reads may query directly; writes must run a workflow so they get validation, compensation, and event emission.
Only GET, POST, and DELETE. Mercur (following Medusa) does not use PUT or PATCH. Updates are modeled as POST to the resource. Keep every route to these three verbs.
No cross-module service calls. A module service must never import or call another module’s service. Modules are isolated. Cross-module reads happen through Query (the graph); cross-module relationships are declared with module links; cross-module writes are coordinated in 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.
ConcernPut it inNever put it in
Input shape / type validationAPI 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 guardThe frontend alone
Business orchestration (multi-step, multi-module operations)Workflow (steps + compensation)API route handler, module service
A single data mutationWorkflow step (module service call inside it)API route, subscriber, job
Cross-module readsQuery (query.graph)Direct service-to-service imports
Cross-module relationshipsModule 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 eventsWorkflow step (emitEventStep)Module service, subscriber
Response shaping / field selectionAPI route queryConfig (fields)The module service
Presentation, composition, UXFrontend (panels / storefront)The API or any backend layer
A quick mental test: “Does this change data?” → it must run inside a workflow. “Does this react to a change?” → it’s a subscriber. “Does this run on a schedule?” → it’s a job. “Is this just reading and shaping data for a screen?” → it’s a route + Query. Everything else is either module CRUD or frontend.

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.