@mercurjs/docs 2.2.1 → 2.3.0-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/content/home.mdx +107 -0
  2. package/content/learn/architecture.mdx +168 -83
  3. package/content/learn/introduction.mdx +161 -56
  4. package/content/learn/migration-to-2-0.mdx +187 -0
  5. package/content/learn/why-mercur.mdx +91 -0
  6. package/content/platform/attribute/concepts/attribute-types.mdx +59 -0
  7. package/content/platform/attribute/concepts/global-vs-inline.mdx +50 -0
  8. package/content/platform/attribute/concepts/variant-axes.mdx +61 -0
  9. package/content/platform/attribute/guides/attach-attributes-to-a-product.mdx +69 -0
  10. package/content/platform/attribute/guides/create-a-variant-axis.mdx +83 -0
  11. package/content/platform/attribute/guides/create-an-attribute.mdx +81 -0
  12. package/content/platform/attribute/overview.mdx +87 -0
  13. package/content/platform/attribute/reference/data-models.mdx +56 -0
  14. package/content/platform/attribute/reference/events.mdx +41 -0
  15. package/content/platform/attribute/reference/links.mdx +45 -0
  16. package/content/platform/attribute/reference/service.mdx +49 -0
  17. package/content/platform/attribute/reference/workflows.mdx +45 -0
  18. package/content/platform/catalog/concepts/master-products.mdx +54 -0
  19. package/content/platform/catalog/concepts/product-seller-allowlist.mdx +48 -0
  20. package/content/platform/catalog/concepts/status-lifecycle.mdx +58 -0
  21. package/content/platform/catalog/concepts/variants-categories-collections.mdx +49 -0
  22. package/content/platform/catalog/guides/allowlist-stores.mdx +59 -0
  23. package/content/platform/catalog/guides/create-a-master-product.mdx +70 -0
  24. package/content/platform/catalog/guides/publish-or-reject-a-product.mdx +79 -0
  25. package/content/platform/catalog/overview.mdx +90 -0
  26. package/content/platform/catalog/reference/data-models.mdx +66 -0
  27. package/content/platform/catalog/reference/events.mdx +41 -0
  28. package/content/platform/catalog/reference/links.mdx +44 -0
  29. package/content/platform/catalog/reference/service.mdx +52 -0
  30. package/content/platform/catalog/reference/workflows.mdx +40 -0
  31. package/content/platform/commission/concepts/order-commission-lines.mdx +65 -0
  32. package/content/platform/commission/concepts/rule-matching.mdx +86 -0
  33. package/content/platform/commission/concepts/rules-and-rates.mdx +84 -0
  34. package/content/platform/commission/guides/batch-update-rules.mdx +58 -0
  35. package/content/platform/commission/guides/create-a-rate.mdx +72 -0
  36. package/content/platform/commission/guides/refresh-order-commission-lines.mdx +55 -0
  37. package/content/platform/commission/overview.mdx +86 -0
  38. package/content/platform/commission/reference/data-models.mdx +75 -0
  39. package/content/platform/commission/reference/events.mdx +58 -0
  40. package/content/platform/commission/reference/links.mdx +45 -0
  41. package/content/platform/commission/reference/service.mdx +51 -0
  42. package/content/platform/commission/reference/workflows.mdx +40 -0
  43. package/content/platform/offer/concepts/pricing-and-inventory.mdx +72 -0
  44. package/content/platform/offer/concepts/shipping.mdx +48 -0
  45. package/content/platform/offer/concepts/what-is-an-offer.mdx +65 -0
  46. package/content/platform/offer/guides/bulk-create-offers.mdx +84 -0
  47. package/content/platform/offer/guides/create-an-offer.mdx +83 -0
  48. package/content/platform/offer/guides/manage-offer-inventory.mdx +53 -0
  49. package/content/platform/offer/overview.mdx +84 -0
  50. package/content/platform/offer/reference/data-models.mdx +56 -0
  51. package/content/platform/offer/reference/events.mdx +39 -0
  52. package/content/platform/offer/reference/links.mdx +60 -0
  53. package/content/platform/offer/reference/service.mdx +53 -0
  54. package/content/platform/offer/reference/workflows.mdx +38 -0
  55. package/content/platform/order-group/concepts/computed-totals.mdx +59 -0
  56. package/content/platform/order-group/concepts/order-splitting.mdx +61 -0
  57. package/content/platform/order-group/concepts/the-order-group.mdx +64 -0
  58. package/content/platform/order-group/guides/list-order-groups.mdx +67 -0
  59. package/content/platform/order-group/guides/retrieve-an-order-group.mdx +63 -0
  60. package/content/platform/order-group/guides/split-a-cart.mdx +58 -0
  61. package/content/platform/order-group/overview.mdx +83 -0
  62. package/content/platform/order-group/reference/data-models.mdx +40 -0
  63. package/content/platform/order-group/reference/events.mdx +37 -0
  64. package/content/platform/order-group/reference/links.mdx +45 -0
  65. package/content/platform/order-group/reference/service.mdx +50 -0
  66. package/content/platform/order-group/reference/workflows.mdx +39 -0
  67. package/content/platform/payout/concepts/account-lifecycle.mdx +75 -0
  68. package/content/platform/payout/concepts/accounts-and-onboarding.mdx +70 -0
  69. package/content/platform/payout/concepts/payout-pipeline.mdx +99 -0
  70. package/content/platform/payout/guides/create-a-payout-account.mdx +59 -0
  71. package/content/platform/payout/guides/process-a-provider-webhook.mdx +74 -0
  72. package/content/platform/payout/guides/start-provider-onboarding.mdx +50 -0
  73. package/content/platform/payout/overview.mdx +86 -0
  74. package/content/platform/payout/reference/data-models.mdx +61 -0
  75. package/content/platform/payout/reference/events.mdx +48 -0
  76. package/content/platform/payout/reference/links.mdx +36 -0
  77. package/content/platform/payout/reference/service.mdx +53 -0
  78. package/content/platform/payout/reference/workflows.mdx +32 -0
  79. package/content/platform/product-edit/concepts/change-actions.mdx +66 -0
  80. package/content/platform/product-edit/concepts/change-pipeline.mdx +69 -0
  81. package/content/platform/product-edit/concepts/status-and-auto-confirm.mdx +66 -0
  82. package/content/platform/product-edit/guides/confirm-or-decline-a-change.mdx +76 -0
  83. package/content/platform/product-edit/guides/edit-a-product.mdx +74 -0
  84. package/content/platform/product-edit/guides/request-a-revision.mdx +54 -0
  85. package/content/platform/product-edit/overview.mdx +85 -0
  86. package/content/platform/product-edit/reference/data-models.mdx +65 -0
  87. package/content/platform/product-edit/reference/events.mdx +47 -0
  88. package/content/platform/product-edit/reference/links.mdx +39 -0
  89. package/content/platform/product-edit/reference/service.mdx +43 -0
  90. package/content/platform/product-edit/reference/workflows.mdx +49 -0
  91. package/content/platform/review/concepts/product-vs-seller-reviews.mdx +63 -0
  92. package/content/platform/review/concepts/ratings-and-moderation.mdx +64 -0
  93. package/content/platform/review/concepts/the-review-model.mdx +56 -0
  94. package/content/platform/review/guides/compute-aggregate-ratings.mdx +57 -0
  95. package/content/platform/review/guides/create-a-review.mdx +55 -0
  96. package/content/platform/review/guides/moderate-a-review.mdx +58 -0
  97. package/content/platform/review/guides/respond-to-a-review.mdx +61 -0
  98. package/content/platform/review/overview.mdx +87 -0
  99. package/content/platform/review/reference/data-models.mdx +36 -0
  100. package/content/platform/review/reference/events.mdx +61 -0
  101. package/content/platform/review/reference/links.mdx +43 -0
  102. package/content/platform/review/reference/service.mdx +54 -0
  103. package/content/platform/review/reference/workflows.mdx +31 -0
  104. package/content/platform/store/concepts/lifecycle.mdx +62 -0
  105. package/content/platform/store/concepts/store-entity.mdx +53 -0
  106. package/content/platform/store/concepts/team.mdx +50 -0
  107. package/content/platform/store/guides/create-a-store.mdx +55 -0
  108. package/content/platform/store/guides/manage-the-team.mdx +55 -0
  109. package/content/platform/store/guides/moderate-a-store.mdx +59 -0
  110. package/content/platform/store/overview.mdx +86 -0
  111. package/content/platform/store/reference/data-models.mdx +89 -0
  112. package/content/platform/store/reference/events.mdx +43 -0
  113. package/content/platform/store/reference/links.mdx +71 -0
  114. package/content/platform/store/reference/service.mdx +51 -0
  115. package/content/platform/store/reference/workflows.mdx +35 -0
  116. package/content/references/api/admin/commission-rates/create-commission-rate.mdx +1 -1
  117. package/content/references/api/admin/commission-rates/list-commission-rates.mdx +2 -2
  118. package/content/references/api/admin/commission-rates/update-commission-rate.mdx +1 -1
  119. package/content/references/api/admin/offers/batch-create-offers.mdx +3 -3
  120. package/content/references/api/admin/order-groups/list-order-groups.mdx +1 -1
  121. package/content/references/api/admin/product-attributes/create-attribute-value.mdx +2 -2
  122. package/content/references/api/admin/product-attributes/create-product-attribute.mdx +1 -1
  123. package/content/references/api/admin/product-attributes/update-product-attribute.mdx +1 -1
  124. package/content/references/api/admin/product-changes/confirm-product-change.mdx +1 -1
  125. package/content/references/api/admin/products/batch-product-attributes.mdx +1 -1
  126. package/content/references/api/admin/products/create-product.mdx +1 -1
  127. package/content/references/api/admin/products/preview-product.mdx +1 -1
  128. package/content/references/api/admin.mdx +4 -5
  129. package/content/references/api/conventions.mdx +9 -7
  130. package/content/references/api/store/carts/add-line-item.mdx +1 -1
  131. package/content/references/api/store/offers/list-offers.mdx +1 -1
  132. package/content/references/api/store/order-groups/list-order-groups.mdx +1 -1
  133. package/content/references/api/store.mdx +4 -13
  134. package/content/references/api/vendor/members/accept-member-invite.mdx +1 -1
  135. package/content/references/api/vendor/offers/batch-create-offers.mdx +4 -0
  136. package/content/references/api/vendor/offers/batch-offer-inventory-items.mdx +2 -0
  137. package/content/references/api/vendor/offers/create-offer.mdx +13 -1
  138. package/content/references/api/vendor/offers/list-offers.mdx +4 -0
  139. package/content/references/api/vendor/offers/retrieve-offer.mdx +4 -0
  140. package/content/references/api/vendor/offers/update-offer.mdx +12 -0
  141. package/content/references/api/vendor/payout-accounts/create-onboarding.mdx +1 -1
  142. package/content/references/api/vendor/products/batch-product-attributes.mdx +1 -1
  143. package/content/references/api/vendor/products/create-product-variant.mdx +1 -1
  144. package/content/references/api/vendor/products/create-product.mdx +1 -1
  145. package/content/references/api/vendor/products/delete-product.mdx +1 -1
  146. package/content/references/api/vendor/products/update-product.mdx +1 -1
  147. package/content/references/api/vendor/sellers/create-seller.mdx +2 -2
  148. package/content/references/api/vendor/sellers/list-sellers.mdx +1 -1
  149. package/content/references/api/vendor.mdx +6 -5
  150. package/content/references/configuration.mdx +16 -33
  151. package/content/references/overview.mdx +34 -52
  152. package/content/references/panel-extensions/create-page.mdx +194 -0
  153. package/content/references/panel-extensions/custom-fields.mdx +256 -0
  154. package/content/references/panel-extensions/overview.mdx +102 -0
  155. package/content/references/panel-extensions/widgets.mdx +212 -0
  156. package/content/resources/ai/mcp.mdx +2 -2
  157. package/content/resources/ai/overview.mdx +21 -16
  158. package/content/resources/ai/skills.mdx +67 -0
  159. package/content/resources/best-practices/api-routes.mdx +55 -43
  160. package/content/resources/best-practices/custom-fields.mdx +116 -92
  161. package/content/resources/best-practices/frontend.mdx +62 -50
  162. package/content/resources/best-practices/module-links.mdx +48 -34
  163. package/content/resources/best-practices/modules.mdx +53 -27
  164. package/content/resources/best-practices/overview.mdx +45 -17
  165. package/content/resources/best-practices/subscribers-and-jobs.mdx +37 -24
  166. package/content/resources/best-practices/types.mdx +38 -23
  167. package/content/resources/best-practices/workflows.mdx +33 -21
  168. package/content/resources/customization/custom-fields.mdx +15 -15
  169. package/content/resources/customization/extend-a-workflow.mdx +7 -4
  170. package/content/resources/customization/extending-panels.mdx +55 -52
  171. package/content/resources/deployment/medusa-cloud.mdx +21 -20
  172. package/content/resources/deployment/self-host.mdx +123 -0
  173. package/content/resources/integrations/overview.mdx +38 -0
  174. package/content/resources/integrations/stripe-connect.mdx +39 -38
  175. package/content/resources/tutorials/add-a-block.mdx +25 -18
  176. package/content/resources/tutorials/add-a-widget.mdx +32 -23
  177. package/content/resources/tutorials/add-order-detail-button.mdx +33 -20
  178. package/content/resources/tutorials/attributes-and-variant-axes.mdx +28 -27
  179. package/content/resources/tutorials/build-a-block.mdx +26 -15
  180. package/content/resources/tutorials/custom-api-route.mdx +32 -20
  181. package/content/resources/tutorials/custom-panel-page.mdx +21 -12
  182. package/content/resources/tutorials/customize-navigation.mdx +30 -23
  183. package/content/resources/tutorials/extend-forms-and-tables.mdx +36 -28
  184. package/content/resources/tutorials/extend-onboarding.mdx +38 -35
  185. package/content/resources/tutorials/master-products-and-offers.mdx +28 -23
  186. package/content/telemetry.mdx +3 -3
  187. package/content/user-guide/admin/attributes/how-tos/create-an-attribute.mdx +64 -0
  188. package/content/user-guide/admin/attributes/how-tos/manage-possible-values.mdx +40 -0
  189. package/content/user-guide/admin/attributes/overview.mdx +22 -0
  190. package/content/user-guide/admin/commissions/how-tos/create-a-commission-rule.mdx +63 -0
  191. package/content/user-guide/admin/commissions/how-tos/edit-the-global-commission.mdx +48 -0
  192. package/content/user-guide/admin/commissions/how-tos/manage-a-commission-rule.mdx +45 -0
  193. package/content/user-guide/admin/commissions/overview.mdx +25 -0
  194. package/content/user-guide/admin/overview.mdx +20 -12
  195. package/content/user-guide/admin/product-requests/how-tos/review-a-new-product.mdx +58 -0
  196. package/content/user-guide/admin/product-requests/how-tos/review-a-product-edit.mdx +48 -0
  197. package/content/user-guide/admin/product-requests/overview.mdx +25 -0
  198. package/content/user-guide/vendor/offers/how-tos/create-an-offer.mdx +59 -0
  199. package/content/user-guide/vendor/offers/how-tos/update-prices-and-stock.mdx +40 -0
  200. package/content/user-guide/vendor/offers/overview.mdx +22 -0
  201. package/content/user-guide/vendor/onboarding.mdx +79 -0
  202. package/content/user-guide/vendor/orders/how-tos/fulfill-an-order.mdx +49 -0
  203. package/content/user-guide/vendor/orders/how-tos/mark-an-order-as-delivered.mdx +33 -0
  204. package/content/user-guide/vendor/orders/how-tos/process-a-return.mdx +42 -0
  205. package/content/user-guide/vendor/orders/how-tos/refund-an-order.mdx +38 -0
  206. package/content/user-guide/vendor/orders/how-tos/ship-an-order.mdx +40 -0
  207. package/content/user-guide/vendor/orders/overview.mdx +31 -0
  208. package/content/user-guide/vendor/overview.mdx +23 -12
  209. package/content/user-guide/vendor/products/how-tos/edit-a-product.mdx +44 -0
  210. package/content/user-guide/vendor/products/how-tos/submit-a-product.mdx +63 -0
  211. package/content/user-guide/vendor/products/overview.mdx +22 -0
  212. package/llms.txt +176 -142
  213. package/package.json +1 -1
  214. package/content/learn/concepts.mdx +0 -84
  215. package/content/learn/installation.mdx +0 -117
  216. package/content/learn/mirakl-alternative.mdx +0 -86
  217. package/content/migration/from-1-x-to-2-0.mdx +0 -152
  218. package/content/migration/from-2-0-to-2-1.mdx +0 -105
  219. package/content/migration/overview.mdx +0 -58
  220. package/content/references/api/store/search/search.mdx +0 -136
  221. package/content/references/modules/commission.mdx +0 -106
  222. package/content/references/modules/custom-fields.mdx +0 -45
  223. package/content/references/modules/media.mdx +0 -55
  224. package/content/references/modules/offer.mdx +0 -64
  225. package/content/references/modules/payout.mdx +0 -121
  226. package/content/references/modules/product-attribute.mdx +0 -111
  227. package/content/references/modules/product-edit.mdx +0 -80
  228. package/content/references/modules/search.mdx +0 -112
  229. package/content/references/modules/seller.mdx +0 -175
  230. package/content/references/panel-extension-api.mdx +0 -337
  231. package/content/references/workflows/cart/add-seller-shipping-method-to-cart.mdx +0 -48
  232. package/content/references/workflows/cart/complete-cart-with-split-orders.mdx +0 -36
  233. package/content/references/workflows/cart/list-seller-shipping-options-for-cart.mdx +0 -39
  234. package/content/references/workflows/cart/update-cart-seller-promotions.mdx +0 -42
  235. package/content/references/workflows/commission/batch-commission-rules.mdx +0 -49
  236. package/content/references/workflows/commission/create-commission-rates.mdx +0 -41
  237. package/content/references/workflows/commission/delete-commission-rates.mdx +0 -34
  238. package/content/references/workflows/commission/refresh-order-commission-lines.mdx +0 -30
  239. package/content/references/workflows/commission/update-commission-rates.mdx +0 -35
  240. package/content/references/workflows/media/set-category-images.mdx +0 -41
  241. package/content/references/workflows/media/set-collection-images.mdx +0 -41
  242. package/content/references/workflows/member/accept-member-invite.mdx +0 -33
  243. package/content/references/workflows/member/add-seller-member.mdx +0 -30
  244. package/content/references/workflows/member/create-member-invites.mdx +0 -34
  245. package/content/references/workflows/member/delete-member-invite.mdx +0 -24
  246. package/content/references/workflows/member/remove-seller-member.mdx +0 -28
  247. package/content/references/workflows/member/resend-member-invite.mdx +0 -28
  248. package/content/references/workflows/member/update-member-role.mdx +0 -28
  249. package/content/references/workflows/member/update-member.mdx +0 -35
  250. package/content/references/workflows/offer/batch-offer-inventory-items.mdx +0 -62
  251. package/content/references/workflows/offer/create-offers.mdx +0 -65
  252. package/content/references/workflows/offer/delete-offers.mdx +0 -35
  253. package/content/references/workflows/offer/update-offers.mdx +0 -53
  254. package/content/references/workflows/order/cancel-order-fulfillment.mdx +0 -34
  255. package/content/references/workflows/order/confirm-claim-request.mdx +0 -25
  256. package/content/references/workflows/order/confirm-exchange-request.mdx +0 -25
  257. package/content/references/workflows/order/confirm-order-edit-request.mdx +0 -25
  258. package/content/references/workflows/order/confirm-return-receive.mdx +0 -25
  259. package/content/references/workflows/order/create-order-fulfillment.mdx +0 -45
  260. package/content/references/workflows/order-group/get-order-group-detail.mdx +0 -29
  261. package/content/references/workflows/order-group/get-order-groups-list.mdx +0 -38
  262. package/content/references/workflows/overview.mdx +0 -72
  263. package/content/references/workflows/payout/create-onboarding.mdx +0 -36
  264. package/content/references/workflows/payout/create-payout-account.mdx +0 -33
  265. package/content/references/workflows/payout/create-payout.mdx +0 -30
  266. package/content/references/workflows/payout/process-payout-for-webhook.mdx +0 -34
  267. package/content/references/workflows/product/confirm-products.mdx +0 -48
  268. package/content/references/workflows/product/create-products.mdx +0 -63
  269. package/content/references/workflows/product/link-sellers-to-product-category.mdx +0 -40
  270. package/content/references/workflows/product/link-sellers-to-product.mdx +0 -40
  271. package/content/references/workflows/product/reject-product.mdx +0 -48
  272. package/content/references/workflows/product/request-product-change.mdx +0 -48
  273. package/content/references/workflows/product-attribute/add-product-attributes-to-product.mdx +0 -49
  274. package/content/references/workflows/product-attribute/create-and-link-product-attributes-to-product.mdx +0 -46
  275. package/content/references/workflows/product-attribute/create-product-attribute-values.mdx +0 -43
  276. package/content/references/workflows/product-attribute/create-product-attributes.mdx +0 -56
  277. package/content/references/workflows/product-attribute/delete-product-attribute-values.mdx +0 -30
  278. package/content/references/workflows/product-attribute/delete-product-attributes.mdx +0 -30
  279. package/content/references/workflows/product-attribute/remove-product-attributes-from-product.mdx +0 -29
  280. package/content/references/workflows/product-attribute/update-product-attribute-values.mdx +0 -44
  281. package/content/references/workflows/product-attribute/update-product-attributes-on-product.mdx +0 -41
  282. package/content/references/workflows/product-attribute/update-product-attributes.mdx +0 -48
  283. package/content/references/workflows/product-attribute/upsert-product-attribute-values.mdx +0 -43
  284. package/content/references/workflows/product-edit/auto-confirm-product-change.mdx +0 -39
  285. package/content/references/workflows/product-edit/cancel-product-change.mdx +0 -49
  286. package/content/references/workflows/product-edit/confirm-product-change.mdx +0 -57
  287. package/content/references/workflows/product-edit/create-product-change.mdx +0 -72
  288. package/content/references/workflows/product-edit/reject-product-change.mdx +0 -54
  289. package/content/references/workflows/product-edit/stage-product-change.mdx +0 -75
  290. package/content/references/workflows/seller/approve-seller.mdx +0 -36
  291. package/content/references/workflows/seller/create-seller-account.mdx +0 -59
  292. package/content/references/workflows/seller/create-seller-defaults.mdx +0 -22
  293. package/content/references/workflows/seller/create-sellers.mdx +0 -65
  294. package/content/references/workflows/seller/delete-seller-professional-details.mdx +0 -37
  295. package/content/references/workflows/seller/delete-sellers.mdx +0 -24
  296. package/content/references/workflows/seller/invite-seller.mdx +0 -28
  297. package/content/references/workflows/seller/suspend-seller.mdx +0 -37
  298. package/content/references/workflows/seller/terminate-seller.mdx +0 -37
  299. package/content/references/workflows/seller/unsuspend-seller.mdx +0 -36
  300. package/content/references/workflows/seller/unterminate-seller.mdx +0 -36
  301. package/content/references/workflows/seller/update-seller-address.mdx +0 -55
  302. package/content/references/workflows/seller/update-seller-payment-details.mdx +0 -52
  303. package/content/references/workflows/seller/update-seller-professional-details.mdx +0 -48
  304. package/content/references/workflows/seller/update-sellers.mdx +0 -57
  305. package/content/resources/ai/llms.mdx +0 -74
  306. package/content/resources/integrations/notifications.mdx +0 -39
  307. package/content/resources/integrations/search.mdx +0 -122
  308. package/content/resources/tutorials/configure-commissions.mdx +0 -127
  309. package/content/resources/tutorials/first-marketplace.mdx +0 -45
  310. package/content/resources/tutorials/handle-product-requests.mdx +0 -80
  311. package/content/resources/tutorials/import-export-products.mdx +0 -96
  312. package/content/resources/tutorials/seller-payouts-stripe.mdx +0 -89
  313. package/content/resources/tutorials/store-setup-checklist.mdx +0 -214
  314. package/content/tools/api-client.mdx +0 -155
  315. package/content/tools/cli.mdx +0 -196
  316. package/content/tools/dashboard-sdk.mdx +0 -35
