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

medusa-plugin-dynamic-config

v0.0.29

Published

A starter for Medusa plugins.

Readme

medusa-plugin-dynamic-config

Medusa v2 plugin for defining dynamic configuration schemas in plugin options, storing runtime overrides in DB, and exposing both admin management APIs and nested storefront config output.

Plugin Overview

medusa-plugin-dynamic-config provides a configurable runtime config system for Medusa:

  • define config groups/fields in plugin options (configs)
  • persist editable values in dynamic_config_value table
  • merge defaults + saved values into a snapshot for admin UI/API
  • expose storefront-ready nested JSON config via Store API
  • ship an Admin route (Dynamic Config) for editing config values

Problem It Solves

It enables non-deploy configuration changes (feature toggles, text/copy, numbers, booleans, arrays/objects) through admin-managed values, instead of hardcoding settings in source code.

Medusa Version

Built for Medusa v2 (@medusajs/framework / @medusajs/medusa 2.11.2).

Installation & Setup

Install

npm install medusa-plugin-dynamic-config

or

yarn add medusa-plugin-dynamic-config

Register plugin in medusa-config.ts

import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  plugins: [
    {
      resolve: "medusa-plugin-dynamic-config",
      options: {
        configs: [
          {
            id: "homepage-config",
            title: "Homepage Config",
            active: true,
            structure: [
              {
                id: "banner",
                type: "object",
                children: [
                  {
                    id: "copy",
                    type: "short-text",
                    defaultValue: "Free shipping over $100",
                    required: true,
                  },
                ],
              },
            ],
          },
        ],
      },
    },
  ],
})

Run migrations

npx medusa db:migrate

Creates dynamic_config_value with unique key index.

Configuration (config.ts / plugin options)

This plugin uses plugin options (not a dedicated config.ts file) with a top-level configs option.

Plugin Options

| Option | Type | Required | Default | Description | |---|---|---|---|---| | configs | Record<string, DynamicConfigOptionInput> \| DynamicConfigOptionInput[] | No | [] | Defines dynamic config schema and default values. |

DynamicConfigOptionInput forms

The plugin supports:

  1. Structured group config (DynamicConfigGroupOptionInput)
  2. Legacy flat config (LegacyDynamicConfigOptionInput)

Structured group option fields

| Field | Type | Required | Default | Description | |---|---|---|---|---| | id | string | Yes | - | Group identifier. | | title | string | No | Humanized id | Group display title. | | active | boolean | No | true | Group active status. | | structure | DynamicConfigFieldInput[] | Yes | - | Hierarchical field schema. |

Field input options (DynamicConfigFieldInput)

| Field | Type | Required | Default | Description | |---|---|---|---|---| | id | string | No | Generated | Field id used in key generation. | | key | string | No | Generated | Explicit flat key (overrides generated key). | | title | string | No | Humanized key | Display label. | | description | string | No | - | Field description text. | | helper | string | No | - | Helper/tooltip text. | | type | "short-text" \| "long-text" \| "number" \| "boolean" \| "file" \| "object" \| "array" | No | Inferred | Field type. | | required | boolean | No | true (except object/array variations) | Required validation marker for admin form. | | defaultValue / default_value | unknown | No | type-based fallback | Default value seed. | | value | unknown | No | - | Initial value seed. | | itemType | ConfigFieldType | No | Inferred | Array item type. | | children | DynamicConfigFieldInput[] | No | - | Nested fields (for object or array-object items). |

Complete example config block

{
  resolve: "medusa-plugin-dynamic-config",
  options: {
    configs: [
      {
        id: "homepage-config",
        title: "Homepage Config",
        active: true,
        structure: [
          {
            id: "banner",
            type: "object",
            children: [
              {
                id: "copy",
                type: "short-text",
                defaultValue: "Free express shipping",
                required: true,
              },
              {
                id: "show",
                type: "boolean",
                defaultValue: true,
              },
            ],
          },
          {
            id: "referral-level-rules",
            type: "array",
            itemType: "object",
            children: [
              { id: "level", type: "number", defaultValue: 1 },
              { id: "percent_of_cart_discount", type: "number", defaultValue: 5 },
            ],
            value: [
              { level: 1, percent_of_cart_discount: 5 },
              { level: 2, percent_of_cart_discount: 10 },
            ],
          },
        ],
      },
      {
        id: "branding-logo",
        key: "branding.logo",
        title: "Branding Logo",
        type: "file",
        value: "https://cdn.example.com/logo.png",
      },
    ],
  },
}

