Highlights
This release brings security improvements to the Store API, specifically related to what relations and fields can be retrieved through the routes. We recommend all Medusa stores update to this version.
Below you'll find the details of the change and an upgrade guide to apply it to older versions of Medusa, if you are unable to upgrade to the latest. We usually don't offer patches to older versions, but we deem this change a great hardening of Medusa stores and we want all of our users to benefit from it. Upgrading should be seamless, but feel free to reach out in case you have issues with this.
Strict Allowed Fields and Relations on Store API Routes
🚧 Breaking change
Every Store API route now declares an allowed list, and that list is enforced strictly: a requested field or relation is returned only if its exact path appears in the list. This means that the data exposed in Store API routes are explicitly defined, preventing unwanted leaks of data from expanding deep, internal relations.
The disallowed list that was added in the previous release has been effectively removed with this change, since the allowed guard is sufficient to block fields not in the list from being retrieved.
Any field outside a route's allowed list is silently stripped from the response. If your storefront requests fields that are no longer allowed, add them back with the allowFields middleware:
import { defineMiddlewares } from "@medusajs/medusa"
import { allowFields } from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/store/products",
middlewares: [allowFields("brand", "brand.name")],
},
],
})The middleware that adds to
req.allowedmust not specify amethodormethodskey. A route-scoped middleware runs after the core query validation and has no effect.
For the full list of allowed fields and relations for every Store route, check the PR #16702.
We also recommend setting the allowed query configuration for your custom API routes as well for added security. You can set it with the validateAndTransformQuery middleware:
import {
validateAndTransformQuery,
defineMiddlewares,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export const GetCustomSchema = createFindParams()
export default defineMiddlewares({
routes: [
{
matcher: "/customs",
method: "GET",
middlewares: [
validateAndTransformQuery(
GetCustomSchema,
{
defaults: [
"id",
"title",
"products.id",
"products.title",
],
isList: true,
allowed: [
"id",
"title",
"products.id",
"products.title",
// other fields that aren't retrieved by default but can be retrieved
"brand.id"
]
}
),
],
},
],
})Update with Patches
If you're on an old Medusa version and can't update to the latest version immediately, we have provided the following patches for different Medusa versions:
If you're using an older version, you can ask your AI agent to repurpose the patches to your version.
Each range ships patches for @medusajs/framework, @medusajs/medusa, and @medusajs/loyalty-plugin (applies only if the plugin is installed).
It also includes a SKILL.md prompt that you can pass to your agent to apply the patch and make any necessary changes in your store:
Follow the instructions in 2.20.1/SKILL.mdUpdate with MCP Server
Medusa MCP users can update their project using the following prompt:
Update my Medusa project to v2.21.0Update Prompt
Use the following prompt to update your Medusa project to v2.21.0 and make the necessary changes:
Update Prompt
<role>
You are a Medusa upgrade engineer working inside a user's Medusa project. You reason like someone who upgrades production commerce backends: you bump versions safely, apply required breaking-change migrations, verify the build and database, and never adopt optional features the user did not ask for. You treat every field you re-expose on a public Store API route as a security decision. You make no change the user has not approved.
</role>
<task>
Investigate this project and produce a migration plan to upgrade it from its current Medusa version to v2.21.0. The dominant breaking change in this release is the **strict allowed-fields list on every Store API route**: audit every Store API consumer in the workspace for \`fields\` selections that the new allowlist drops, scope the minimum set of fields that must be added back, and re-expose exactly those with the \`allowFields\` middleware. Present the plan for the user's approval before making any edits.
</task>
<context>
v2.21.0 is a minor release whose headline breaking change is a security hardening of the Store API query layer. This prompt covers the required upgrade steps and that breaking change in depth — do not adopt optional/additive features in this release.
## 1. Package version bump to v2.21.0
Bump every \`@medusajs/*\` package to its v2.21.0-aligned version. Packages that do not follow the \`2.x\` line (notably \`@medusajs/ui\`) must be resolved to the version co-released with 2.21.0 rather than forced to \`2.21.0\`. Verify every resolved version against the npm registry or the release notes before writing it; do not invent version numbers.
## 2. Every Store API route now enforces a strict \`allowed\` field list
Store API routes accept a \`fields\` query parameter. Before 2.21.0, only a handful of routes constrained it, and the \`allowed\` list they used was a *prefix* check: listing \`region\` also granted \`region.id\`, \`region.orders\`, \`region.orders.customer.email\`, and everything nested below. A client could walk from a public resource into private data.
In 2.21.0:
- **\`allowed\` matching is strict.** A requested field is permitted only if its full, normalized dotted path appears in the list as-is. \`region\` allows \`region\` alone; \`region.id\` now needs \`region.id\` listed separately. Selecting a relation in full (\`*region\`) grants nothing nested under it.
- **Every core Store route now ships an \`allowed\` list**, built from that route's default fields plus a small set of explicitly vetted extras. Routes covered include carts, collections, currencies, customers, orders, payment-collections, payment-providers, product-categories, product-options, product-tags, product-types, product-variants, products, products/search, regions, return-reasons, returns, and shipping-options. Do not trust this list from memory — enumerate it from the installed package (script in \`<steps>\`).
- **The Store denylist is gone.** The \`disallowed\` lists that core Store routes used to carry have been replaced by the allowlist, which is now the only field boundary those routes have. Anything not listed is unreachable; anything added back is reachable with nothing behind it as a second net.
- **Admin routes are unaffected** by this change, and so are custom routes outside the \`/store\` prefix. A custom Store route the project defines is only affected if its own \`validateAndTransformQuery\` query config sets \`allowed\`.
### The failure mode is silent
A field outside \`allowed\` is **stripped from the query before it runs**. The request still returns \`200\`; the field is simply missing from the response. There is no \`400\`, no error message, and nothing in the backend logs. A storefront that relied on the field renders \`undefined\`, an empty list, or crashes downstream at the point of use — far from the request that caused it. This is why the audit step is mandatory: nothing else will surface the breakage.
The one loud failure is sorting: if \`order\` names a field outside the route's \`allowed\` list, the request throws \`Order field {field} is not valid\`. Audit \`order\` / \`sort\` parameters alongside \`fields\`.
### How a requested field is normalized before the check
\`fields\` is a comma-separated list. Each entry is normalized, then checked for exact membership in the route's \`allowed\` set:
| Written by the client | Meaning | Normalizes to |
| --- | --- | --- |
| \`title\` | select \`title\`, and replace the route defaults | \`title\` |
| \`+title\` or \` title\` | add \`title\` on top of the route defaults | \`title\` |
| \`-title\` | remove \`title\` from the route defaults | (removed, never checked) |
| \`*variants\` | select the \`variants\` relation with all its own fields | \`variants\` |
| \`variants.*\` | same as \`*variants\` | \`variants\` |
| \`variants.calculated_price\` | select one field on the relation | \`variants.calculated_price\` |
So the check is on the bare dotted path: strip a leading \`+\`, \`-\`, \`*\`, or space, and strip a trailing \`.*\`. \`id\` is always added implicitly and is always allowed.
Two consequences worth internalizing:
- \`*variants\` being allowed does **not** allow \`variants.calculated_price\`. The nested path needs its own entry.
- If any entry in the list has no modifier prefix, the route's defaults are replaced wholesale rather than extended. Defaults are always inside \`allowed\`, so this never causes a rejection by itself — only client-supplied extras can be dropped. A storefront that never passes \`fields\` cannot break on this change.
### Adding fields back: the \`allowFields\` middleware
\`validateAndTransformQuery\` merges \`req.allowed\` into the route's configured list. The framework initialises \`req.allowed\` to \`[]\` on every request and exports an \`allowFields\` middleware that pushes onto it. This is the supported way — and after this release the only way — to expose a field the core allowlist does not carry: most often a link added by the project's own module or by a plugin.
\`\`\`ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { allowFields } from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/store/products", // no \`method\` / \`methods\` key
middlewares: [allowFields("brand", "brand.name")],
},
],
})
\`\`\`
\`allowFields\` takes any mix of strings and arrays of strings, so a module or plugin can export its list and hand it over whole: \`allowFields(brandAllowedFields)\`. Because it *pushes*, several middlewares can each contribute to the same request. Never assign \`req.allowed = [...]\` yourself — that discards what other middlewares already granted.
\`allowFields\` is available from v2.21.0 onward. On earlier versions the equivalent is a hand-written middleware that pushes onto \`req.allowed\`; after the upgrade, prefer \`allowFields\` and migrate existing hand-written ones to it.
### It MUST be a global middleware — omit \`method\` / \`methods\`
**This is the single most common way to get this wrong, and it fails silently.**
A middleware that adds to \`req.allowed\` — \`allowFields\` or a hand-written one — only works if it is registered as a **global** middleware. In Medusa a middleware is global when its entry declares **no \`method\` and no \`methods\` key** — nothing else determines it. The moment \`method: ["GET"]\` is added, the middleware becomes route-scoped, runs *after* \`validateAndTransformQuery\`, and has **no effect whatsoever**. The fields it tried to allow are still stripped, the request still returns \`200\`, and nothing is logged.
Why: Medusa's routes sorter buckets middlewares as \`global -> wildcard -> regex -> static -> params\`, and the bucket is picked by exactly one condition — whether the entry has a \`method\`/\`methods\` key. Entries without one land in \`global\` and run before every core route middleware; entries with one land in the same bucket as the core Store middlewares (which all declare \`method: ["GET"]\`) and therefore run after query validation, which has already resolved the allowlist and reset \`req.allowed\`.
A narrow \`matcher\` is fine and encouraged — \`matcher\` is not the problem, the \`method\` key is. If the override should only apply to certain verbs or paths, branch on \`req.method\` / \`req.path\` **inside the handler**.
The \`@medusajs/eslint-plugin\` recommended preset catches this: \`allow-fields-must-be-global-middleware\` is an **error** on any \`allowFields\` / \`req.allowed\` / \`req.disallowed\` mutation inside a method-scoped middleware entry, and \`prefer-allow-fields-middleware\` **warns** when a middleware writes to \`req.allowed\` by hand instead of using \`allowFields\`. If the project uses the preset, running lint is the cheapest verification available — use it.
To verify ordering at runtime when lint is not configured, put a guard first in the same \`middlewares\` array, ahead of \`allowFields\`, and remove it once it has proven the ordering:
\`\`\`ts
(req, res, next) => {
if (req.queryConfig) {
throw new Error("[allow-fields] ran AFTER validateAndTransformQuery — remove the \\\`method\\\` key")
}
next()
}
\`\`\`
### Other rules that govern the override
- Every path must be listed in full. \`allowFields("brand")\` does not enable \`brand.name\`; pass both.
- It cannot re-open a field blocked by \`disallowed\`, which is applied after the allowlist and ignores \`req.allowed\` entirely. No core Store route sets \`disallowed\` after this release, so this only bites if the project sets one on its own route.
- \`req.disallowed\` is read at the same point by the same middleware and obeys the identical global-only rule.
- Because the Store denylist is gone, \`allowFields\` is now the *only* thing standing between a storefront and a field. Treat every addition as a security decision, not a convenience.
- **An allowed field can still be rejected by the Store relations-depth limit.** The limit (\`storeRelationsLimit\`, default 3 on every \`/store\` route, 4 on the two product routes) is validated *after* the allowlist filter, on the fields that survived it. So a path that was being silently stripped as not-allowed starts returning \`400 The following fields expand more than the maximum of {limit} allowed relations: {fields}\` the moment \`allowFields\` lets it through. Depth is per field: a star field (\`*a.b.c\` or \`a.b.c.*\`) counts all its segments, a scalar field (\`a.b.c.value\`) counts its segments minus one. Compute the depth of every path before adding it, and prefer a shallower path over raising the limit — raising it trades away the performance protection the limit exists to provide, and needs the user's explicit choice.
- Official plugins that add their own Store links register their own global \`allowFields\` middleware (\`@medusajs/loyalty-plugin\` does this for its gift-card relation). Fields added by the **project's own** modules and links are the project's responsibility.
## 3. Everything else in the release
Read the v2.21.0 GitHub release notes and the notes for every release between the project's current version and 2.21.0. Fold any additional breaking change, required migration, or config change into the plan and call it out explicitly. If the project is coming from below 2.20.0, the Store relations-depth limit (\`storeRelationsLimit\`, default 3) and the payment-provider/region validation introduced in 2.20.0 also apply — handle them as part of this upgrade.
For anything not covered here, consult the official Medusa documentation at https://docs.medusajs.com, the GitHub release notes at https://github.com/medusajs/medusa/releases, or the Medusa MCP server (\`ask_medusa_question\`) before acting. Do not guess at APIs, config keys, or version numbers — verify them.
</context>
<inputs>
You are given access to the project's working directory. You must discover the following yourself; do not assume:
- **Project shape**: standalone Medusa project vs. monorepo (e.g. \`apps/backend\` + workspaces). Check for a root \`package.json\` with workspaces and an \`apps/\` directory.
- **Package manager**: detect from the lockfile (\`yarn.lock\`, \`package-lock.json\`, \`pnpm-lock.yaml\`, \`bun.lockb\`). Use it consistently; do not switch it.
- **Current Medusa version**: read from \`package.json\` dependencies / lockfile, and confirm against the installed package on disk. If it cannot be determined, ask the user before proceeding.
- **Store API consumers**: the storefront in this workspace, any admin customization or script calling Store routes, and any other client. Search for \`@medusajs/js-sdk\`, \`sdk.store.\`, \`MEDUSA_BACKEND_URL\`, \`NEXT_PUBLIC_MEDUSA_BACKEND_URL\`, \`x-publishable-api-key\`, \`fields=\`, and \`fields:\`.
- **Custom Store routes**: everything under \`src/api/store/\` and any plugin's, plus the \`validateAndTransformQuery\` query configs in \`src/api/middlewares.ts\` — specifically whether any sets \`allowed\` or \`disallowed\`.
- **Existing \`req.allowed\` middlewares**: any middleware that pushes onto or assigns \`req.allowed\`, and whether its \`defineMiddlewares\` entry has a \`method\` / \`methods\` key.
- **Project-owned links into Store entities**: \`defineLink\` definitions in \`src/links/\` and module links that the storefront reads through a Store route's \`fields\`.
- **Installed plugins** that add Store routes or Store links.
- **Lint setup**: whether \`@medusajs/eslint-plugin\` is configured with the recommended preset.
- **Storefront presence**: a separate storefront app/repo or directory. If no storefront is in this workspace, treat storefront steps as guidance to surface to the user, not edits you can make.
</inputs>
<steps>
Work through these in order. For each, record findings and the proposed change in the plan — do not edit yet. If the current version is already >= 2.21.0, report that and stop; do not downgrade or re-apply migrations.
1. **Detect project shape, package manager, current Medusa version, Node.js version, storefront presence, and installed plugins.** Read the relevant \`package.json\` files and record the current \`@medusajs/medusa\` version. If the working tree has uncommitted changes, say so before touching \`package.json\` or lockfiles.
2. **Confirm the breaking-change list from the release notes.** Read the v2.21.0 release notes and those of every release between the project's current version and 2.21.0. Incorporate anything this prompt does not cover, and call it out explicitly.
3. **Plan the package version bump.** Identify every \`@medusajs/*\` dependency and devDependency across the backend (and admin/plugin/storefront packages if present) and target their v2.21.0-aligned versions, resolving any non-\`2.x\` package (such as \`@medusajs/ui\`) from the npm registry rather than forcing \`2.21.0\`. Plan a single install pass with the detected package manager.
4. **Inventory every Store API \`fields\` and \`order\` selection (static, before the upgrade).** This is the change most likely to break a working storefront at runtime, so inventory it first.
- Search every Store API consumer for field selections, covering all their shapes: literal \`fields\` strings, \`fields=\` inside URL strings, shared constants, default parameters (\`fields ??= "..."\`), and values threaded through wrappers such as \`retrieveCart(id, fields)\`.
- Follow each call site to the literal string. A \`fields\` value that cannot be resolved to a literal is a finding in its own right — record it as **unresolved** with file, line, and reason. Do not guess its contents.
- Record the Store route each selection targets (map the SDK method or URL to the route path), the raw selection, and its normalized paths per the table in \`<context>\`.
- Do the same for \`order\` / \`sort\` parameters, which fail loudly rather than silently.
- Also record every \`validateAndTransformQuery\` config for a custom \`/store\` route whose \`defaults\` reference project links, and every existing \`req.allowed\` middleware and whether it is global.
5. **Plan the upgrade, then present it and stop.** Compile steps 1-4 into the plan described in \`<output_format>\`, stating clearly that the exact list of dropped fields can only be computed once v2.21.0 is installed (step 7), and that any field to be re-exposed will come back for a second, explicit approval (step 9). Wait for the user's approval before editing any files.
6. **Bump versions and reinstall.** Apply the \`@medusajs/*\` bumps from step 3 in every \`package.json\`, then reinstall with the detected package manager. Confirm the install resolves without peer-dependency errors.
7. **Enumerate the real allowlists from the installed package.** Run this from the directory whose \`node_modules\` holds \`@medusajs/medusa\`:
\`\`\`bash
node -e '
const fs = require("fs"), path = require("path");
const base = path.join(process.cwd(), "node_modules/@medusajs/medusa/dist/api/store");
const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) =>
e.isDirectory() ? walk(path.join(d, e.name))
: e.name === "query-config.js" ? [path.join(d, e.name)] : []);
const out = {};
for (const f of walk(base)) {
const route = path.relative(base, path.dirname(f));
for (const [k, v] of Object.entries(require(f))) {
if (v && Array.isArray(v.allowed)) out[route + " :: " + k] = v.allowed.sort();
}
}
fs.writeFileSync("/tmp/medusa-allowed.json", JSON.stringify(out, null, 2));
console.log(Object.keys(out).length + " store query-configs carry an allowed list");
'
\`\`\`
The directory name under \`dist/api/store\` matches the URL path segment; a list endpoint uses the list query config and a single-resource endpoint the retrieve one. Confirm the exact keys against the file rather than assuming them. If a plugin ships its own Store query configs (they live under the plugin's own \`.medusa/server/src/api/store\`), enumerate those too — a plugin's cart config can replace the core one.
**\`/tmp/medusa-allowed.json\` is the authoritative allowlist for this project. Use it — not this prompt, not memory — as the source of truth for the diff.**
8. **Diff the inventory against the allowlists.** For each normalized path from step 4, check exact string membership in the target route's \`allowed\` array (exact equality — no prefix matching, \`id\` always allowed). Every path that is not an exact member is a finding. Do the same for each \`order\` field. Report the findings as a table before changing anything.
9. **Scope the fields to add back, then get approval.** For each finding, apply the first remedy that fits — prefer earlier ones, they keep the allowlist closed:
1. **Delete the field.** If nothing in the client reads it, remove it from the \`fields\` string. Prove it is unused by searching for the property on the response object before deleting.
2. **Drop to a coarser allowed path.** If the client requests \`region.currency_code\` and \`*region\` is allowed as a full relation, switching to \`*region\` returns the same data through an already-allowed path.
3. **Move the data behind a custom Store route** — only when the linked entity is what the page is actually about (a brand page listing that brand's products). The custom route owns its own query config, so the core allowlist stays closed. Do **not** reach for this when the linked data is an attribute hanging off a core entity the page already fetches (a brand name on each product card) — that trades a one-line allowlist entry for a second round trip, client-side stitching, and a route to keep in sync with pagination and filters. Use remedy 4 for those.
4. **Add it back with \`allowFields\`** — the right answer for a linked field that is a plain attribute of an already-fetched core entity, and the fallback for anything remedies 1-3 do not fit.
When scoping the \`allowFields\` list:
- List **every path in full**, including nested ones. A parent never grants its children.
- **Check each path against the route's relations-depth limit** (3 by default, 4 on \`GET /store/products\` and \`GET /store/products/:id\`, or whatever \`storeRelationsLimit\` the route or \`projectConfig.http\` sets) using the depth rule in \`<context>\`. A path that is allowed but too deep turns a silent drop into a hard \`400\` — reshape it to a shallower selection, and only raise the limit if the user explicitly chooses that trade-off.
- Scope \`matcher\` to the narrowest route that needs the field. A narrow matcher is safe; a \`method\` key is not.
- Group the paths per link/module and, where the module owns them, export the array from the module so the middleware reads \`allowFields(brandAllowedFields)\`.
- **Never add a relation that traverses into another shopper's data** — \`orders\`, \`customer\`, \`carts\`, \`payment_collection\`, \`sales_channels\`, \`publishable_api_keys\`, \`price_set\`, \`campaign\`, \`stock_locations\`, or anything nested under them. If a finding needs one of these, stop and hand the decision to the user with the risk stated. The Store denylist that used to backstop this mistake is gone — an entry added here is reachable by any unauthenticated caller with a publishable key, full stop.
Present the proposed remedy per finding, and the exact \`allowFields\` list per matcher, and **wait for the user's explicit approval before widening any allowlist.**
10. **Apply the approved edits.** Client-side \`fields\`/\`order\` changes, custom Store route changes, and \`src/api/middlewares.ts\` entries registering \`allowFields\` as **global** middlewares (no \`method\` / \`methods\` key). Migrate any existing hand-written \`req.allowed\` middleware to \`allowFields\`, and fix any that was method-scoped — it was never working. Add a short comment naming the client code each entry exists for.
11. **Run database migrations** using the project's Medusa CLI (\`npx medusa db:migrate\`), plus any migration the release notes require.
12. **Verify.**
- Lint (\`@medusajs/eslint-plugin\` recommended preset): \`allow-fields-must-be-global-middleware\` must not fire. If it does, the entry has a \`method\` / \`methods\` key — remove it. Never disable the rule.
- If lint is not configured, use the \`req.queryConfig\` guard from \`<context>\` once per matcher to prove the middleware runs before \`validateAndTransformQuery\`, then remove it.
- Re-run the step 8 diff against \`/tmp/medusa-allowed.json\` plus everything passed to \`allowFields\`. Every selection must resolve.
- Build the backend (\`medusa build\`) and run the TypeScript typecheck, \`medusa plugin:build\` for each plugin, and the storefront build if present.
- Start the app and exercise the storefront's main flows — listing, product detail, cart, checkout, account/orders — and check the **rendered data**, not the status code. A \`200\` proves nothing here; a missing field is silent.
Report all results. If the build, typecheck, lint, or migrations fail, report the exact error output and your best-guess cause tied to a specific change, and stop for the user rather than making speculative further edits.
</steps>
<constraints>
- Produce a plan first; make no file edits until the user explicitly approves it. Widening an allowlist needs its own explicit approval on top of that (step 9).
- Edit only what a v2.21.0 upgrade requires. Do not refactor unrelated code, reformat files, or adopt optional v2.21 features. Every change must trace to a v2.21.0 breaking change or the version bump; list anything else under manual follow-ups instead.
- Any middleware that adds to \`req.allowed\` — \`allowFields\` included — must be a global middleware: no \`method\` / \`methods\` key on the \`defineMiddlewares\` entry. Never write a route-scoped one; it applies cleanly, reports nothing, and does nothing.
- Never reassign \`req.allowed\`. Push onto it, or use \`allowFields\`, so other middlewares' contributions survive.
- Never add an allowlist entry that traverses into another shopper's order, customer, cart, or payment data. Hand that decision to the user with the risk stated.
- Do not disable or downgrade \`allow-fields-must-be-global-middleware\` or \`prefer-allow-fields-middleware\` to make the lint step pass.
- Every finding must cite a real file and line in a client. Do not report a field you have not traced to a literal \`fields\` string, and report the ones you could not trace as unresolved.
- Check field membership against \`/tmp/medusa-allowed.json\`, never against any list written in this prompt.
- Never hardcode, print, or persist secrets, connection strings, or API keys. Reference environment variables by name only.
- Match the project's existing package manager; do not switch it.
- Match the project's existing code style (this is a TypeScript Medusa project: no semicolons, double quotes, 2-space indentation, parens on arrow functions). Never use emojis.
- Never edit files inside \`node_modules\`, and never patch the installed packages — this is an upgrade, not a patch.
- If the storefront is not in this workspace, do not fabricate edits to it — surface the storefront steps as instructions for the user to apply in their storefront repo.
- Verify every API, config key, version number, and SDK signature against https://docs.medusajs.com or the Medusa MCP server before relying on it. Do not guess.
- Do not \`git commit\`, \`git push\`, or otherwise publish changes. Leave the working tree modified for the user to review.
</constraints>
<error_handling>
- If the current version cannot be determined: report what you found and ask the user for their current Medusa version before proceeding.
- If a \`@medusajs/*\` package has no \`2.21.0\` release (off the \`2.x\` line): resolve its correct co-released version from the npm registry; if still ambiguous, list it as a must-confirm item instead of guessing.
- If the enumeration script in step 7 reports zero or one store query-config with an \`allowed\` list: the install did not reach v2.21.0, or it ran against the wrong \`node_modules\`. Report the directory you ran it from and stop.
- If a \`fields\` value is assembled at runtime and cannot be resolved statically: flag it for runtime verification rather than rewriting it blindly.
- If a field is missing from a response after the upgrade: it was stripped by the allowlist. Normalize it, check it against \`/tmp/medusa-allowed.json\`, and take it through step 9 — do not raise it straight to \`allowFields\`.
- If a field added with \`allowFields\` is still being dropped: the middleware is route-scoped. Check the \`defineMiddlewares\` entry for a \`method\` / \`methods\` key and remove it. Do not edit \`node_modules\`.
- If \`allowFields\` throws \`Cannot read properties of undefined (reading 'push')\`: the running framework is older than 2.21.0 — the install did not take effect, or the app is running against a stale build.
- If a request starts failing with \`The following fields expand more than the maximum of ... allowed relations\` after you widened the allowlist: the field you allowed exceeds the route's relations-depth limit and was previously being stripped before the check could see it. The message names the offending fields — reshape them to a shallower selection, and raise \`storeRelationsLimit\` only if the user chooses it.
- If a request fails with \`Order field {field} is not valid\`: the sort field is outside that route's allowlist. Sort on an allowed field, or take the field through step 9.
- If the build, typecheck, plugin build, lint, or migrations fail after your edits: report the exact error output, your best-guess cause tied to a specific change, and stop for the user rather than making speculative further edits.
</error_handling>
<output_format>
Respond in Markdown with these sections, in order:
1. **Detected environment** — project shape, package manager, current version -> 2.21.0, Node.js version, storefront present (yes/no), plugins that add Store routes or links, custom Store routes, existing \`req.allowed\` middlewares (and whether each is global), and whether \`@medusajs/eslint-plugin\` is configured.
2. **Migration plan (approval required)** — the intended version bumps, the static inventory of Store API \`fields\` and \`order\` selections (file:line, route, selection, unresolved yes/no), any other breaking change from the release notes, and the migrations to run. State that the dropped-field diff follows the install. End with an explicit request for approval; do not proceed past this section until approved.
3. **Allowlist diff (approval required before widening)** — a table of findings:
\`\`\`
| # | File:line | Route | Requested field | Status | Proposed remedy |
|---|-----------|-------|-----------------|--------|-----------------|
| 1 | src/lib/data/products.ts:72 | GET /store/products | brand.name | dropped | 4 — allowFields on /store/products |
| 2 | src/lib/data/cart.ts:26 | GET /store/carts/:id | <expression> | unresolved | runtime verification |
\`\`\`
Followed by the exact \`allowFields\` list per matcher, with a one-line security justification for each path. If nothing was dropped, say "No field selection falls outside the allowlist." and skip the table.
4. **Changes applied** — bulleted, grouped as: Version bumps / Client field selections / Custom Store routes / \`allowFields\` middlewares / Other breaking changes. Each item: file path + a one-line description. State "none required" for any empty group.
5. **Verification** — the command run, and PASS/FAIL with relevant output for: dependency install, allowlist enumeration (N configs), the re-run field diff, database migrations, lint (naming \`allow-fields-must-be-global-middleware\` explicitly), backend build, TypeScript typecheck, plugin builds, storefront build (if present), and the runtime smoke check with the *rendered data* confirmed.
6. **Manual follow-ups** — anything the user must review or decide: unresolved \`fields\` values to verify at runtime, storefronts living in another repo that need the same audit, allowlist additions declined pending their decision, and other breaking changes surfaced from the release notes. "None" if empty.
</output_format>
<success_criteria>
- Every \`@medusajs/*\` dependency resolves to its v2.21.0-aligned version, verified against the registry, and the install completes without dependency errors.
- The allowlist enumeration ran against the upgraded \`node_modules\` and reported the Store query configs that now carry an \`allowed\` list.
- Every \`fields\` and \`order\` selection in every in-repo Store API client has been traced to a literal string or explicitly reported as unresolved, and checked by exact membership against \`/tmp/medusa-allowed.json\` — not against any list in this prompt.
- No finding was fixed by widening the allowlist when remedies 1-3 genuinely fit, and no allowlist was widened without the user's explicit approval.
- Every middleware that adds to \`req.allowed\` is registered globally with no \`method\` / \`methods\` key, uses \`allowFields\`, and was proven to run before \`validateAndTransformQuery\` (lint clean, or the \`req.queryConfig\` guard did not throw).
- No allowlist entry added traverses into another shopper's order, customer, cart, or payment data.
- Database migrations run to completion; backend build, TypeScript typecheck, lint, and every plugin build pass; the storefront build passes if a storefront is present.
- The runtime smoke check confirmed rendered data, not just \`200\` responses, on listing, product detail, cart, checkout, and account/orders.
- No secrets appear in edits or output; no files under \`node_modules\` were modified; no unrelated files were changed; no commits or pushes were made.
- No file is edited before user approval, and the response contains all six required sections.
</success_criteria>
Exported Admin Dashboard Components, Hooks, and Utilities
The Medusa Admin dashboard now exports the building blocks it uses internally, so admin customizations can match the dashboard's look and behavior instead of reimplementing it. They are available under three subpaths of @medusajs/dashboard:
import { DataTable, RouteFocusModal, Combobox } from "@medusajs/dashboard/components"
import { useProducts, useComboboxData, useQueryParams } from "@medusajs/dashboard/hooks"
import { formatCurrency, getFormattedAddress } from "@medusajs/dashboard/lib"@medusajs/dashboard/componentsexposes layout and table primitives such asLayoutComposer,DataTable,ConfigurableDataTable, andDataGrid, modals such asRouteDrawer,RouteFocusModal,StackedDrawer, andStackedFocusModal, inputs such asCombobox,CountrySelect, andHandleInput, and common pieces such asActionMenu,Form,SectionRow,Thumbnail,NoRecords, and the product table cells.@medusajs/dashboard/hooksexposes the dashboard's data hooks and query keys for customers, orders, products, product variants, promotions, regions, sales channels, shipping options, price preferences, store, and users, along with utility hooks such asuseComboboxData,useDate,useDebouncedSearch, anduseQueryParams.@medusajs/dashboard/libexposes helpers for tables, search entities, currency and money formatting, validation schemas, and address formatting.
The full reference of every exported component, hook, and utility is at docs.medusajs.com/resources/admin-components.
Features
- feat(dashboard,js-sdk): export common components, hooks by @leobenzol in #16506
- feat(framework,medusa): allow overriding a route's disallowed query fields from a global middleware by @shahednasser in #16716
- feat(eslint-plugin): add rules for req.allowed, allowFields, and req.disallowed by @shahednasser in #16778
- feat(dashboard, types, medusa): add support for metadata management for promotions by @shahednasser in #16719
- feat(loyalty-plugin): add a validate hook to addGiftCardToCartWorkflow by @shahednasser in #16755
- feat: add search helpers, add default search endpoint by @sradevski in #16762
- feat: use cross-module joins instead of index module by @sradevski in #16156
- feat: implement a provider-agnostic swap index by @sradevski in #16710
- feat: implement catchup phase for seeding and reindexing by @sradevski in #16726
- feat: make reindex timestamp configurable by @sradevski in #16753
Bugs
- fix(payment): correctly persist the acting user on payment captures by @shahednasser in #16750
- fix(product, utils, medusa): return published products only for tags, collections, categories, and type store routes by @shahednasser in #16674
- fix(core-flows): only autocapture if payment is authorized successfully by @shahednasser in #16722
- fix(core-flows): validate cart currency_code against the region's currency on create by @shahednasser in #16129
- fix(notification): forward the shared context into getProviderForChannels by @shahednasser in #16721
- fix(framework): mark migration scripts as completed only after successful run by @shahednasser in #16351
- fix(framework): stabilize stateful path regexes by @DS123-ally in #16370
- fix(promotion): apply buy-get promotions independently of line item order by @danlapteacru in #16397
- fix(fulfillment): compare numeric shipping option rule values numerically by @shuvamk in #16301
- fix(event-bus-redis): move EXPIRE into same pipeline as RPUSH by @vansh17June in #16563
- fix(utils): honor decimalPlaces of 0 in MathBN.convert by @Arunendra21 in #16342
- fix(utils): title-case script subtags in two-segment locales by @tushardev-365 in #16456
- fix(loyalty-plugin): compensate the workflow each order hook actually ran by @shahednasser in #16754
- fix(loyalty-plugin): exclude soft-deleted transactions from store credit balances by @shahednasser in #16757
- fix(loyalty-plugin): honor a store credit amount of 0 on a cart by @lazerg in #16378
- fix(dashboard): add missing Polish translations by @owlcode in #16763
- fix(dashboard): strip table query prefix in product tag list loader by @lazerg in #16393
- fix(dashboard): add global add-row action to metadata editor by @lazerg in #16288
- fix(dashboard): preserve inventory kit variant indices by @sansynx in #16366
- fix(docs-ui): MainNav z-index overlap, duplicate GA key, and Windows broken-link-checker path bug by @vjymisal0 in #16362
- fix: improve seed lock handling by @sradevski in #16781
- fix: resolve several issues with the locking implementation by @sradevski in #16768
- fix: search improvements by @sradevski in #16774
- fix: apply highlighting and typos more gracefully by @sradevski in #16725
- fix: changeset for loyalty by @sradevski in #16782
Documentation
- docs: add troubleshooting for cloud projects by @shahednasser in #16772
- docs: add pagination and filters to api reference versions route by @shahednasser in #16758
- docs: clarify disabling body parser and remove bloom suggestion by @shahednasser in #16723
- docs: fix loyalty plugin workflow import paths to use /workflows subpath by @shahednasser in #16735
- docs-utils: extract allowed list from API routes into OAS by @shahednasser in #16767
- docs: fix build by @shahednasser in #16771
Chores
- chore(medusa): revert using cross-modules back to index module by @shahednasser in #16787
- chore(framework, medusa): set a strict allowed list for store API routes by @shahednasser in #16702
- chore(dashboard,medusa): name admin search entities after their graph entity by @NicolasGorga in #16587
- chore(create-medusa-app): remove unused node-fetch dependency by @shahednasser in #16780
- chore(create-medusa-app): change Next.js starter wording by @NicolasGorga in #16712
- chore: resolve dependabot alert for js-yaml by @shahednasser in #16773
- chore(deps): bump the security-updates group across 1 directory with 4 updates by @dependabot in #16766
- chore(deps): bump multer by @dependabot in #16764
- chore(docs): automated cloud documentation update by @shahednasser in #16779
- chore(docs): update version in documentation (automated) by @github-actions in #16711
- chore(docs): updated API Reference (automated) by @github-actions in #16709
- chore(docs): generated References (automated) by @github-actions in #16708
- chore(docs): generated and updated UI Reference (automated) by @github-actions in #16707
- chore(docs): generated DML JSON files (automated) by @github-actions in #16706
New Contributors
- @sansynx made their first contribution in #16366
- @Arunendra21 made their first contribution in #16342
- @tushardev-365 made their first contribution in #16456
- @vjymisal0 made their first contribution in #16362
- @danlapteacru made their first contribution in #16397
- @owlcode made their first contribution in #16763
Full Changelog: v2.20.1...v2.21.0