@@ -1,22 +1,23 @@
1
1
  ---
2
- title: "API routes"
3
- description: "Thin HTTP adapters — typed request/response generics, Zod validation with createFindParams, filterable fields, middlewares as filters, and queryConfig."
2
+ title: "How to Create an API Route"
3
+ sidebarTitle: "Create an API Route"
4
+ description: "Write API routes as thin HTTP adapters that type both sides, validate with Zod, and scope reads through middlewares and queryConfig."
4
5
  ---
5
6
 
6
- An API route is a thin adapter between HTTP and the rest of the system. Its whole job is: validate the request, run a [workflow](/rc/resources/best-practices/workflows) (for writes) or a [Query](/rc/resources/best-practices/workflows#the-query-engine) (for reads), and shape the response. No business logic lives here.
7
+ An API route is a thin adapter between HTTP and the rest of the system. It validates the request, runs a [workflow](/rc/resources/best-practices/workflows) for writes or a [Query](/rc/resources/best-practices/workflows#the-query-engine) for reads, and shapes the response. No business logic lives here.
7
8
 
8
9
  <Note>
9
- Routes are [Medusa file-based API routes](https://docs.medusajs.com/learn/fundamentals/api-routes): a `route.ts` under `src/api/**` exports handlers named after HTTP verbs, and a sibling `middlewares.ts` wires validation and filters. Examples below use a custom **Brand** module exposed under `/admin/brands`.
10
+ Routes are [Medusa file-based API routes](https://docs.medusajs.com/learn/fundamentals/api-routes). A `route.ts` under `src/api/**` exports handlers named after HTTP verbs, and a sibling `middlewares.ts` wires validation and filters. Examples below use a custom **Brand** module exposed under `/admin/brands`.
10
11
  </Note>
11
12
 
12
13
  ## Type both the request and the response
13
14
 
14
- Every handler is typed on **both** sides mirror how Medusa's own routes are written:
15
+ Type every handler on both sides. Mirror how Medusa's own routes are written:
15
16
 
16
- - `AuthenticatedMedusaRequest<TBodyOrQuery>` the generic is the validated **body** (writes) or **query params** (reads) type.
17
- - `MedusaResponse<TResponse>` the generic is the **response shape**, so `res.json(...)` is checked and the SDK infers a real return type instead of `unknown`.
17
+ - **`AuthenticatedMedusaRequest<TBodyOrQuery>`:** the generic is the validated body for writes or the query params type for reads.
18
+ - **`MedusaResponse<TResponse>`:** the generic is the response shape, so `res.json(...)` is checked and the SDK infers a real return type instead of `unknown`.
18
19
 
19
- ```ts title="src/api/admin/brands/route.ts"
20
+ ```ts src/api/admin/brands/route.ts
20
21
  import {
21
22
  AuthenticatedMedusaRequest,
22
23
  MedusaResponse,
@@ -64,13 +65,13 @@ export const POST = async (
64
65
  ```
65
66
 
66
67
  <Warning>
67
- Don't leave `MedusaResponse` bare. An untyped response means `res.json({...})` accepts anything and the typed SDK resolves that endpoint to an empty/`unknown` response the exact opposite of the point of the typed client. Always pass the response generic.
68
+ Don't leave `MedusaResponse` bare. An untyped response means `res.json({...})` accepts anything and the typed SDK resolves that endpoint to an empty or `unknown` response, the exact opposite of the point of the typed client. Always pass the response generic.
68
69
  </Warning>
69
70
 
70
71
  ## Only `GET`, `POST`, `DELETE`
71
72
 
72
73
  <Warning>
73
- Mercur routes use **only** `GET`, `POST`, and `DELETE`. There is no `PUT` or `PATCH` model an update as a `POST` to the resource. Keeping to three verbs is what keeps the typed SDK (`.query` / `.mutate` / `.delete`) consistent across every route.
74
+ Mercur routes use only `GET`, `POST`, and `DELETE`. There is no `PUT` or `PATCH`. Model an update as a `POST` to the resource. Keeping to three verbs is what keeps the typed SDK (`.query` / `.mutate` / `.delete`) consistent across every route.
74
75
  </Warning>
75
76
 
76
77
  | Verb | Meaning | SDK method |
@@ -81,11 +82,11 @@ export const POST = async (
81
82
 
82
83
  ## Validation with Zod + exported types
83
84
 
84
- Validation happens in `middlewares.ts` via `validateAndTransformBody` / `validateAndTransformQuery`, and every schema exports its inferred type so the handler generic and the SDK share one source of truth.
85
+ Validation happens in `middlewares.ts` via `validateAndTransformBody` / `validateAndTransformQuery`. Every schema exports its inferred type so the handler generic and the SDK share one source of truth.
85
86
 
86
- **Bodies** are plain Zod objects:
87
+ Bodies are plain Zod objects:
87
88
 
88
- ```ts title="src/api/admin/brands/validators.ts"
89
+ ```ts src/api/admin/brands/validators.ts
89
90
  import { z } from "zod"
90
91
 
91
92
  export const AdminCreateBrand = z.object({
@@ -96,9 +97,9 @@ export const AdminCreateBrand = z.object({
96
97
  export type AdminCreateBrandType = z.infer<typeof AdminCreateBrand>
97
98
  ```
98
99
 
99
- **List/read params** use the framework helpers `createFindParams` (pagination + `fields` + `order`) and `createSelectParams` (retrieve) rather than a hand-rolled object. This is what wires pagination and field selection consistently across every route:
100
+ List and read params use the framework helpers `createFindParams` (pagination, `fields`, and `order`) and `createSelectParams` (retrieve) rather than a hand-rolled object. This wires pagination and field selection consistently across every route:
100
101
 
101
- ```ts title="src/api/admin/brands/validators.ts"
102
+ ```ts src/api/admin/brands/validators.ts
102
103
  import { createFindParams, createOperatorMap } from "@medusajs/medusa/api/utils/validators"
103
104
 
104
105
  export const AdminGetBrandsParams = createFindParams({
@@ -117,13 +118,13 @@ export const AdminGetBrandsParams = createFindParams({
117
118
  export type AdminGetBrandsParamsType = z.infer<typeof AdminGetBrandsParams>
118
119
  ```
119
120
 
120
- The handler then trusts `req.validatedBody` / the validated query to already match those types never re-validate inside the handler.
121
+ The handler then trusts `req.validatedBody` and the validated query to already match those types. Never re-validate inside the handler.
121
122
 
122
123
  ## List vs retrieve
123
124
 
124
- A list route (`GET /admin/brands`) and a retrieve route (`GET /admin/brands/:id`) select fields the same way but differ in their params helper and response shape. Retrieve uses `createSelectParams` (field selection only no pagination or filters) and returns a single entity:
125
+ A list route (`GET /admin/brands`) and a retrieve route (`GET /admin/brands/:id`) select fields the same way but differ in their params helper and response shape. Retrieve uses `createSelectParams` (field selection only, no pagination or filters) and returns a single entity:
125
126
 
126
- ```ts title="src/api/admin/brands/[id]/route.ts"
127
+ ```ts src/api/admin/brands/[id]/route.ts
127
128
  export const GET = async (
128
129
  req: AuthenticatedMedusaRequest<AdminGetBrandParamsType>,
129
130
  res: MedusaResponse<AdminBrandResponse>
@@ -146,18 +147,18 @@ export const GET = async (
146
147
  }
147
148
  ```
148
149
 
149
- ```ts title="src/api/admin/brands/validators.ts retrieve params"
150
+ ```ts src/api/admin/brands/validators.ts: retrieve params
150
151
  import { createSelectParams } from "@medusajs/medusa/api/utils/validators"
151
152
 
152
153
  export const AdminGetBrandParams = createSelectParams()
153
154
  export type AdminGetBrandParamsType = z.infer<typeof AdminGetBrandParams>
154
155
  ```
155
156
 
156
- Both share the same `defaults` idea but declare them separately in the query config (`list` vs `retrieve`) see [`queryConfig`](/rc/resources/best-practices/api-routes#queryconfig-and-field-selection) below.
157
+ Both share the same `defaults` idea but declare them separately in the query config (`list` vs `retrieve`). See [`queryConfig`](/rc/resources/best-practices/api-routes#queryconfig-and-field-selection) below.
157
158
 
158
159
  ## Filterable fields
159
160
 
160
- `req.filterableFields` is the **parsed, validated filter set** produced by `validateAndTransformQuery` from the query params above. Only fields your validator declares can appear there an unknown query param is dropped, not passed through. The handler forwards it straight to Query:
161
+ `req.filterableFields` is the parsed, validated filter set produced by `validateAndTransformQuery` from the query params above. Only fields your validator declares can appear there. An unknown query param is dropped, not passed through. The handler forwards it straight to Query:
161
162
 
162
163
  ```ts
163
164
  const { data: brands, metadata } = await query.graph({
@@ -169,14 +170,14 @@ const { data: brands, metadata } = await query.graph({
169
170
  ```
170
171
 
171
172
  <Tip>
172
- This is why filtering is declarative and safe: to make a field filterable you add it to the validator; to scope a request you inject onto `req.filterableFields` in middleware (next section). The handler never builds a `where` clause by hand.
173
+ This is why filtering is declarative and safe. To make a field filterable you add it to the validator. To scope a request you inject onto `req.filterableFields` in middleware (next section). The handler never builds a `where` clause by hand.
173
174
  </Tip>
174
175
 
175
176
  ## Middlewares as filters
176
177
 
177
- Middlewares aren't only for validation — they're where you inject **scoping filters** so handlers stay ignorant of the rule. A small middleware writes onto `req.filterableFields`; because the handler already forwards that to Query, the scope is applied without the handler knowing. For example, force `GET /admin/brands` to only ever return active rows:
178
+ Middlewares aren't only for validation. They're where you inject scoping filters so handlers stay ignorant of the rule. A small middleware writes onto `req.filterableFields`, and because the handler already forwards that to Query, the scope is applied without the handler knowing. For example, force `GET /admin/brands` to only ever return active rows:
178
179
 
179
- ```ts title="src/api/admin/brands/middlewares.ts"
180
+ ```ts src/api/admin/brands/middlewares.ts
180
181
  import {
181
182
  MedusaRequest,
182
183
  MedusaResponse,
@@ -205,28 +206,28 @@ export const adminBrandsMiddlewares = [
205
206
  ```
206
207
 
207
208
  <Tip>
208
- Injecting a filter in middleware means a new route on the same resource is scoped by construction, not by remembering to add a filter. This works with `query.graph` because `is_active` lives on the **brand's own module**.
209
+ Injecting a filter in middleware means a new route on the same resource is scoped by construction, not by remembering to add a filter. This works with `query.graph` because `is_active` lives on the brand's own module.
209
210
  </Tip>
210
211
 
211
212
  <Warning>
212
- You can only filter this way on a field that belongs to the entity's **own module**. Filtering by a **linked** module's field (e.g. products by their `brand`) does **not** work with `query.graph` Query aggregates modules after the fact, so there's no join to filter on. Cross-module filtering requires the [Index Module](/rc/resources/best-practices/module-links#filtering-by-a-linked-field--the-index-module) and `query.index`.
213
+ You can only filter this way on a field that belongs to the entity's own module. Filtering by a linked module's field (e.g. products by their `brand`) does not work with `query.graph`. Query aggregates modules after the fact, so there's no join to filter on. Cross-module filtering requires the [Index Module](/rc/resources/best-practices/module-links#filtering-by-a-linked-field--the-index-module) and `query.index`.
213
214
  </Warning>
214
215
 
215
216
  ## Trust the auth middleware
216
217
 
217
- Authentication and actor resolution happen in middleware (`authenticate`), so by the time your handler runs the actor is already established **trust it**. Read identity from the request context, never from the body:
218
+ Authentication and actor resolution happen in middleware (`authenticate`), so by the time your handler runs the actor is already established. Trust it. Read identity from the request context, never from the body:
218
219
 
219
220
  ```ts
220
221
  const userId = req.auth_context.actor_id // set by the authenticate middleware
221
222
  ```
222
223
 
223
224
  <Warning>
224
- Don't re-derive or re-check identity inside handlers, and don't read user/owner ids from the request body always take them from `req.auth_context` (or a context object a scoping middleware populated). Trusting the middleware keeps authorization in one place.
225
+ Don't re-derive or re-check identity inside handlers, and don't read user or owner ids from the request body. Always take them from `req.auth_context` (or a context object a scoping middleware populated). Trusting the middleware keeps authorization in one place.
225
226
  </Warning>
226
227
 
227
228
  ## Vendor routes: `seller_context`
228
229
 
229
- Every route under `/vendor/*` is **already authenticated and seller-scoped** you don't wire auth yourself. By the time your handler runs, the caller is a verified seller member and the request carries a `req.seller_context` you can trust:
230
+ Every route under `/vendor/*` is already authenticated and seller-scoped. You don't wire auth yourself. By the time your handler runs, the caller is a verified seller member and the request carries a `req.seller_context` you can trust:
230
231
 
231
232
  ```ts
232
233
  export const POST = async (
@@ -239,12 +240,12 @@ export const POST = async (
239
240
  }
240
241
  ```
241
242
 
242
- `req.seller_context` gives you `seller_id`, `currency_code`, and the `seller_member` all verified, so you never re-check membership in a handler.
243
+ `req.seller_context` gives you `seller_id`, `currency_code`, and the `seller_member`, all verified, so you never re-check membership in a handler.
243
244
 
244
245
  <Warning>
245
- Never take a `seller_id` from the request body or query to decide ownership — that's caller-supplied. The **only** authoritative seller is `req.seller_context.seller_id`. To scope a vendor list route to the caller's data, add the `filterBySellerId()` middleware and every query is constrained automatically no per-handler `where`:
246
+ Never take a `seller_id` from the request body or query to decide ownership. That's caller-supplied. The only authoritative seller is `req.seller_context.seller_id`. To scope a vendor list route to the caller's data, add the `filterBySellerId()` middleware and every query is constrained automatically, with no per-handler `where`:
246
247
 
247
- ```ts title="src/api/vendor/offers/middlewares.ts"
248
+ ```ts src/api/vendor/offers/middlewares.ts
248
249
  import { filterBySellerId } from "@mercurjs/core/..."
249
250
 
250
251
  {
@@ -262,7 +263,7 @@ export const POST = async (
262
263
 
263
264
  `validateAndTransformQuery` takes a query config that controls which `fields` are selectable, `isList`, and default pagination. The handler reads the resolved selection from `req.queryConfig.fields` and pagination from `req.queryConfig.pagination`.
264
265
 
265
- ```ts title="src/api/admin/brands/query-config.ts"
266
+ ```ts src/api/admin/brands/query-config.ts
266
267
  export const adminBrandQueryConfig = {
267
268
  list: {
268
269
  defaults: ["id", "name", "is_active", "created_at"],
@@ -275,14 +276,14 @@ export const adminBrandQueryConfig = {
275
276
  ```
276
277
 
277
278
  <Warning>
278
- **`fields` replaces defaults unless prefixed.** An unprefixed field in the request's `fields` param *replaces* the route's default set; prefix with `+`/`-` to merge (e.g. `+brand.name`) or base fields like `thumbnail` silently drop. This is the `medusa-fields-param` gotcha.
279
+ **`fields` replaces defaults unless prefixed.** An unprefixed field in the request's `fields` param *replaces* the route's default set. Prefix with `+`/`-` to merge (e.g. `+brand.name`), or base fields like `thumbnail` silently drop. This is the `medusa-fields-param` gotcha.
279
280
  </Warning>
280
281
 
281
282
  ## Response types
282
283
 
283
- Declare the response shapes next to the route (or in `@mercurjs/types` for shared ones) and use them as the `MedusaResponse` generic the SDK reads these to type `.query()` / `.mutate()` returns:
284
+ Declare the response shapes next to the route (or in `@mercurjs/types` for shared ones) and use them as the `MedusaResponse` generic. The SDK reads these to type `.query()` and `.mutate()` returns:
284
285
 
285
- ```ts title="src/api/admin/brands/types.ts"
286
+ ```ts src/api/admin/brands/types.ts
286
287
  import { PaginatedResponse } from "@medusajs/framework/types"
287
288
 
288
289
  export interface AdminBrandResponse {
@@ -296,11 +297,22 @@ export type AdminBrandListResponse = PaginatedResponse<{
296
297
 
297
298
  ## Checklist for a route
298
299
 
299
- - Handler is thin: validate run workflow (writes) or `query.graph` (reads) respond.
300
- - **Both** generics set: `AuthenticatedMedusaRequest<TBody|TQuery>` and `MedusaResponse<TResponse>` never a bare `MedusaResponse`.
301
- - Only `GET` / `POST` / `DELETE` exported; updates are `POST`.
302
- - Query params built with `createFindParams` / `createSelectParams`; bodies with Zod; inferred types exported.
303
- - Filterable fields declared in the validator; scoping injected via a `filterableFields` middleware, not inlined.
304
- - Identity read from `req.auth_context`, never the body. On vendor routes, the authoritative seller is `req.seller_context.seller_id` (set by `ensureSellerMiddleware`); scope reads with `filterBySellerId()`.
305
- - `fields` prefixed with `+`/`-` to merge; defaults declared in `queryConfig`.
300
+ - Handler is thin: validate, run a workflow (writes) or `query.graph` (reads), then respond.
301
+ - Both generics set: `AuthenticatedMedusaRequest<TBody|TQuery>` and `MedusaResponse<TResponse>`, never a bare `MedusaResponse`.
302
+ - Only `GET` / `POST` / `DELETE` exported. Updates are `POST`.
303
+ - Query params built with `createFindParams` / `createSelectParams`, bodies with Zod, and inferred types exported.
304
+ - Filterable fields declared in the validator. Scoping injected via a `filterableFields` middleware, not inlined.
305
+ - Identity read from `req.auth_context`, never the body. On vendor routes, the authoritative seller is `req.seller_context.seller_id` (set by `ensureSellerMiddleware`). Scope reads with `filterBySellerId()`.
306
+ - `fields` prefixed with `+`/`-` to merge. Defaults declared in `queryConfig`.
306
307
  - No mutations outside a workflow.
308
+
309
+ ## Next steps
310
+
311
+ <CardGroup cols={2}>
312
+ <Card title="Workflows" href="/rc/resources/best-practices/workflows">
313
+ Run business logic and the Query engine behind your routes.
314
+ </Card>
315
+ <Card title="Module links" href="/rc/resources/best-practices/module-links#filtering-by-a-linked-field--the-index-module">
316
+ Filter by a linked field with the Index Module and `query.index`.
317
+ </Card>
318
+ </CardGroup>
@@ -1,15 +1,20 @@
1
1
  ---
2
- title: "Custom fields"
3
- description: "The full loop — add a custom field in core, render it in the panels with defineCustomFieldsConfig, pull linked data with the link property, and type it end-to-end."
2
+ title: "How to Add a Custom Field"
3
+ sidebarTitle: "Add a Custom Field"
4
+ description: "Attach extra data to a built-in entity across the full stack: declare it in core, render it in the panels, pull in linked data, and type it end-to-end."
4
5
  ---
5
6
 
6
- Custom Fields attach extra data to an existing entity (a product, customer, order…) through configuration no hand-written model or migration. What makes them powerful is the **full loop across the stack**: you declare the field in core, render it in the panels with `defineCustomFieldsConfig`, optionally pull in [linked-module](/rc/resources/best-practices/module-links) data with the `link` property, and [type it end-to-end](/rc/resources/best-practices/types) so every SDK call carries it.
7
+ Custom fields attach extra data to a built-in entity such as a product, customer, or order through configuration, with no hand-written model or migration.
7
8
 
8
- This page is the best-practices view. For the full storage-side setup see [Custom Fields](/rc/resources/customization/custom-fields).
9
+ What makes them powerful is the full loop across the stack. You declare the field in core, render it in the panels with `defineCustomFieldsConfig`, optionally pull in [linked-module](/rc/resources/best-practices/module-links) data with the `link` property, and [type it end-to-end](/rc/resources/best-practices/types) so every SDK call carries it.
10
+
11
+ <Info>
12
+ This page is the best-practices view. For the full storage-side setup see [Custom Fields](/rc/resources/customization/custom-fields).
13
+ </Info>
9
14
 
10
15
  ## Reach for a custom field vs a module
11
16
 
12
- The decision is about the **shape and lifecycle** of the data, not its size.
17
+ The decision is about the shape and lifecycle of the data, not its size.
13
18
 
14
19
  <CardGroup cols={2}>
15
20
  <Card title="Use a custom field when…" icon="circle-check">
@@ -21,16 +26,16 @@ The decision is about the **shape and lifecycle** of the data, not its size.
21
26
  </CardGroup>
22
27
 
23
28
  <Warning>
24
- Custom Fields is strictly **one row per parent entity**. Forcing a one-to-many or stateful concept into it works until you need a second row or a state transition. If in doubt, model it as a [module](/rc/resources/best-practices/modules).
29
+ Custom Fields is strictly one row per parent entity. Forcing a one-to-many or stateful concept into it works until you need a second row or a state transition. If in doubt, model it as a [module](/rc/resources/best-practices/modules).
25
30
  </Warning>
26
31
 
27
32
  ## The full loop
28
33
 
29
34
  ### 1. Declare the field in core
30
35
 
31
- Register the Custom Fields module and describe the field in `medusa-config.ts`. The module generates the side table, the link, and the schema on `db:migrate`:
36
+ Register the Custom Fields module and describe the field in `medusa-config.ts`. The module generates the side table, the link, and the schema on `db:migrate`.
32
37
 
33
- ```ts title="medusa-config.ts"
38
+ ```ts medusa-config.ts
34
39
  {
35
40
  resolve: "@mercurjs/core/modules/custom-fields",
36
41
  options: {
@@ -43,21 +48,21 @@ Register the Custom Fields module and describe the field in `medusa-config.ts`.
43
48
  }
44
49
  ```
45
50
 
46
- The value now lives in the module's own `custom_fields` side table linked to the product, readable alongside it through `query.graph`, and written through `additional_data` on the entity's create/update route. (Filtering products *by* a custom-field value is cross-module and needs the [Index Module](/rc/resources/best-practices/module-links#filtering-by-a-linked-field--the-index-module), not `query.graph`.)
51
+ The value now lives in the module's own `custom_fields` side table. It is linked to the product, readable alongside it through `query.graph`, and written through `additional_data` on the entity's create/update route. Filtering products by a custom-field value is cross-module and needs the [Index Module](/rc/resources/best-practices/module-links#filtering-by-a-linked-field--the-index-module), not `query.graph`.
47
52
 
48
53
  <Warning>
49
- **Prefer the `custom_fields` link over stuffing values into `metadata`.** `metadata` is an untyped JSON bag with no schema, no queryable columns, and no clean extension point it turns into a dumping ground. The Custom Fields module gives you a real linked table (`custom_fields.*`) with typed columns you can read and (via the Index Module) filter on, while still being config-only. Reach for `metadata` only for genuinely throwaway, never-queried scratch data.
54
+ Prefer the `custom_fields` link over stuffing values into `metadata`. `metadata` is an untyped JSON bag with no schema, no queryable columns, and no clean extension point. It turns into a dumping ground. The Custom Fields module gives you a real linked table (`custom_fields.*`) with typed columns you can read and, via the Index Module, filter on, while still being config-only. Reach for `metadata` only for genuinely throwaway, never-queried scratch data.
50
55
  </Warning>
51
56
 
52
57
  <Tip>
53
- Mutations still go through workflows. The panel submits custom-field values under **`additional_data`** on the parent's create/update route and `additional_data` is exactly what [workflow hooks](/rc/resources/best-practices/workflows#hooks--let-others-extend-your-workflow) receive. So the same values you enter in the panel can be consumed by a `productsCreated` / `productUpdated` hook to run follow-up logic, persist to the linked table, or trigger side effects. Never write a custom-field value with a direct route write.
58
+ Mutations still go through workflows. The panel submits custom-field values under `additional_data` on the parent's create/update route, and `additional_data` is exactly what [workflow hooks](/rc/resources/best-practices/workflows#hooks--let-others-extend-your-workflow) receive. So the same values you enter in the panel can be consumed by a `productsCreated` / `productUpdated` hook to run follow-up logic, persist to the linked table, or trigger side effects. Never write a custom-field value with a direct route write.
54
59
  </Tip>
55
60
 
56
61
  ### 2. Render it in the panel with `defineCustomFieldsConfig`
57
62
 
58
- Drop one file per model under the panel's `src/custom-fields/`. A single config contributes **form fields** (edit drawer, submitted under `additional_data`), read-only **displays** (detail sections), and **list columns**. Declare `link: "custom_fields"` so the module's data is fetched alongside the product and available to your fields and displays:
63
+ Drop one file per model under the panel's `src/custom-fields/`. A single config contributes form fields (edit drawer, submitted under `additional_data`), read-only displays (detail sections), and list columns. Declare `link: "custom_fields"` so the module's data is fetched alongside the product and available to your fields and displays.
59
64
 
60
- ```tsx title="apps/vendor/src/custom-fields/product.tsx"
65
+ ```tsx apps/vendor/src/custom-fields/product.tsx
61
66
  import { defineCustomFieldsConfig } from "@mercurjs/dashboard-sdk"
62
67
  import { createFormHelper } from "@mercurjs/dashboard-shared"
63
68
 
@@ -67,7 +72,7 @@ const form = createFormHelper<ProductWithCustomFields>()
67
72
 
68
73
  export default defineCustomFieldsConfig({
69
74
  model: "product",
70
- link: "custom_fields", // fetch custom_fields.* with the product no hand-written field list
75
+ link: "custom_fields", // fetch custom_fields.* with the product, no hand-written field list
71
76
  forms: [
72
77
  {
73
78
  zone: "edit",
@@ -87,7 +92,7 @@ export default defineCustomFieldsConfig({
87
92
  fields: [
88
93
  {
89
94
  id: "is_featured",
90
- component: ({ data }) => (data.custom_fields?.is_featured ? "Featured" : ""),
95
+ component: ({ data }) => (data.custom_fields?.is_featured ? "Featured" : "-"),
91
96
  },
92
97
  ],
93
98
  },
@@ -96,23 +101,23 @@ export default defineCustomFieldsConfig({
96
101
  ```
97
102
 
98
103
  <Note>
99
- The `zone` values are typed the panel's codegen scans the host `<FormExtensionZone>` / `<DisplayExtensionZone>` usages and emits the valid zones per model into `extension-targets.d.ts`. `zone: "nope"` fails `tsc`. You don't hand-maintain that list.
104
+ The `zone` values are typed. The panel's codegen scans the host `<FormExtensionZone>` / `<DisplayExtensionZone>` usages and emits the valid zones per model into `extension-targets.d.ts`. `zone: "nope"` fails `tsc`. You don't hand-maintain that list.
100
105
  </Note>
101
106
 
102
107
  The `displays` fields follow an add / replace / remove convention keyed by `id`:
103
108
 
104
- - **unknown id** appends a new read-only row,
105
- - **built-in id + component** replaces that field's render,
106
- - **built-in id + `component: null`** hides the field.
109
+ - **Unknown id:** appends a new read-only row.
110
+ - **Built-in id with component:** replaces that field's render.
111
+ - **Built-in id with `component: null`:** hides the field.
107
112
 
108
113
  ## The extension API `link` property
109
114
 
110
- A custom-field config can also declare **module links to fetch alongside the entity** with the `link` property. This is how you surface data from a linked module (e.g. a `brand`) in the product's columns and displays without wiring a second query:
115
+ A custom-field config can also declare module links to fetch alongside the entity with the `link` property. This is how you surface data from a linked module such as a `brand` in the product's columns and displays without wiring a second query.
111
116
 
112
- ```tsx title="apps/vendor/src/custom-fields/product.tsx"
117
+ ```tsx apps/vendor/src/custom-fields/product.tsx
113
118
  export default defineCustomFieldsConfig({
114
119
  model: "product",
115
- link: "brand", // fetch brand.* with each product one or an array of links
120
+ link: "brand", // fetch brand.* with each product, one or an array of links
116
121
  list: {
117
122
  columns: [
118
123
  // linked data is available on the row, no extra fetch
@@ -129,18 +134,18 @@ export default defineCustomFieldsConfig({
129
134
  ```
130
135
 
131
136
  <Tip>
132
- `link` replaces the old "remember to add the fields to every fetch" chore. Under the hood the panel reads the registry's links (`getLinks(model)`) and merges them into the built-in list, detail, and edit fetches with `withLinkFields(fields, links)` (`+brand.*`) so the linked data is present in **all three** places automatically. There's no `extendFields`: declaring the `link` is what makes its fields available to both columns and displays.
137
+ `link` replaces the old "remember to add the fields to every fetch" chore. Under the hood the panel reads the registry's links (`getLinks(model)`) and merges them into the built-in list, detail, and edit fetches with `withLinkFields(fields, links)` (`+brand.*`), so the linked data is present in all three places automatically. There's no `extendFields`: declaring the `link` is what makes its fields available to both columns and displays.
133
138
  </Tip>
134
139
 
135
140
  <Warning>
136
- The link must actually exist as a [module link](/rc/resources/best-practices/module-links) and, in the vendor panel, respect the curated-field constraint the fetch derived from `link` runs against the vendor product query, which rejects arbitrary `*`-relation overrides. Declare the link, then reference only its real fields.
141
+ The link must actually exist as a [module link](/rc/resources/best-practices/module-links) and, in the vendor panel, respect the curated-field constraint. The fetch derived from `link` runs against the vendor product query, which rejects arbitrary `*`-relation overrides. Declare the link, then reference only its real fields.
137
142
  </Warning>
138
143
 
139
144
  ## 3. Type it end-to-end
140
145
 
141
- The rendered value comes back from the API, but the panel's `ProductDTO` doesn't know about `is_featured` yet. Close the gap with a one-line declaration-merging `.d.ts` so **every** SDK endpoint is typed no per-call casts:
146
+ The rendered value comes back from the API, but the panel's `ProductDTO` doesn't know about `is_featured` yet. Close the gap with a one-line declaration-merging `.d.ts` so every SDK endpoint is typed, with no per-call casts.
142
147
 
143
- ```ts title="apps/vendor/src/types/custom-fields.d.ts"
148
+ ```ts apps/vendor/src/types/custom-fields.d.ts
144
149
  import "@medusajs/types"
145
150
 
146
151
  declare module "@medusajs/types" {
@@ -150,92 +155,111 @@ declare module "@medusajs/types" {
150
155
  }
151
156
  ```
152
157
 
153
- Now `product.custom_fields?.is_featured` is typed on every `sdk.vendor.products.*` response the runtime value is delivered by the `link` / registry merge above, not a hand-added `+field.*` (the vendor product query rejects arbitrary `*`-relation overrides). The mechanics why merging into the upstream interface flows through are covered in [Types & augmentation](/rc/resources/best-practices/types#the-scenario-a-custom-field-typed-end-to-end).
158
+ Now `product.custom_fields?.is_featured` is typed on every `sdk.vendor.products.*` response. The runtime value is delivered by the `link` / registry merge above, not a hand-added `+field.*` (the vendor product query rejects arbitrary `*`-relation overrides). The mechanics, why merging into the upstream interface flows through, are covered in [Types & augmentation](/rc/resources/best-practices/types#the-scenario-a-custom-field-typed-end-to-end).
154
159
 
155
160
  ## The full override flow: `additional_data` → route → workflow hook
156
161
 
157
- Rendering and typing a field is only half the story. The reason custom fields submit under **`additional_data`** is that it's the framework's built-in extension channel: values entered in the panel travel through the entity's existing API route into the workflow's **hooks**, where your own code consumes them — **without forking the route or the workflow**. This is exactly what a Mercur override looks like.
162
+ Rendering and typing a field is only half the story. The reason custom fields submit under `additional_data` is that it's the framework's built-in extension channel: values entered in the panel travel through the entity's existing API route into the workflow's hooks, where your own code consumes them, without forking the route or the workflow. This is exactly what a Mercur override looks like.
158
163
 
159
164
  The flow has three links in the chain.
160
165
 
161
- ### 1. The panel submits under `additional_data`
162
-
163
- You already did this a `defineCustomFieldsConfig` `edit`/`create` field is submitted as `additional_data.<field>` on the entity's create/update request. Nothing else to wire on the frontend.
166
+ <Steps>
167
+ <Step title="The panel submits under additional_data">
168
+ You already did this. A `defineCustomFieldsConfig` `edit`/`create` field is submitted as `additional_data.<field>` on the entity's create/update request. Nothing else to wire on the frontend.
169
+ </Step>
164
170
 
165
- ### 2. The route accepts it via `additionalDataValidator`
171
+ <Step title="The route accepts it via additionalDataValidator">
172
+ The vendor/admin product routes accept an `additional_data` body param, but each key must be declared or it's rejected. Register the allowed keys with `additionalDataValidator` in a middleware, with no need to touch the route handler.
166
173
 
167
- The vendor/admin product routes accept an `additional_data` body param, but each key must be **declared** or it's rejected. Register the allowed keys with `additionalDataValidator` in a middleware — no need to touch the route handler:
174
+ ```ts src/api/middlewares.ts
175
+ import { defineMiddlewares } from "@medusajs/framework/http"
176
+ import { z } from "@medusajs/framework/zod"
168
177
 
169
- ```ts title="src/api/middlewares.ts"
170
- import { defineMiddlewares } from "@medusajs/framework/http"
171
- import { z } from "@medusajs/framework/zod"
172
-
173
- export default defineMiddlewares({
174
- routes: [
175
- {
176
- method: "POST",
177
- matcher: "/vendor/products",
178
- additionalDataValidator: {
179
- brand_id: z.string().optional(),
178
+ export default defineMiddlewares({
179
+ routes: [
180
+ {
181
+ method: "POST",
182
+ matcher: "/vendor/products",
183
+ additionalDataValidator: {
184
+ brand_id: z.string().optional(),
185
+ },
186
+ },
187
+ ],
188
+ })
189
+ ```
190
+ </Step>
191
+
192
+ <Step title="A workflow hook consumes it">
193
+ Mercur's product create workflow is `createProductsWorkflow` from `@mercurjs/core/workflows` (id `mercur-create-products`). It is what the vendor route runs, and it exposes a `productsCreated` hook that runs after the products are created, receiving both the created records and your `additional_data`. Consume it to perform the real work, here [linking](/rc/resources/best-practices/module-links) the product to a brand, with a compensation function so a failure rolls the link back.
194
+
195
+ ```ts src/workflows/hooks/created-product.ts
196
+ import { createProductsWorkflow } from "@mercurjs/core/workflows"
197
+ import { StepResponse } from "@medusajs/framework/workflows-sdk"
198
+ import { Modules } from "@medusajs/framework/utils"
199
+ import { LinkDefinition } from "@medusajs/framework/types"
200
+ import { BRAND_MODULE } from "../../modules/brand"
201
+
202
+ createProductsWorkflow.hooks.productsCreated(
203
+ async ({ products, additional_data }, { container }) => {
204
+ if (!additional_data?.brand_id) {
205
+ return new StepResponse([], [])
206
+ }
207
+
208
+ const link = container.resolve("link")
209
+ const links: LinkDefinition[] = products.map((product) => ({
210
+ [Modules.PRODUCT]: { product_id: product.id },
211
+ [BRAND_MODULE]: { brand_id: additional_data.brand_id },
212
+ }))
213
+
214
+ await link.create(links)
215
+ return new StepResponse(links, links)
180
216
  },
181
- },
182
- ],
183
- })
184
- ```
185
-
186
- ### 3. A workflow hook consumes it
187
-
188
- Mercur's product create workflow — `createProductsWorkflow` from `@mercurjs/core/workflows` (id `mercur-create-products`) — is what the vendor route runs, and it exposes a `productsCreated` **hook** that runs after the products are created, receiving both the created records and your `additional_data`. Consume it to perform the real work — here, [linking](/rc/resources/best-practices/module-links) the product to a brand — with a compensation function so a failure rolls the link back:
189
-
190
- ```ts title="src/workflows/hooks/created-product.ts"
191
- import { createProductsWorkflow } from "@mercurjs/core/workflows"
192
- import { StepResponse } from "@medusajs/framework/workflows-sdk"
193
- import { Modules } from "@medusajs/framework/utils"
194
- import { LinkDefinition } from "@medusajs/framework/types"
195
- import { BRAND_MODULE } from "../../modules/brand"
196
-
197
- createProductsWorkflow.hooks.productsCreated(
198
- async ({ products, additional_data }, { container }) => {
199
- if (!additional_data?.brand_id) {
200
- return new StepResponse([], [])
201
- }
202
-
203
- const link = container.resolve("link")
204
- const links: LinkDefinition[] = products.map((product) => ({
205
- [Modules.PRODUCT]: { product_id: product.id },
206
- [BRAND_MODULE]: { brand_id: additional_data.brand_id },
207
- }))
208
-
209
- await link.create(links)
210
- return new StepResponse(links, links)
211
- },
212
- // compensation — undo the links if a later step fails
213
- async (links, { container }) => {
214
- if (!links?.length) {
215
- return
216
- }
217
- await container.resolve("link").dismiss(links)
218
- }
219
- )
220
- ```
217
+ // compensation: undo the links if a later step fails
218
+ async (links, { container }) => {
219
+ if (!links?.length) {
220
+ return
221
+ }
222
+ await container.resolve("link").dismiss(links)
223
+ }
224
+ )
225
+ ```
226
+ </Step>
227
+ </Steps>
221
228
 
222
229
  <Note>
223
- Mercur's `createProductsWorkflow` wraps Medusa's stock create-products flow and adds the marketplace layer (seller association, attributes, audit trail). Because it re-exposes the `validate` and `productsCreated` hooks, you extend the **Mercur** flow the same way you would a plain Medusa one consume its hook, don't fork it.
230
+ Mercur's `createProductsWorkflow` wraps Medusa's stock create-products flow and adds the marketplace layer (seller association, attributes, audit trail). Because it re-exposes the `validate` and `productsCreated` hooks, you extend the Mercur flow the same way you would a plain Medusa one. Consume its hook, don't fork it.
224
231
  </Note>
225
232
 
226
233
  <Tip>
227
- This is the **override pattern** in one sentence: the panel writes to `additional_data`, the route lets it through via `additionalDataValidator`, and a `hooks.<name>` consumer turns it into real behaviour all additively, without copying or replacing any built-in code. It's how you extend a Mercur (or Medusa) flow instead of forking it. See [Workflows → hooks](/rc/resources/best-practices/workflows#hooks--let-others-extend-your-workflow).
234
+ This is the override pattern in one sentence: the panel writes to `additional_data`, the route lets it through via `additionalDataValidator`, and a `hooks.<name>` consumer turns it into real behaviour, all additively, without copying or replacing any built-in code. It's how you extend a Mercur (or Medusa) flow instead of forking it. See [Workflows → hooks](/rc/resources/best-practices/workflows#hooks--let-others-extend-your-workflow).
228
235
  </Tip>
229
236
 
230
237
  <Warning>
231
- The hook runs **inside** the workflow, so its mutation still obeys the [one-mutation-per-step + compensation](/rc/resources/best-practices/workflows#one-mutation-per-step--compensation) rule always pair `link.create` with a `link.dismiss` compensation. Never do the work in a route handler after the workflow returns; put it in the hook.
238
+ The hook runs inside the workflow, so its mutation still obeys the [one-mutation-per-step + compensation](/rc/resources/best-practices/workflows#one-mutation-per-step--compensation) rule. Always pair `link.create` with a `link.dismiss` compensation. Never do the work in a route handler after the workflow returns. Put it in the hook.
232
239
  </Warning>
233
240
 
234
241
  ## Checklist
235
242
 
236
- - Data is genuinely one-row-per-parent with no lifecycle custom field; otherwise a module.
237
- - Field registered in `medusa-config.ts`; `db:migrate` run; writes go through `additional_data` on a workflow.
238
- - Panel: one `defineCustomFieldsConfig` per model contributes forms / displays / list, with typed `zone`s.
239
- - Linked-module data pulled in with the `link` property (not a hand-written second fetch); the link exists and respects vendor field constraints.
243
+ - Data is genuinely one row per parent with no lifecycle: use a custom field, otherwise a module.
244
+ - Field registered in `medusa-config.ts`, `db:migrate` run, writes go through `additional_data` on a workflow.
245
+ - Panel: one `defineCustomFieldsConfig` per model contributes forms, displays, and list, with typed `zone`s.
246
+ - Linked-module data pulled in with the `link` property (not a hand-written second fetch), the link exists and respects vendor field constraints.
240
247
  - Extended fields typed once via a `.d.ts` merging into the framework DTO, and requested with `+…*` so they arrive.
241
- - The override chain is complete: panel `additional_data` `additionalDataValidator` declares the key a `hooks.<name>` consumer does the work inside the workflow, with compensation. No route/workflow forked.
248
+ - The override chain is complete: panel, then `additional_data`, then `additionalDataValidator` declares the key, then a `hooks.<name>` consumer does the work inside the workflow, with compensation. No route or workflow forked.
249
+
250
+ ## Next steps
251
+
252
+ <CardGroup cols={2}>
253
+ <Card title="Module links" href="/rc/resources/best-practices/module-links">
254
+ Relate modules and fetch linked data alongside an entity.
255
+ </Card>
256
+ <Card title="Modules" href="/rc/resources/best-practices/modules">
257
+ Model data that has its own lifecycle, rows, and routes.
258
+ </Card>
259
+ <Card title="Workflows & hooks" href="/rc/resources/best-practices/workflows#hooks--let-others-extend-your-workflow">
260
+ Extend a built-in flow through its hooks instead of forking it.
261
+ </Card>
262
+ <Card title="Types & augmentation" href="/rc/resources/best-practices/types#the-scenario-a-custom-field-typed-end-to-end">
263
+ Type a custom field end-to-end with declaration merging.
264
+ </Card>
265
+ </CardGroup>