Environment Variables

No runtime process.env.* usage exists in this plugin source.

| Variable | Required | Purpose | Example | |---|---|---|---| | None | No | Runtime behavior is driven by plugin options and database values. | - |

⚠️ Note: src/modules/README.md includes template docs mentioning process.env, but those references are not used by runtime plugin logic.

REST APIs / Routes

GET /admin/dynamic-config/configs

Returns merged config snapshot (schema groups + effective values).

  • Auth: Admin JWT/session (admin route scope)
  • Query params: none
  • Response schema:
{
  "groups": [
    {
      "id": "homepage-config",
      "title": "Homepage Config",
      "active": true,
      "fields": []
    }
  ],
  "values": {
    "homepage-config.banner.copy": "Free express shipping"
  }
}

POST /admin/dynamic-config/configs

Saves config values (upsert incoming keys + delete removed keys).

  • Auth: Admin JWT/session
  • Body schema:

| Field | Type | Required | Description | |---|---|---|---| | values | Record<string, string \| number \| boolean> | No | Flat key-value map to persist. Missing keys (compared to currently saved set) are deleted. |

  • Response schema:
{ "success": true }
  • Error: 500 with { error: "..." } on save failures.

GET /store/dynamic-config

Returns nested JSON config transformed from flat keys.

  • Auth: Public Store API route (publishable key usage depends on store setup)
  • Query params: none
  • Response schema (example):
{
  "homepage-config": {
    "banner": {
      "copy": "Free express shipping"
    }
  }
}

Notes:

  • Uses default values merged with saved DB values.
  • Applies flat-key to nested-object reconstruction.
  • Includes array index parsing (e.g. rules[0].level).

GET /store/plugin

  • Auth: Public
  • Response: HTTP 200

Important endpoint examples

curl -X GET "http://localhost:9000/admin/dynamic-config/configs" \
  -H "Authorization: Bearer <admin_jwt>"
curl -X POST "http://localhost:9000/admin/dynamic-config/configs" \
  -H "Authorization: Bearer <admin_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "values": {
      "homepage-config.banner.copy": "Now shipping worldwide",
      "homepage-config.banner.show": true
    }
  }'
curl -X GET "http://localhost:9000/store/dynamic-config" \
  -H "x-publishable-api-key: <publishable_key>"
await fetch("/admin/dynamic-config/configs", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    values: {
      "branding.logo": "https://cdn.example.com/logo-v2.png",
    },
  }),
})

Services

DynamicConfigModuleService

  • Location: src/modules/dynamic-config/service.ts
  • Extends: MedusaService with ConfigValue model
  • Purpose:
    • parse/normalize plugin config schema
    • merge defaults with persisted overrides
    • persist value updates
    • provide flat and nested projections

Key methods:

| Method | Signature | Description | |---|---|---| | listGroups | () => DynamicConfigGroupDefinition[] | Returns normalized group schema. | | listValues | () => Promise<ConfigValueMap> | Returns merged defaults + DB-saved values. | | listConfigValuesByKeyPrefix | (keyPrefix: string) => Promise<ConfigValueMap> | Returns persisted entries matching prefix. | | getNestedConfigByKeyPrefix | (keyPrefix: string) => Promise<Record<string, unknown>> | Returns nested JSON for matched prefix keys. | | getSnapshot | () => Promise<DynamicConfigSnapshot> | Returns { groups, values } payload for admin API/UI. | | saveValues | (values: ConfigValueMap) => Promise<void> | Upserts incoming keys and deletes keys removed from incoming map. |

