Overview
Icon for Admin Customizations

Admin Customizations

Reusable quality-of-life admin customizations

@webbers/webbers-medusa

Quality-of-life admin customizations for Medusa v2, extracted from the Frecious backend so they can be reused across projects.

Features

  • Notes — free-text notes on any entity, shown in the admin detail sidebar. Ships customer and order notes (with links + widgets); the generic Copy to clipboardnote module, Copy to clipboard/admin/note endpoint and note component are reusable so other plugins can attach notes to their own entities (see "Attaching notes to your own entities").
  • Anonymize customer — AVG/GDPR data scrub for a customer across customer, cart and order records, plus any host-configured extra tables (superadmin only). See "Configuring extra tables".
  • Transfer guest orders — merge unassigned guest orders (matched by email) into a customer account.
  • Edit customer email — change a registered customer's email atomically across customer and auth identity, plus any host-configured extra tables (clears the cached Klaviyo id). See "Configuring extra tables".
  • Order discount breakdown — sidebar widget summarising the discount codes applied to an order, with amounts and percentages.
  • Order notifications — sidebar widget listing the transactional emails sent for an order.
  • Always-free promotion — flag a promotion as "always free shipping". Ships the data model, workflows, link and admin widget; the host app wires the trigger (see below).
  • Disallow price-list discounts — a toggle on the price-list detail page that stops discount/promotion codes from overriding that price list. Ships the data model, link, workflows, admin API + widget, and a store add-to-cart endpoint that enforces it; the storefront wires the endpoint (see below).

Toggling features

The plugin adds a Settings → Webbers QoL page in the admin with a switch per feature. Toggling a feature off:

  • hides its admin widget — every widget reads the flags via Copy to clipboarduseFeatureEnabled(key) and renders nothing when disabled; and
  • blocks its endpoints — a Copy to clipboardfeatureGuard middleware returns Copy to clipboard403 for the feature's API routes (see Copy to clipboardsrc/api/middlewares.ts).

Flags are persisted by the bundled Copy to clipboardwebbers_settings module (table Copy to clipboardwebbers_feature_flag) and default to ON when no row exists, so the plugin is fully active out of the box. The canonical list of features lives in Copy to clipboardsrc/feature-flags.ts (shared by the server and admin builds).

Always-free is widget-only gating: disabling it hides the promotion switch, but the actual link is still written by the host app's promotion hooks, so leave the hooks in place if you rely on existing always-free promotions.

After installing, run the migration so the flag table exists:

npx medusa db:migrate

What's included

Concern Location Feature catalog (single source of truth) Copy to clipboardsrc/feature-flags.ts Settings module + migration (feature flags) Copy to clipboardsrc/modules/webbers-settings Settings admin page (Settings → Webbers QoL) Copy to clipboardsrc/admin/routes/settings/webbers-medusa Note module + migration Copy to clipboardsrc/modules/note Custom-query (Copy to clipboardcustom_query) module (raw SQL helpers, no migration) Copy to clipboardsrc/modules/custom-query Always-free (Copy to clipboardpromotion_addition) module + migration Copy to clipboardsrc/modules/promotion-addition Price-list-ext (Copy to clipboardprice_list_ext) module + migration Copy to clipboardsrc/modules/price-list-ext Links (customer/order note, promotion always-free, price-list-ext) Copy to clipboardsrc/links Workflows (note, always-free, price-list-ext) Copy to clipboardsrc/workflows Admin widgets Copy to clipboardsrc/admin/widgets Admin API routes Copy to clipboardsrc/api/admin Store API route (price-list-aware add-to-cart) Copy to clipboardsrc/api/store/carts/[id]/line-item-price-list HTTP middlewares (note + edit-email validators, promotion Copy to clipboardadditional_data, price-list-ext) Copy to clipboardsrc/api/middlewares.ts

Installation

Install the plugin in your Medusa app:

npm install @webbers/webbers-medusa
# or, during local development:
npx medusa plugin:add @webbers/webbers-medusa

