---
title: "Announcing Preview of Global Product Options in Medusa"
description: "Medusa's preview release adds global product options, letting admin users create reusable options across multiple products. Learn how to install and try it."
url: https://medusajs.com/blog/announcing-global-product-options-in-medusa
last_updated: 2026-08-13T14:26:00Z
---

# Announcing Preview of Global Product Options in Medusa

_Published 2026-06-04 by Shahed  Nasser in Product_

We're releasing global product options as a preview feature in Medusa. Until now, product options like Size or Color were scoped to individual products. This release lets admin users define product options once and reuse them across any number of products.

We're shipping this as a preview to collect feedback before it becomes stable. If you run into issues or have suggestions, please open a [GitHub issue](https://github.com/medusajs/medusa/issues).

## What global product options change

Previously, each product carried its own isolated set of options. If you had 200 products that all used the same Size option, each one held its own copy. There was no way to standardize values or manage them from a single place.

With global product options, you can:

- Create a product option once from the admin dashboard
- Assign that option to multiple products
- Manage option values centrally, without editing each product individually
- Mix global options with product-specific ones as needed
- Filter products by their options in the storefront

## How to install the preview

The feature is available on the `options-preview` dist-tag. Follow these steps to enable it:

**1\. Update all `@medusajs/*` packages to the preview version**

In your `package.json`, set every `@medusajs/*` dependency to `preview`:

```json
{
  "dependencies": {
    "@medusajs/admin-sdk": "options-preview",
    "@medusajs/admin-shared": "options-preview",
    "@medusajs/caching": "options-preview",
    "@medusajs/cli": "options-preview",
    "@medusajs/dashboard": "options-preview",
    "@medusajs/draft-order": "options-preview",
    "@medusajs/framework": "options-preview",
    "@medusajs/medusa": "options-preview",
  }
}

```

We also recommend removing your `node_modules` and lock files to ensure the correct versions are installed across all Medusa packages.

Then run your package manager's install command to pull the updated packages.

**2\. Run database migrations**

> Your original data will not be impacted by running these migrations. However, you may not be able to revert this change if you choose to use the latest version before this feature is out. Proceed with caution.

```bash
yarn medusa db:migrate

```

Or the equivalent for your package manager (`npx medusa db:migrate`, `pnpm medusa db:migrate`).

This will update your database tables and previously created options to align with the new schema introduced by this release.

**3\. Update your storefront and backend usages (optional)**

If your storefront or custom backend code references product options, you can use the following prompt with an AI coding assistant like Claude Code to update usages to support global product options:

