Enforce Sales Channel Availability in Carts

In this guide, you'll learn how to reject product variants whose product isn't available in the cart's sales channel.

Why Medusa Doesn't Enforce This by Default#

A product's availability in a sales channel is a merchandising filter, not an authorization boundary.

Medusa applies it when you retrieve products. For example, the List Products Store API route only returns the products available in the sales channels of the publishable API key in the request. So, a product that you remove from a sales channel disappears from that channel's storefront.

Medusa doesn't apply that filter when a variant is added to a cart or when a cart is completed. A customer who knows a variant's ID can still add it to a cart scoped to a sales channel that the variant's product isn't available in.

Warning: Medusa doesn't enforce this by default because merchants use sales channels for merchandising, and rejecting items would break carts whenever a merchant changes a product's availability. If you rely on sales channels for legal or contractual restrictions, such as licensing agreements that forbid selling a product in a country, add the validation shown in this guide.

The inventory check that does run when you add an item to a cart, which the confirmVariantInventoryWorkflow performs, only checks the quantity available at the stock locations of the cart's sales channel. It doesn't check whether the variant's product is linked to that sales channel.

To enforce the availability, consume the validate hook of the workflows that add items to a cart and complete a cart. A workflow hook is a point in a workflow where you can inject custom functionality as a step function. If the step function throws an error, the workflow stops and the API route returns the error.

Note: This guide doesn't cover draft orders that admin users create in the dashboard, as it's assumed that merchants don't need to enforce sales channel availability for them.

Step 1: Create the Validation Function#

Start by creating a function that finds the variants that aren't available in a sales channel. You'll reuse it in both hooks.

Create the file src/utils/sales-channel-availability.ts with the following content:

src/utils/sales-channel-availability.ts
1import { MedusaContainer } from "@medusajs/framework/types"2
3export async function getUnavailableVariantIds({4  container,5  variantIds,6  salesChannelId,7}: {8  container: MedusaContainer9  variantIds: string[]10  salesChannelId: string11}) {12  const query = container.resolve("query")13
14  const { data: variants } = await query.graph({15    entity: "variant",16    fields: ["id", "product.sales_channels.id"],17    filters: {18      id: variantIds,19    },20  })21
22  return variants23    .filter((variant) => {24      const salesChannels =25        variant.product?.sales_channels ?? []26
27      return !salesChannels.some(28        (salesChannel) =>29          salesChannel?.id === salesChannelId30      )31    })32    .map((variant) => variant.id)33}

The function uses Query to retrieve the variants with the sales channels of their product, then returns the IDs of the variants whose product isn't linked to the specified sales channel.

A product without any sales channels isn't available in any of them, so the function considers its variants unavailable.


Step 2: Validate Items Added to the Cart#

Next, consume the validate hook of the addToCartWorkflow, which the Add Line Item Store API route executes.

Create the file src/workflows/hooks/validate-add-to-cart.ts with the following content:

src/workflows/hooks/validate-add-to-cart.ts
1import { MedusaError } from "@medusajs/framework/utils"2import { addToCartWorkflow } from "@medusajs/medusa/core-flows"3import {4  getUnavailableVariantIds,5} from "../../utils/sales-channel-availability"6
7addToCartWorkflow.hooks.validate(8  async ({ input, cart }, { container }) => {9    if (!cart.sales_channel_id) {10      return11    }12
13    const variantIds = (input.items ?? [])14      .map((item) => item.variant_id)15      .filter(Boolean) as string[]16
17    if (!variantIds.length) {18      return19    }20
21    const unavailableIds = await getUnavailableVariantIds({22      container,23      variantIds,24      salesChannelId: cart.sales_channel_id,25    })26
27    if (unavailableIds.length) {28      throw new MedusaError(29        MedusaError.Types.NOT_ALLOWED,30        `The variants ${unavailableIds.join(", ")} aren't ` +31          `available in the cart's sales channel.`32      )33    }34  }35)

The hook receives the cart and the input passed to the workflow, which holds the items to add. You retrieve the IDs of the variants that aren't available in the cart's sales channel, then throw a MedusaError if there are any. The Add Line Item API route then returns a response with the 400 status code.

Note: A cart isn't always associated with a sales channel. The example returns early in that case, since there's nothing to validate against.

Step 3: Validate the Cart on Completion#

A merchant can change a product's availability after a customer adds it to their cart. So, also consume the validate hook of the completeCartWorkflow to check the cart's items before Medusa creates the order.

Create the file src/workflows/hooks/validate-complete-cart.ts with the following content:

src/workflows/hooks/validate-complete-cart.ts
1import { MedusaError } from "@medusajs/framework/utils"2import {3  completeCartWorkflow,4} from "@medusajs/medusa/core-flows"5import {6  getUnavailableVariantIds,7} from "../../utils/sales-channel-availability"8
9completeCartWorkflow.hooks.validate(10  async ({ cart }, { container }) => {11    if (!cart.sales_channel_id) {12      return13    }14
15    const variantIds = (cart.items ?? [])16      .map((item) => item.variant_id)17      .filter(Boolean) as string[]18
19    if (!variantIds.length) {20      return21    }22
23    const unavailableIds = await getUnavailableVariantIds({24      container,25      variantIds,26      salesChannelId: cart.sales_channel_id,27    })28
29    if (unavailableIds.length) {30      throw new MedusaError(31        MedusaError.Types.NOT_ALLOWED,32        `The variants ${unavailableIds.join(", ")} are no ` +33          `longer available in the cart's sales channel.`34      )35    }36  }37)

This stops the cart completion for carts that a customer created before the merchant changed a product's availability.


Test it Out#

To test out the validation:

  1. Start the Medusa application.
  2. In the Medusa Admin dashboard, add a product that doesn't belong to a sales channel, or remove a product from a sales channel, as explained in the user guide.
  3. Create a cart associated with that sales channel using a publishable API key linked to it.
  4. Send a request to the Add Line Item Store API route with a variant of the product you removed.

The request returns a response with the 400 status code and the error message you specified.

Was this page helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Medusa MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break