Register it in the host app's Copy to clipboardmedusa-config.ts:

plugins: [
{
resolve: '@webbers/webbers-medusa',
options: {},
},
]

After installing, run the migrations so the plugin's tables exist:

npx medusa db:migrate

The bundled Copy to clipboardcustom_query module

The plugin bundles a Copy to clipboardcustom_query module exposing Copy to clipboardPSQLQuery / Copy to clipboardPSQLTransaction — the anonymize, transfer-guest-orders, edit-email and order-notifications routes resolve it from the container by key (Copy to clipboardcustom_query).

Upgrading from ≤ 1.0.0: earlier versions required the host app to register its own Copy to clipboardcustom_query module. That copy must now be removed (both the module directory and its Copy to clipboardmedusa-config.ts entry) — two modules with the same key cannot coexist. The bundled module is a drop-in replacement (same key, same API, no migration/table).
This plugin has no dependency on any subscriptions package (or any other domain module). The anonymize / edit-email routines only touch Copy to clipboardcustomer, Copy to clipboardcart and Copy to clipboardorder tables out of the box; any extra tables (e.g. Copy to clipboardsubscription) are declared by the host via plugin options — see "Configuring extra tables" below.

Configuring extra tables (anonymize / edit-email)

The anonymize and edit-email routines scrub the core Medusa tables on their own. To include tables owned by other modules — without this plugin depending on them — declare them in the plugin options in your Copy to clipboardmedusa-config.ts. Every table/column name is validated (Copy to clipboard^[A-Za-z_][A-Za-z0-9_]*$) and all statements run inside the same transaction as the core scrub, so atomicity is preserved.

