npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hartl-services/medusa-free-shipping

v0.2.2

Published

Configurable free-shipping threshold plugin for Medusa.

Readme

Medusa Free Shipping

Medusa v2.19 plugin for a shop-wide free-shipping threshold with a separate threshold for affiliates. It has no Admin page and no HTTP route of its own: its switch and its settings are declared to @hartl-services/medusa-base and edited on the base plugin's generic page Settings → Hartl Services Plugins → Versandkostenfrei → Öffnen (/settings/hartl-services/plugin/free_shipping).

What it does

  • Keeps two automatic Medusa promotions (AUTO-FREE-SHIPPING-STANDARD, AUTO-FREE-SHIPPING-AFFILIATE, 100 % off shipping) in sync with the settings and the plugin switch:

    • right after the settings are saved in the Admin (plugin-settings.updated.v1 subscriber),
    • right after the switch is changed in the Admin (plugin-features.updated.v1 subscriber),
    • every 15 minutes through the free-shipping-promotion-sync scheduled job (worker instances only), which also repairs drift and covers a lost event.

    Every run holds one Locking-module lock (free-shipping-promotion-sync, 60 s timeout) around the whole sync - reading the switch and the settings and reconciling both promotions - so the job and the two subscribers never interleave their rule changes, and the last run always writes the latest values. The shop's configured Locking provider applies (Medusa's default in-memory provider serializes within one process only; use a shared provider such as @medusajs/locking-redis when server and worker are separate processes). The sync writes only differences, so a run against in-sync promotions is read-only. Changed rules are created before outdated ones are deleted, so an automatic promotion never exists without its rules (which would grant free shipping to every cart).

  • Adds the cart's affiliate qualification to the promotion context (hook on updateCartPromotionsWorkflow). This uses the affiliate module only if it is registered; without the Affiliate plugin, only the default threshold applies.

Settings

Stored by the base plugin (plugin_setting_state, plugin key free_shipping); every setting has a default, so the plugin works without ever saving the page. The plugin has no medusa-config options.

| Key | Type | Default | Label | Effect | | ----------------------------------------- | ------- | ------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | | default_threshold | number ≥ 0 | 50 | Kostenlose Lieferung ab (€) | item_total threshold of AUTO-FREE-SHIPPING-STANDARD | | affiliate_threshold | number ≥ 0 | 100 | Für Affiliates ab (€) | item_total threshold of AUTO-FREE-SHIPPING-AFFILIATE | | affiliate_applies_to_account_holders | boolean | on | Gilt, wenn der Kunde selbst Affiliate ist | the cart's customer is an affiliate → affiliate threshold | | affiliate_applies_to_referred_customers | boolean | off | Gilt, wenn der Warenkorb über einen Affiliate-Link/-Code zugeordnet wurde | the cart is assigned to an affiliate referral → affiliate threshold |

The base validates the values (a negative threshold is rejected with 400). Server code reads them with resolveFreeShippingSettings(container) (src/utils/free-shipping-settings.ts, a typed wrapper around the base's getPluginSettings). The two boolean rules are evaluated in the cart hook on every promotion computation, so they take effect without a promotion resync; the thresholds reach the promotions through the sync described above.

The cart hook is fail-safe: if the settings cannot be read, the error is reported with logger.error (the base plugin's logger wrapper forwards it to Sentry) and the cart is computed with the two rules this process last read successfully, or with their declared defaults (account holders on, referred customers off) when it has not read them yet. The cart request never fails because of it. The job and the subscribers let a read failure propagate (the job runs again, the event is retried).

Events

| Event | Direction | Handling | | ---------------------------- | --------- | ------------------------------------------------------------------------------------ | | plugin-settings.updated.v1 | consumed | plugin_key === "free_shipping": re-sync both promotions with the current settings | | plugin-features.updated.v1 | consumed | plugin_key === "free_shipping": re-sync (activate/deactivate) both promotions |

Both events are emitted by the base plugin after an Admin save; their contracts are exported from @hartl-services/medusa-food-supplements-contracts/events/plugin-features. The plugin emits no events.

Installation in a shop

pnpm add @hartl-services/medusa-free-shipping @hartl-services/medusa-base @hartl-services/medusa-food-supplements-contracts

@hartl-services/medusa-base (>= 1.0.0) is a required peer: it stores the switch and the settings, renders the Admin page and emits the change events, and must be registered as a plugin as well.

// medusa-config.ts
plugins: [
  { resolve: "@hartl-services/medusa-base", options: {} },
  {
    resolve: "@hartl-services/medusa-free-shipping",
    options: {},
  },
]

Register @hartl-services/medusa-base before this plugin: medusa db:migrate migrates the modules in configuration order, and the 0.2.0 upgrade migration writes into the base plugin's tables. Run medusa db:migrate after installing or upgrading.

Upgrading to 0.2.0

  • The plugin's own settings table free_shipping_settings is dropped, and with it GET/POST /admin/free-shipping-settings, the former Admin page and the Contracts subpaths types/free-shipping-admin, schemas/free-shipping and paths/free-shipping. The migration carries the stored thresholds and both affiliate rules over into the base settings (plugin_setting_state, plugin key free_shipping) unless the base already holds settings for the plugin.
  • The former enabled column is replaced by the base switch free_shipping; a stored enabled = false is carried over as switch off (unless the base already holds a switch value for the plugin).
  • If the base tables do not exist yet when the migration runs (base not registered, or registered after this plugin), nothing is carried over: the migration logs a NOTICE, drops the table, and the defaults (50 / 100 / on / off, switch on) apply until the values are entered on the plugin's base page.

Feature toggles

The plugin declares its switch under the plugin key free_shipping. Storefronts read it from GET /store/plugin-features/free_shipping.

| Key | Label | Default | Gates | Keeps running while off | | --------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | (plugin switch) | Versandkostenfrei | on | status of the promotions AUTO-FREE-SHIPPING-STANDARD and AUTO-FREE-SHIPPING-AFFILIATE (active while on, inactive while off) | the settings (thresholds stay editable and are synced into the inactive promotions), the free-shipping-promotion-sync job (it deactivates the promotions), the promotion-context hook on the cart path |

There are no sub-features. No route answers 404 because of the switch; the store-visible effect of "off" is that the two automatic promotions are inactive. The switch takes effect immediately on an Admin change and at the latest with the next 15-minute job run otherwise.

Peer dependencies are @hartl-services/medusa-base and the host's Medusa 2.19 server packages @medusajs/framework and @medusajs/medusa. The plugin ships no Admin UI of its own (its page is the base plugin's generic one), so it needs none of the Admin packages (@medusajs/admin-sdk, @medusajs/ui, React, React Router, TanStack Query).