Skill 14 · Building With Medusa
Subchapter 14.7
reference/module-links.mdMarkdown11 KBView on GitHub
Module links create associations between data models in different modules while maintaining module isolation. Use links to connect your custom models to Commerce Module models (products, customers, orders, etc.).
IMPORTANT FOR CLAUDE CODE: When implementing module links, use the TodoWrite tool to track your progress through these steps. This ensures you don’t miss any critical steps and provides visibility to the user.
Create these tasks in your todo list:
Add the ID of a linked data model in the custom data model if the custom data model belongs to it or extends it. Otherwise, skip this step.
For example, add ID of customer and product to custom product review model:
import { model } from "@medusajs/framework/utils"
const Review = model.define("review", {
// other properties...
// ID of linked customer
customer_id: model.text(),
// ID of linked product
product_id: model.text()
})
export default Review⚠️ CRITICAL RULE: Create ONE link definition per file. Do NOT export an array of links from a single file.
Create link files in src/links/:
// ✅ CORRECT - src/links/product-brand.ts (one link per file)
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import BrandModule from "../modules/brand"
export default defineLink(
ProductModule.linkable.product,
BrandModule.linkable.brand
)If one model links to multiple others, create multiple files:
// ✅ CORRECT - src/links/review-product.ts
export default defineLink(
ReviewModule.linkable.review,
ProductModule.linkable.product
)
// ✅ CORRECT - src/links/review-customer.ts
export default defineLink(
ReviewModule.linkable.review,
CustomerModule.linkable.customer
)
// ❌ WRONG - Don't export array of links from one file
export default [
defineLink(ReviewModule.linkable.review, ProductModule.linkable.product),
defineLink(ReviewModule.linkable.review, CustomerModule.linkable.customer),
] // This doesn't work!IMPORTANT: The .linkable property is automatically added to all modules by Medusa. You do NOT need to add .linkable() or any linkable definition to your data models. Simply use ModuleName.linkable.modelName when defining links.
For example, if you have a Review data model in a ReviewModule:
ReviewModule.linkable.review (works automatically).linkable() method to the Review model definition (not needed, causes errors)⚠️ NEXT STEP: After defining a link, you MUST immediately proceed to Step 3 to run migrations (npx medusa db:migrate). Do not skip this step!
Allow multiple records to link to one record:
// A brand can have many products
export default defineLink(
{
linkable: ProductModule.linkable.product,
isList: true,
},
BrandModule.linkable.brand
)Automatically delete links when a record is deleted:
export default defineLink(ProductModule.linkable.product, {
linkable: BrandModule.linkable.brand,
deleteCascade: true,
})⚠️ CRITICAL - DO NOT SKIP: After defining links, you MUST run migrations to sync the link to the database. Without this step, the link will not work and you will get runtime errors.
npx medusa db:migrateWhy this matters:
Common mistake: Defining a link in src/links/ and immediately trying to use it in a workflow or query without running migrations first. Always run migrations immediately after defining a link.
⚠️ CRITICAL - Link Order (Direction): When creating or dismissing links, the order of modules MUST match the order in defineLink(). Mismatched order causes runtime errors.
// Example link definition: product FIRST, then brand
export default defineLink(
ProductModule.linkable.product,
BrandModule.linkable.brand
)To create a link between records in workflow composition functions, use the createRemoteLinkStep:
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import {
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
const BRAND_MODULE = "brand"
export const myWorkflow = createWorkflow(
"my-workflow",
function (input) {
// ...
// ✅ CORRECT - Order matches defineLink (product first, then brand)
const linkData = transform({ input }, ({ input }) => {
return [
{
[Modules.PRODUCT]: {
product_id: input.product_id,
},
[BRAND_MODULE]: {
brand_id: input.brand_id,
},
},
]
})
createRemoteLinkStep(linkData)
// ...
}
)
// ❌ WRONG - Order doesn't match defineLink
const linkData = transform({ input }, ({ input }) => {
return [
{
[BRAND_MODULE]: {
brand_id: input.brand_id,
},
[Modules.PRODUCT]: {
product_id: input.product_id,
},
},
]
}) // Runtime error: link direction mismatch!To dismiss (remove) a link between records in workflow composition functions, use the dismissRemoteLinkStep:
import { Modules } from "@medusajs/framework/utils"
import { dismissRemoteLinkStep } from "@medusajs/medusa/core-flows"
import {
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
const BRAND_MODULE = "brand"
export const myWorkflow = createWorkflow(
"my-workflow",
function (input) {
// ...
// Order MUST match defineLink (product first, then brand)
const linkData = transform({ input }, ({ input }) => {
return [
{
[Modules.PRODUCT]: {
product_id: input.product_id,
},
[BRAND_MODULE]: {
brand_id: input.brand_id,
},
},
]
})
dismissRemoteLinkStep(linkData)
// ...
}
)Outside workflows or in workflow steps, use the link utility to create and manage links between records. Order MUST match defineLink() here too:
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
// In an API route or workflow step
const link = container.resolve(ContainerRegistrationKeys.LINK)
const BRAND_MODULE = "brand"
// ✅ CORRECT - Create a link (order matches defineLink: product first, then brand)
await link.create({
[Modules.PRODUCT]: { product_id: "prod_123" },
[BRAND_MODULE]: { brand_id: "brand_456" },
})
// ✅ CORRECT - Dismiss (remove) a link (same order: product first, then brand)
await link.dismiss({
[Modules.PRODUCT]: { product_id: "prod_123" },
[BRAND_MODULE]: { brand_id: "brand_456" },
})
// ❌ WRONG - Order doesn't match defineLink
await link.create({
[BRAND_MODULE]: { brand_id: "brand_456" },
[Modules.PRODUCT]: { product_id: "prod_123" },
}) // Runtime error: link direction mismatch!Use query.graph() to fetch data across linked modules. Note: query.graph() can retrieve linked data but cannot filter by properties of linked modules (data models in separate modules).
const query = container.resolve("query")
// ✅ Get products with their linked brands (no cross-module filtering)
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "title", "brand.*"], // brand.* fetches linked brand data
filters: {
id: "prod_123", // ✅ Filter by product properties only
},
})
// ✅ Get brands with their linked products
const { data: brands } = await query.graph({
entity: "brand",
fields: ["id", "name", "products.*"],
})
// ❌ DOES NOT WORK: Cannot filter products by linked brand properties
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "title", "brand.*"],
filters: {
brand: {
name: "Nike" // ❌ Fails: brand is in a different module
}
}
})To filter by properties of linked modules (separate modules with module links), use query.index() from the Index Module:
const query = container.resolve("query")
// ✅ Filter products by linked brand name using Index Module
const { data: products } = await query.index({
entity: "product",
fields: ["*", "brand.*"],
filters: {
brand: {
name: "Nike" // ✅ Works with Index Module!
}
}
})Key Distinction:
query.graph() - filtering works ✅query.index() for filtering ✅Index Module Requirements:
@medusajs/index packagemedusa-config.tsMEDUSA_FF_INDEX_ENGINE=true in .envnpx medusa db:migratefilterable in link definition:// src/links/product-brand.ts
defineLink(
{ linkable: ProductModule.linkable.product, isList: true },
{ linkable: BrandModule.linkable.brand, filterable: ["id", "name"] }
)See the Querying Data reference for complete details on both methods.
Add extra data to the link table:
export default defineLink(
ProductModule.linkable.product,
BrandModule.linkable.brand,
{
database: {
extraColumns: {
featured: {
type: "boolean",
defaultValue: "false",
},
},
},
}
)Set custom column values when creating links:
await link.create({
product: { product_id: "prod_123" },
brand: { brand_id: "brand_456" },
data: { featured: true },
})