A new module for Medusa: plugins simply declare their settings, and admins manage them in the Admin, with no medusa-config edits and no redeploys.

At Gorgo, we've been building Medusa plugins for over a year, plugins that connect stores to payment providers, fulfillment services, and ERP systems. The same question kept coming up in every one of them: how do you pass a plugin its parameters, and how do you manage them? API keys, connection settings, and other user configuration are usually set through and environment variables, so any change requires a code edit and a redeploy. To avoid that, every plugin has to build its own UI, wire up an API, validate fields, and write workflows.
Today we're announcing the Integration Module for Medusa, which takes this chore away. Plugins simply declare their parameters in the module's provider, and the store admin manages them in the Admin through a generated interface, with no edits and no redeploys.
Configuring a Medusa plugin usually comes down to two problems, and both are harder than they should be.
The Integration Module moves plugin settings out of code and into the Admin and the database. A plugin describes the settings it needs, a store admin fills them in under Settings → Integrations, and at runtime the plugin reads ready, validated values.
As an author, you only describe the settings schema; the module takes care of the UI, storage, encryption, and validation. And if a plugin needs something the generated UI doesn't support, you can create a custom widget.
You declare a descriptor with that lists your options and their types, the sections they group into, and the validation rules. From that single declaration, the Admin generates the settings page: the right control for each field, validation, conditional visibility, and masked secrets. You don't have to write any UI, CRUD API, or the associated workflows.
A minimal provider is a descriptor plus the class that ships it:
1import { AbstractIntegrationProvider, defineIntegration } from "@gorgo/medusa-integration"2
3const descriptor = defineIntegration({4 category: "payment",5 displayName: "acme.name",6 supportsMultipleInstances: true,7
8 options: {9 apiKey: {10 type: "string",11 required: true,12 secret: true,13 label: "acme.fields.apiKey",14 },15 mode: {16 type: "enum",17 values: ["test", "live"],18 default: "test",19 label: "acme.fields.mode",20 },21 },22
23 sections: [24 {25 id: "credentials",26 title: "acme.sections.credentials",27 options: ["apiKey", "mode"]28 },29 ],30
31 testConnection: async ({ options }) => {32 const ok = await pingAcme(options.apiKey, options.mode)33 return ok ? { status: "passed" } : { status: "failed", message: "Invalid API key" }34 },35})36
37export class AcmeIntegrationProvider extends AbstractIntegrationProvider {38 static identifier = "acme"39 get descriptor() {40 return descriptor41 }42}Values like are i18n keys, resolved through Medusa's standard localization, and the translations themselves live in .
Next, register the provider in :
1const ACME_INTEGRATION_ID = "acme-1"2
3module.exports = defineConfig({4 plugins: [5 {6 resolve: "@acme/medusa-payment-acme",7 options: {},8 },9 {10 resolve: "@gorgo/medusa-integration",11 options: {12 encryptionKey: process.env.INTEGRATION_ENCRYPTION_KEY,13 providers: [14 {15 resolve: "@acme/medusa-payment-acme/providers/integration-acme",16 id: ACME_INTEGRATION_ID,17 },18 ],19 },20 },21 ],22 modules: [23 {24 resolve: "@medusajs/medusa/payment",25 options: {26 providers: [27 {28 resolve: "@acme/medusa-payment-acme/providers/payment-acme",29 id: "acme",30 options: {31 id: ACME_INTEGRATION_ID // matches the integration provider id above32 },33 },34 ],35 },36 },37 ],38})Here sets the integration instance key. The same has to be passed to the consumer, a payment provider in this example. That's what the module uses to find and return the settings of the right integration. The two s are declared in independent places in the config, so you have to keep them in sync by hand, and it's easier to do that through a constant.
(here ) is the AES-256-GCM key the module uses to encrypt secrets at rest. Set it in your environment and keep it stable, since changing or losing it makes stored secrets unreadable.
At runtime, the plugin reads its parameters anywhere it runs with and gets back a typed, decrypted object. Incomplete or disabled settings don't resolve, so a half-filled draft is never returned.
1import { resolveIntegrationOptions } from "@gorgo/medusa-integration"2
3abstract class AcmeBase extends AbstractPaymentProvider {4 constructor(options) {5 super(options)6 const { apiKey, mode } = await resolveIntegrationOptions({7 identifier: "acme",8 instance_id: options.id9 })10 }11
12 // ...13}One descriptor gives your plugin a complete settings interface:
If you already maintain a Medusa plugin, moving to the Integration Module comes down to a simple port: move your existing options into the module provider's descriptor. And you read them with where you currently read environment variables or . We migrated all of our own providers this way (payments, fulfillment, and ERP), and you can browse a complete Medusa app with every provider wired up in the repository.
The step-by-step guide walks you through the whole migration.
The module also displays a catalog of available integrations right in the Admin. Store admins can find your plugin and install it with a short guide. And because the settings UI is generated, your plugin looks clean and native, in the same style as the Medusa Admin.
Install the module:
npm install @gorgo/medusa-integrationFrom there, declare your plugin's settings and other metadata, register it as an Integration Module provider, and switch it to read its parameters with . The full installation and usage guide is in the documentation, and the source is on GitHub.
The Integration Module is in beta, and we're actively developing it, adding built-in integrations and expanding the catalog. If you're building a Medusa plugin, we'll help you move over. If you run a Medusa store, give it a try and tell us what's missing.
Join the discussion in the Medusa Telegram community. Issues and Pull Requests are always welcome in the repository on GitHub.