Internal helper behavior implemented in service:

  • field key auto-generation
  • primitive normalization/serialization/deserialization
  • array/object schema handling
  • stale key cleanup at service init

Nested config utility functions

From src/modules/dynamic-config/nested-config.ts:

| Function | Description | |---|---| | parseKeySegments | Parses flat keys into property/index segments. | | convertToNestedJSON | Reconstructs nested objects/arrays from flat key map. | | unwrapSingleKeyPrimitiveObjectWrappers | Unwraps single-key primitive object wrappers in arrays. | | stripFlatKeyPrefix | Removes a prefix from flat keys before nested reconstruction. |

Workflows & Steps (Medusa v2)

No runtime workflows (createWorkflow) or steps (createStep) are implemented in plugin runtime code.

Subscribers / Event Hooks

No runtime subscribers are implemented in plugin runtime code.

Admin UI / Widgets

Admin route extension: Dynamic Config page

  • Location: src/admin/routes/dynamic-config/page.tsx
  • Route label: Dynamic Config
  • Icon: Adjustments
  • What it renders:
    • config dashboard with summary cards
    • collapsible group panels
    • dynamic field controls for supported field types
    • API example section
    • save form state with success/error feedback
  • Interactions:
    • fetch schema + values from GET /admin/dynamic-config/configs
    • client-side required-field validation
    • save values via POST /admin/dynamic-config/configs
    • group toggling and dynamic array field item operations
  • Data consumed:
    • snapshot payload (groups, values)
    • user-entered updates mapped back to flat key map

Models & Entities

dynamic_config_value

Defined in src/modules/dynamic-config/models/config-value.ts and migration Migration20250120000000....

| Field | Type | Nullable | Description | |---|---|---|---| | id | text | No | Primary key | | key | text | No | Flat config key | | value | text | No | Serialized config value | | created_at | timestamptz | No | Creation timestamp | | updated_at | timestamptz | No | Update timestamp | | deleted_at | timestamptz | Yes | Soft-delete timestamp |

Indexes / constraints:

  • unique partial index dynamic_config_value_key_idx on key where deleted_at IS NULL

Relationships:

  • No explicit relations to other Medusa entities; this is a standalone key-value store.

Use Cases & Examples

  1. Storefront copy control without deploys

    • Scenario: update homepage banner text immediately.
    • Use: admin page/API save, storefront reads via GET /store/dynamic-config.
  2. Feature toggle management

    • Scenario: enable/disable UI modules with boolean flags.
    • Use: boolean field definitions + storefront nested config consumption.
  3. Threshold and numeric setting tuning

    • Scenario: update pricing/order thresholds quickly.
    • Use: number fields persisted through admin POST /admin/dynamic-config/configs.
  4. Structured nested settings

    • Scenario: maintain grouped object/array settings (e.g., rule lists).
    • Use: structured schema (object / array) + nested JSON conversion utilities.
  5. Environment-specific config overlays

    • Scenario: set per-environment values while preserving defaults in plugin options.
    • Use: plugin defaults + DB overrides merged by listValues.

Troubleshooting

Admin page shows “Dynamic config schema is empty...”

  • Cause: plugin registered without configs option entries.
  • Fix: add structured or legacy config definitions under plugin options.configs.

Saving fails with Failed to save config (...)

  • Cause: backend save endpoint returned error.
  • Fix: inspect server logs and verify valid payload shape under { values: { ... } }.

Store API returns empty object

  • Cause: no defaults configured and no values saved.
  • Fix: define defaults in plugin options or save values through admin API/page.

Missing table or DB errors for dynamic_config_value

  • Cause: migration not applied.
  • Fix: run npx medusa db:migrate.

Unexpected removed keys after save

  • Cause: saveValues deletes previously persisted keys not present in incoming payload.
  • Fix: when updating through custom clients, send complete intended key set (not only partial subset) if you need to retain existing persisted keys.

Nested array/object output looks wrapped

  • Cause: source keys/shape may include wrapper patterns around array items.
  • Fix: ensure consistent field key definitions; plugin includes wrapper unwrapping logic for single-key primitive-object wrappers.