```text
## Context

You are upgrading a Medusa project (backend + storefront) to a Medusa version that introduces **global product options**.

**What changed in Medusa core (PR #13817):**

- Previously, `product_option` records were *exclusive* to a single product. Each product defined its own `Size`, `Color`, etc., and options could not be shared.
- Now, the relationship is many-to-many through two new pivot tables:
  - `product_product_option` — links options to products.
  - `product_product_option_value` — optionally restricts a product to a subset of an option's values.
- `ProductOptionDTO` gained an `is_exclusive: boolean` field. Options created via the legacy embedded-in-product path are `is_exclusive: true`; options created standalone are `is_exclusive: false` and reusable.
- `CreateProductDTO.options` now accepts either:
  1. an inline `CreateProductOptionDTO` (legacy — creates a new exclusive option), or
  2. `{ id: string; value_ids?: string[] }` to link an existing global option (optionally restricting which values are linked).
- The store API accepts `option_value_id` as a list filter on `/store/products` to filter by selected option values.
- New core-flows in `@medusajs/medusa/core-flows`:
  - `createProductOptionsWorkflow` — creates global options.
  - `createAndLinkProductOptionsToProductWorkflow` — add/remove/update options on an existing product.
  - `setProductProductOptionsWorkflow` — low-level set/replace.

Your job is to update **both the backend and storefront** of the target project so they keep working with the new model and (on the storefront) expose the new filtering capability.

For a worked example of these changes applied to a real project, see [medusajs/dtc-starter#22](https://github.com/medusajs/dtc-starter/pull/22).

## Ground rules

- Do not assume the project is the Next.js starter. The storefront may be Next.js, Remix, SvelteKit, Astro, plain React, or something else. Adapt the patterns described below to whatever stack you find.
- Do not assume the catalog. The seed script may have a single product or dozens; products may have different option combinations.
- Investigate before editing. Read the relevant files, understand current conventions (path aliases, styling system, data-fetching helpers), and match them.
- Run typecheck/lint on each app after changes and resolve issues you introduce.
- Do **not** add new dependencies if the existing ones suffice (e.g., reuse the project's existing `clsx`/`classnames` helper, headless UI primitives, fetcher) — only add a dep if there is no alternative.

## Backend tasks

1. **Locate seed scripts and any custom workflows that create products with inline options.**
   - Common locations: `src/scripts/seed*.ts`, `src/migration-scripts/*.ts`, `src/workflows/**/*.ts`.
   - Grep for `createProductsWorkflow(` and `options: [` to find call sites.

2. **For each product creation site that defines inline options:**
   - Extract the union of options/values used across all products into one or more **global** option creations using `createProductOptionsWorkflow` from `@medusajs/medusa/core-flows`.
     - Example shape:

       ```ts
       const { result: productOptions } = await createProductOptionsWorkflow(container).run({
         input: {
           product_options: [
             { title: "Size",  values: ["S", "M", "L", "XL"] },
             { title: "Color", values: ["Black", "White"]     },
           ],
         },
       })
       ```

   - Build a lookup (e.g. `productOptions.find(o => o.title === "Size")!`) for the IDs you'll reference.
   - Replace the inline `options: [{ title, values }]` on each product with `options: [{ id: <optionId> }]` (or include multiple option IDs for products that use multiple).
   - If a given product should only carry a subset of an option's values, pass `{ id, value_ids: [<value_id>, ...] }` — the value IDs are in the created option's `values[]` array.
   - **Leave the variants' `options: { Size: "S", ... }` mapping unchanged** — the title-keyed mapping still works.

3. **If the project has custom flows that manage options on existing products,** prefer the new workflows over direct module-service calls:
   - Use `createAndLinkProductOptionsToProductWorkflow` for add/remove/update operations from your own code or admin extensions.

4. **Verify**: build the backend (`yarn build`/`pnpm build` or `medusa build`), re-run the seed, and confirm products appear in admin with the global options attached. Optionally inspect the DB: rows should exist in `product_product_option` and `product_product_option_value`.

## Storefront tasks

These are *patterns*, not exact files. Map them onto the framework in use.

1. **Update the product-list data fetcher** (wherever the storefront calls `/store/products`):
   - Add `*variants.options` to the requested `fields` so each variant carries its option-value associations (needed if you ever want to highlight which variant a selected value belongs to).
   - Extend the query-params type to allow `option_value_id?: string | string[]`.
   - Accept an `optionValueIds?: string[]` parameter on the higher-level "listAndSort" helper; when provided and non-empty, include `option_value_id` in the request. Deduplicate the value IDs.

2. **Add a small "option-value query param" utility:**
   - Constant: `OPTION_VALUE_QUERY_KEY = "optionValueIds"`.
   - `parseOptionValueIds(input)` that accepts either a `URLSearchParams` (client) or a record-shaped server-side `searchParams` (Next.js App Router, Remix loader data, etc.) and returns a deduped `string[]`. Support comma-separated string fallback for shared links.

3. **Build an `OptionsPicker` UI component** (sidebar on the global store/product-list route only):
   - Fetch options from `/store/product-options?is_exclusive=false&fields=*values` using the project's existing fetch helper or the JS SDK. Filter to non-exclusive (global) options so per-product legacy options don't pollute the picker.
   - Render each option as a collapsible group of toggleable chips (one chip per value). Selected values toggle in/out of the URL query string under `OPTION_VALUE_QUERY_KEY`.
   - Reuse the project's existing accordion / chip / button primitives. Match the project's classname helper (`clsx`, `classnames`, `cn`, etc.) and design tokens.

4. **Wire the picker into the refinement / filter sidebar:**
   - When updating filter params, **also delete `page`** so the user lands on page 1 of the filtered set.
   - Skip the router push if the resulting URL is unchanged (avoids redundant navigations).
   - Add a `hideOptionsPicker` boolean prop (or equivalent) so screens that already imply an option scope (category, collection) can hide the picker. **Default: show on the global "store/all-products" route, hide on category and collection routes.**

5. **Thread `optionValueIds` from route to template to data call:**
   - Route/loader: parse from `searchParams`/URL using `parseOptionValueIds`.
   - Pass through the template down to the paginated product list component, which passes it into the data fetcher.

6. **Pagination / count caveat:**
   - When `option_value_id` is applied, the API's `count` may not reflect the filtered set in older client logic that pre-fetches a page of 100 and then sorts client-side. If the project uses such a pattern, recompute the "filtered count" from the returned array length so pagination doesn't show ghost pages.

7. **Verify**: typecheck the storefront. Then run the dev server and confirm:
   - The picker renders on the all-products page and not on category/collection pages.
   - Selecting a value updates the URL with `optionValueIds=...`, resets `page`, and reloads a filtered list.
   - Combining picker + sort works.
   - Deep-linking to a URL with `optionValueIds` reproduces the filter on first load.

## Reporting

When done, write a short summary (one section per app) covering:

- Files added/changed.
- Any deviations from the patterns above (and why — e.g., the project lacks `@medusajs/ui`, so primitives were swapped).
- Any new dependencies (avoid these if possible).
- Verification you performed (typecheck output, smoke test results)
```

## Stability and feedback

This is a preview release. The APIs and data model may change before the feature reaches stable. We don't recommend running it in production yet.

If something doesn't work as expected or the behavior isn't what you'd want, let us know by opening a [GitHub issue](https://github.com/medusajs/medusa/issues). Direct feedback at this stage shapes what ships as stable.

## Get started

Update your packages to `options-preview`, run migrations, and give it a try. We're looking forward to hearing what you think.
