Files
2026-03-07 11:07:45 -03:00

11 KiB

Module Links

Contents

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.).

  • Extend commerce entities: Add brands to products, wishlists to customers
  • Cross-module associations: Connect custom modules to each other
  • Maintain isolation: Keep modules independent and reusable

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:

  • Optional: Add linked ID in custom data model (if one-to-one or one-to-many)
  • Define the link in src/links/
  • Configure list or delete cascade options if needed
  • CRITICAL: Run migrations: npx medusa db:migrate (Never skip this step!)
  • Create links in code using link.create() or createRemoteLinkStep
  • Query linked data using query.graph()
  • CRITICAL: Run build to validate implementation (catches type errors and issues)

Optional: Add Linked ID in Custom Data Model

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:

  • CORRECT: ReviewModule.linkable.review (works automatically)
  • WRONG: Adding .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
)

Delete Cascades

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:migrate

Why this matters:

  • Links create database tables that store the relationships between modules
  • Without migrations, these tables don't exist and link operations will fail
  • This step is REQUIRED before creating any links in code or querying linked data

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
)

In Workflow Composition Functions

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

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!

Step 5: Querying Linked Data

Using query.graph() - Retrieve Linked Data

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
    }
  }
})

Using query.index() - Filter Across Linked Modules

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:

  • Same module relations (e.g., Product → ProductVariant): Use query.graph() - filtering works
  • Different module links (e.g., Product → Brand): Use query.index() for filtering

Index Module Requirements:

  1. Install @medusajs/index package
  2. Add to medusa-config.ts
  3. Enable MEDUSA_FF_INDEX_ENGINE=true in .env
  4. Run npx medusa db:migrate
  5. Mark properties as filterable 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 },
})