Create a Plugin
In this chapter, you'll learn how to create a MedusaGo plugin, test it in an application, and publish it.
A plugin is a Go package of reusable MedusaGo customizations that you can use in any MedusaGo application. By creating and publishing a plugin, you can reuse your customizations across multiple projects or share them with the community.
The chapter builds a small wishlist plugin as an example. It has a module with a wishlist table, a link to customers, an API route, a subscriber, a workflow hook, and a scheduled job.
1. Create a Plugin Project#
A plugin is a Go module. Create a directory for it, and initialize the module with the name you'll publish it under, which is the import path of its main package:
mkdir medusa-plugin-wishlist
cd medusa-plugin-wishlist
go mod init github.com/acme/medusa-plugin-wishlist
go get github.com/hitkey-io/medusago@latest
create-medusago-app installer, and the plugin commands of the Medusa CLI (plugin:build, plugin:develop, plugin:publish, plugin:add, and plugin:db:generate) aren't supported. You don't need them: a Go module doesn't have a build step, and you publish and install it with the Go toolchain.Plugin Directory Structure#
The main package of the plugin is at the root of the module. Its customizations are in the following sub-directories, which mirror the structure of a MedusaGo project:
src/api: contains the API routes and middlewares of the plugin.src/jobs: contains scheduled jobs.src/links: contains module links.src/modules: contains modules.src/subscribers: contains subscribers.src/workflows: contains workflows. You can also add hooks undersrc/workflows/hooks.
Unlike in a project, MedusaGo doesn't scan these directories. You list what the plugin contains in its definition, as explained in the next section.
2. Define the Plugin#
The plugin registers a plugins.Definition value with the framework. It describes the plugin's customizations. Create the file plugin.go at the root of the plugin with the following content:
// Name is the name of the plugin: the `Resolve` value of its entry in the
// `Plugins` of the application's configuration.
const Name = "github.com/acme/medusa-plugin-wishlist"
// Plugin is the plugin's definition.
var Plugin = &plugins.Definition{
Name: Name,
Version: "1.0.0",
// the modules of src/modules
Modules: []plugins.Module{
{Dir: "wishlist", Exports: &wishlistmodule.Module.ModuleExports},
},
// the links of src/links
Links: links.Define,
// the API routes and middlewares of src/api
Routes: api.Routes(),
Middlewares: api.Middlewares(),
// the subscribers of src/subscribers
Subscribers: subscribers.All(),
// runs once, when the plugin is loaded: consume workflow hooks and
// register scheduled jobs here
Load: func() {
hooks.Register()
jobs.Register()
},
}
A plugins.Definition has the following properties:
Property | Description |
|---|---|
| The plugin's name. An application uses it as the |
| The plugin's version. |
| The plugin's modules. |
| A function that defines the plugin's module links with |
| The plugin's API routes and middlewares, in the same form as the |
| The plugin's subscribers. Each has the |
| A function that runs once when the plugin is loaded. Use it to consume workflow hooks and to register scheduled jobs. |
Then, register the definition when the package is imported:
An application that imports the package registers the plugin. The plugin's customizations are only added to the application if it also lists the plugin in the Plugins of its configuration.
3. Create a Module#
A module in a plugin is a module like the ones in a project. For example, create the file src/modules/wishlist/models.go with the following content:
package wishlist
import "github.com/hitkey-io/medusago/packages/core/utils/src/dml"
// Wishlist is the data model of the `wishlist` table.
var Wishlist = dml.Define("wishlist", dml.Schema{
{Name: "id", Property: dml.Id().PrimaryKey()},
{Name: "customer_id", Property: dml.Text()},
})
Then, create its definition in src/modules/wishlist/index.go:
// Module is the module's definition. The options of the plugin are passed
// to the module in the options of its service.
var Module = utilsmodulessdk.Module(ModuleName, utilsmodulessdk.ModuleDefinition{
Models: []*dml.Entity{Wishlist},
Migrations: migrations.Migrations,
Service: func(opts modulessdk.ServiceOptions, base *utilsmodulessdk.MedusaService) (any, error) {
service := &WishlistModuleService{MedusaService: base, MaxItems: 100}
if max, ok := opts.Options["max_items"].(int); ok {
service.MaxItems = max
}
return service, nil
},
})
The module is registered in the application with the key of its service, which is the name you pass to Module (wishlist). It's available under this key in the container.
Plugin Options#
The options an application sets for the plugin are passed to every module of the plugin, in the Options of the service's options. For example, the module above reads a max_items option. A module reads an option with a type assertion on the Options map, so check the value's type, and keep a default for an option the application doesn't set. If the application sets the options in medusa-config.json or medusa-config.yaml, a number is a float64.
Generate Migrations#
The migrations of a plugin's modules run when the application runs go run . db:migrate. To generate them, you need a MedusaGo application in which the module is a project module, since db:generate only generates migrations for the modules that are resolved from a local path.
So, create a development application in a dev directory of the plugin, with a main.go and a medusa-config.go like any project. In its configuration, add the module with the path of its directory, relative to the dev directory:
Modules: []config.ModuleConfig{
{
// the path of the module's directory, relative to the dev directory
Resolve: "../src/modules/wishlist",
Module: &wishlistmodule.Module.ModuleExports,
},
},
Then, set the DATABASE_URL of the development application to a PostgreSQL database, and run the following command from the dev directory:
It generates the migrations in src/modules/wishlist/migrations. Add them to the module's definition with the Migrations property, as shown in the module's definition above. The development application must not list the plugin in its Plugins, as the plugin would register the module twice.
4. Create Other Customizations#
You create the other customizations of the plugin as in a project, then add them to its definition.
Links#
Define the plugin's links in a function with DefineLink, and set it as the Links property of the definition:
// Define defines the links of the plugin: a wishlist belongs to a
// customer. The link is read-only, as the wishlist stores the customer's ID.
func Define() error {
customer := utilsmodulessdk.LinkableOf(&modules.Customer)["customer"].ToJSON()
_, err := utilsmodulessdk.DefineLink(
utilsmodulessdk.DefineLinkInput{Linkable: wishlist.Module.Linkable["wishlist"].ToJSON(), Field: "customer_id"},
customer,
utilsmodulessdk.DefineLinkOptions{ReadOnly: true},
)
return err
}
API Routes and Middlewares#
Create a function that returns the []medusahttp.RouteFile of the plugin's routes, and one that returns its []medusahttp.MiddlewareRoute. The paths of the routes are complete, for example /store/wishlists:
// listWishlists lists the wishlists of the authenticated customer.
func listWishlists(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
var customerID any
if req.AuthContext != nil {
customerID, _ = req.AuthContext.Get("actor_id")
}
if customerID == nil || customerID == "" {
return common.NewMedusaError(common.MedusaErrorTypes.UNAUTHORIZED, "Customer authentication is required")
}
service := req.Scope.Resolve(wishlist.ModuleName).(*wishlist.WishlistModuleService)
filters := dal.NewRecord()
filters.Set("customer_id", customerID)
wishlists, err := service.Call(req.Ctx(), "listWishlists", []any{filters})
if err != nil {
return err
}
out := dal.NewRecord()
out.Set("wishlists", wishlists)
return res.JSON(out)
}
// Routes are the API routes of the plugin, like the route files of src/api.
func Routes() []medusahttp.RouteFile {
return []medusahttp.RouteFile{
{Path: "/store/wishlists", Route: medusahttp.Route{GET: listWishlists}},
}
}
// Middlewares are the middlewares of the plugin, like src/api/middlewares.
func Middlewares() []medusahttp.MiddlewareRoute {
return nil
}
Subscribers#
Create plugins.Subscriber values with the events to subscribe to, and a handler. The handler receives the container as a plugins.Resolver:
// All are the subscribers of the plugin.
func All() []plugins.Subscriber {
return []plugins.Subscriber{customerCreated}
}
// customerCreated creates a wishlist for every new customer.
var customerCreated = plugins.Subscriber{
Events: []string{"customer.created"},
Handler: func(ctx context.Context, c plugins.Resolver, event eventbus.Event) error {
data, _ := event.Data.(*dal.Record)
if data == nil {
return nil
}
customerID, _ := data.Get("id")
service := c.Resolve(wishlist.ModuleName).(*wishlist.WishlistModuleService)
wishlistData := dal.NewRecord()
wishlistData.Set("customer_id", customerID)
_, err := service.Call(ctx, "createWishlists", []any{wishlistData})
return err
},
}
Workflow Hooks#
Workflows are Go values, so an application that wants to run a workflow of your plugin imports it from the plugin's package. To consume the hooks of MedusaGo's workflows, call RegisterHook in the Load function:
// Register consumes the hooks of the plugin: it runs once, when the
// plugin is loaded.
func Register() {
customerworkflows.CreateCustomersWorkflow.RegisterHook("customersCreated",
func(ctx composer.StepExecutionContext, data any) (composer.StepResponse[any], error) {
logger := ctx.Container.Resolve(common.ContainerRegistrationKeys.LOGGER).(*slog.Logger)
logger.Info("Wishlist plugin: customers created")
return composer.NewStepResponse[any](nil), nil
}, nil)
}
Scheduled Jobs#
A scheduled job is registered at runtime, so register it in the Load function too:
// Register registers the scheduled jobs of the plugin.
func Register() {
_, err := frameworkjobs.Register(
&frameworkjobs.CronJobConfig{Name: "wishlist-cleanup", Schedule: "0 0 * * *"},
func(ctx context.Context, container composer.Container, jc frameworkjobs.ScheduledJobContext) (any, error) {
slog.Info("Cleaning up the wishlists", "scheduled_for", jc.ScheduledFor)
return nil, nil
},
)
if err != nil {
slog.Error("Unable to register the job", "error", err)
}
}
Load function runs once per process, when the plugin is loaded. A plugin's Load only runs for an application that lists the plugin in its configuration.5. Test the Plugin in an Application#
To test the plugin, use it in a MedusaGo application. You can create one with the create-medusago-app installer.
In the application's directory, add the plugin to its dependencies. While you develop it, use a replace directive that points to the plugin's directory:
go mod edit -replace github.com/acme/medusa-plugin-wishlist=../medusa-plugin-wishlist
go get github.com/acme/medusa-plugin-wishlist
Then, in the application's main.go, import the plugin's package for its side effect, which registers the plugin: import _ "github.com/acme/medusa-plugin-wishlist".
Finally, add the plugin to the Plugins of the application's configuration, in medusa-config.go, with the options you want:
Plugins: []config.PluginConfig{
{
Resolve: "github.com/acme/medusa-plugin-wishlist",
Options: map[string]any{
"max_items": 50,
},
},
},
Resolve is the plugin's Name. If an application lists a plugin that isn't imported, it fails to start with an error saying it can't resolve the plugin.
Run the migrations of the plugin's modules, then start the application:
db:migrate also syncs the plugin's module links. Your plugin's API routes, subscribers, and hooks are now part of the application.
6. Publish the Plugin#
A plugin is published as a Go module. Push it to a Git repository whose URL matches the module's name, then tag a release:
An application then installs the version with go get:
The application's dependencies include the MedusaGo version the plugin requires, or a later one if the application uses a later version. Test the plugin with the versions of MedusaGo you want to support.
Update a Plugin#
To update a plugin in an application, run go get with the new version, then run the migrations: