medusa-plugin-dynamic-config
v0.0.29
Published
A starter for Medusa plugins.
Maintainers
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_valuetable - 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-configor
yarn add medusa-plugin-dynamic-configRegister 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:migrateCreates 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:
- Structured group config (
DynamicConfigGroupOptionInput) - 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.mdincludes template docs mentioningprocess.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:
500with{ 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:
MedusaServicewithConfigValuemodel - 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
- fetch schema + values from
- Data consumed:
- snapshot payload (
groups,values) - user-entered updates mapped back to flat key map
- snapshot payload (
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_idxonkeywheredeleted_at IS NULL
Relationships:
- No explicit relations to other Medusa entities; this is a standalone key-value store.
Use Cases & Examples
Storefront copy control without deploys
- Scenario: update homepage banner text immediately.
- Use: admin page/API save, storefront reads via
GET /store/dynamic-config.
Feature toggle management
- Scenario: enable/disable UI modules with boolean flags.
- Use: boolean field definitions + storefront nested config consumption.
Threshold and numeric setting tuning
- Scenario: update pricing/order thresholds quickly.
- Use: number fields persisted through admin
POST /admin/dynamic-config/configs.
Structured nested settings
- Scenario: maintain grouped object/array settings (e.g., rule lists).
- Use: structured schema (
object/array) + nested JSON conversion utilities.
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
configsoption 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:
saveValuesdeletes 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.