plugins: [
{
resolve: '@webbers/webbers-medusa',
options: {
anonymize: {
// Domain for the generated anonymized email (anon+<ts>@<domain>). Default: anonymized.invalid
emailDomain: 'example.com',
// Tables with a customer-id column: set email columns to the anon email, null the rest.
customerScopedTables: [
{ table: 'subscription', emailColumns: ['email'], nullColumns: ['payment_config'] },
],
// Address tables reached via a parent table's billing/shipping FKs (nulled out).
// customerIdColumn/billingColumn/shippingColumn/nullColumns are optional (sensible defaults).
addressTables: [
{ addressTable: 'subscription_address', parentTable: 'subscription' },
],
},
editEmail: {
// Tables whose email column should follow the customer's new email.
// emailColumn defaults to 'email', customerIdColumn to 'customer_id'.

Attaching notes to your own entities

The Copy to clipboardnote module, the Copy to clipboard/admin/note endpoint (Copy to clipboardentity_type / Copy to clipboardtype are free-form strings) and the note component are generic. To add notes to another entity from a separate plugin or your app — e.g. subscriptions — depend on Copy to clipboard@webbers/webbers-medusa and:

  1. Define the link between your module and the note module:
import { defineLink } from '@medusajs/framework/utils';
import MyModule from '../modules/my-module';
import NoteModule from '@webbers/webbers-medusa/modules/note';
export default defineLink(MyModule.linkable.myEntity, NoteModule.linkable.note);
  1. Render a note widget in your entity's detail zone that calls Copy to clipboard/admin/note with your Copy to clipboardentity_type (the module's registration key). The Copy to clipboardnotes feature flag guards the endpoint, so the note stays hidden/blocked when notes are disabled in Settings → Webbers QoL.

This keeps the QoL plugin free of any dependency on your module while still owning the note storage.

Activating the always-free promotion behaviour

Toggling the widget writes Copy to clipboardadditional_data.always_free on the promotion (the plugin's middleware whitelists that field). Turning that flag into an Copy to clipboardalways_free link requires a handler on Medusa's promotion workflow hooks. Medusa allows only one handler per workflow hook, and a real app usually does other work in Copy to clipboardpromotionsCreated/Copy to clipboardpromotionsUpdated too — so the plugin does not register these hooks for you. Add them in your app's Copy to clipboardsrc/workflows/hooks/promotions:

Copy to clipboardpromotion-created.ts

import { createPromotionsWorkflow } from '@medusajs/medusa/core-flows';
import {
createAlwaysFreeFromPromotionWorkflow,
CreateAlwaysFreeFromPromotionWorkflowInput,
} from '@webbers/webbers-medusa/workflows';
createPromotionsWorkflow.hooks.promotionsCreated(async ({ promotions, additional_data }, { container }) => {
const workflow = createAlwaysFreeFromPromotionWorkflow(container);
for (const promotion of promotions) {
// ...any other app-specific work you already do here...
await workflow.run({
input: { promotion, additional_data } as CreateAlwaysFreeFromPromotionWorkflowInput,
});
}
});

Copy to clipboardpromotion-updated.ts

import { updatePromotionsWorkflow } from '@medusajs/medusa/core-flows';
import { ContainerRegistrationKeys } from '@medusajs/framework/utils';
import {
createAlwaysFreeFromPromotionWorkflow,
CreateAlwaysFreeFromPromotionWorkflowInput,
updateAlwaysFreeFromPromotionWorkflow,
UpdateAlwaysFreeFromPromotionStepInput,
} from '@webbers/webbers-medusa/workflows';
import PromotionAlwaysFreeLink from '@webbers/webbers-medusa/links/promotion-always-free';
updatePromotionsWorkflow.hooks.promotionsUpdated(async ({ promotions, additional_data }, { container }) => {
const query = container.resolve(ContainerRegistrationKeys.QUERY);
const create = createAlwaysFreeFromPromotionWorkflow(container);
const update = updateAlwaysFreeFromPromotionWorkflow(container);
for (const promotion of promotions) {
const existing = await query.graph({
entity: PromotionAlwaysFreeLink.entryPoint,
fields: ['*'],
filters: { promotion_id: promotion.id },
The plugin emits no Copy to clipboard.d.ts (Copy to clipboarddeclaration: false, to avoid a Copy to clipboardcreateWorkflow type-portability error), so these imports resolve to Copy to clipboardany in the host app unless it sets Copy to clipboardnoImplicitAny. That is intentional — keep the casts above so the hook bodies stay type-checked.

Reading the flag elsewhere (e.g. a fulfillment provider deciding shipping is free) can join the link table directly:

SELECT af.always_free
FROM promotion p
LEFT JOIN promotion_promotion_promotion_addition_always_free paf ON paf.promotion_id = p.id
LEFT JOIN always_free af ON af.id = paf.always_free_id
WHERE p.id = ?

Activating the disallow-price-list-discounts behaviour

The admin widget writes Copy to clipboardmetadata.disallow_discounts onto the price list's extended record (Copy to clipboardprice_list_ext, linked to Medusa's Copy to clipboardprice_list). The toggle only records intent — the enforcement happens when items are added to the cart.

The plugin ships the enforcement endpoint at Copy to clipboardPOST /store/carts/:id/line-item-price-list. It behaves like Medusa's built-in add-to-cart, but when the body's Copy to clipboardmetadata.price_list_id points at a price list flagged Copy to clipboarddisallow_discounts, it sets Copy to clipboardis_discountable: false on the line item (and stamps Copy to clipboardmetadata.disallowed_discounts_pricelist = true). When the Disallow price-list discounts feature is toggled off, the endpoint skips that logic and behaves as a plain add-to-cart, so it is safe to call unconditionally.

To activate the behaviour, have the storefront add price-list items through this endpoint instead of the default Copy to clipboardPOST /store/carts/:id/line-items:

await sdk.client.fetch(`/store/carts/${cartId}/line-item-price-list`, {
method: 'POST',
body: {
variant_id: variantId,
quantity,
metadata: { price_list_id: priceListId },
},
});

Build

pnpm --filter @webbers/webbers-medusa build

You may also like

Browse all integrations

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?