Skip to content

v2.0.0

Compare
Choose a tag to compare
@olivermrbl olivermrbl released this 23 Oct 07:04
· 158 commits to develop since this release

Medusa v.2.0.0

We’re excited to announce the release of Medusa 2.0 to the world today. This major version has been over 16 months in the making, with more than 3500 pull requests merged, and represents an incredible engineering feat by our team.

Medusa 2.0 is a complete rewrite of our architecture and feature set with breaking changes to many areas of Medusa 1.0. While we recognize this may be disruptive for our users, we deemed these changes necessary to establish the proper foundation for our vision of the future of building applications with Medusa.

Since this is a complete rewrite and we have yet to finalize an upgrade guide, there is no point in covering all the breaking changes in this announcement. You can expect this to be covered in the upgrade guide, which is published within the next two months. This post focuses on what’s new in Medusa 2.0. We will briefly cover the new architecture and commerce features, but otherwise, leave it to the documentation to educate about all the new concepts. Our documentation has also been rewritten and will, aside from tutorials, guides, and references, offer an in-depth learning path, equipping you with the knowledge needed to build bespoke commerce applications with Medusa.

Package restructuring

First, let’s understand the packages you need to build applications with Medusa 2.0. These haven’t changed much from 1.0, aside from some restructuring for a more logical separation of concerns.

There are three core packages in Medusa 2.0, and these are installed in new projects by default:

  • @medusajs/medusa
  • @medusajs/framework
  • @medusajs/admin-sdk

@medusajs/medusa

If you have been testing previews of Medusa 2.0, you know that we’ve gone through a few iterations of package management to figure out the most appropriate bundling of our commerce features. Eventually, we decided to stick with what we had in Medusa 1.0, a single package containing all commerce modules and the Rest API. Much of this code ships as separate npm packages (more about this in the section covering our modular architecture). However, they are all dependencies of @medusajs/medusa, which makes for a seamless upgrade path whenever new versions of underlying packages are published.

@medusajs/framework

We are excited to introduce a new package dedicated to our framework for customization. This package holds all the tooling needed to extend existing and/or introduce new functionality in Medusa projects. This includes API Routes, Workflows SDK, Modules SDK, Subscribers, Scheduled Jobs, Loaders, DML, and more. We will cover this in more depth in a later section.

@medusajs/admin-sdk

We have restructured our admin packages as part of the dashboard redesign. The commerce dashboard ships as @medusajs/dashboard and is a dependency of the core package @medusajs/medusa. The tooling to extend the dashboard, including UI Widgets and Routes, is now bundled in an Admin SDK package, @medusajs/admin-sdk.

Now that you understand the new packages, let’s move on to the most significant change in Medusa 2.0.


Architecture rewrite

The largest change from Medusa 1.0 to 2.0 is the rewrite of our core architecture. While architecture rewrites often have a tarnished reputation, we believe this decision aligns with how software engineering will evolve in the next decade. We will elaborate on this thinking in a separate blog post.

Complete isolation of domains

In Medusa 2.0, all business domains (services and data models) have been rewritten from scratch to eliminate interdependencies between them. To understand the reasoning behind this change, let’s briefly consider the previous architectural design.

In Medusa 1.0, services, e.g., Product and Cart, held most cross-domain business logic, and relationships between data models were defined via foreign keys in the database. The Cart service strictly depended on the Product service for many operations, e.g., adding a line item to the cart, and the line item data model references product variants via foreign keys. This pattern was applied to all domains in Medusa 1.0, and made it near-impossible to partially adopt our feature offering—it was all or nothing—a dealbreaker for some of our users, especially in the enterprise segment.

These interdependencies between domains significantly constrained the level of extensibility we could offer in the service layer. The only way to “extend” services was to override entire methods, which led to nasty upgrade paths whenever we upgraded those service methods with additional logic.

Medusa 2.0 eliminates all interdependencies between domains. Services are now pure in the sense that they only manage resources within their domain. All cross-domain functionality has been moved to extensible workflows, which will be covered in a later section. We’ve also eliminated all database-level dependencies, removing foreign keys between data models in different modules.

Architecture of Medusa 2.0

Medusa Architecture

Benefits of module isolation

We’ve already touched on some key benefits of module isolation. Over the past three years, we’ve seen a growing demand for incremental adoption, especially from large businesses. These businesses often have a sizeable existing tech stack with various integrations and custom applications. For them, a full migration can take years and cost millions. They need a platform that allows them to migrate their tech stack gradually while keeping the existing systems intact. Our new modular architecture makes that possible (and feasible).

Gradual adoption of modules

Medusa Architecture Gradual Adoption

A related benefit of module isolation is our new standalone mode. Not all companies need the full suite of features of a commerce platform. We’ve seen many requests for (and now usage of) standalone modules, which is a new “runtime” of modules in Medusa 2.0. Companies can install and use a few modules to build out their application. This is typically the preceding step to the gradual migration described above, where companies, over time, adopt more and more modules until they eventually leverage the full power of our platform.

For example, the Cart module can be used standalone to build a custom checkout flow:

import CartService from "@medusajs/cart"

const cart = await cartService.createCarts({
  email: "[email protected]",
  currency_code: "usd"
})

await cartService.addLineItem(cart.id. {
  title: "Custom item",
  unit_price: 1000,
  quantity: 1
})

In a common setup, line items in a cart are associated with products. However, that might not be the case for your use case. You may sell simpler goods that are not tied to a product variant or calculated price. All you need is plug-and-play cart management, and we offer you precisely that.

Services as a lower-level primitive

A non-obvious benefit from our architecture rewrite is that services have become a more useful lower-level primitive. As described above, we've removed all cross-domain business logic from services, limiting them to managing resources within their modules. So, when you use the Cart service to create a cart, you only create a cart. This sounds obvious, but in monolithic architectures, it’s common to carry out cross-domain operations within single service methods. For example, you might create shipping methods or populate the region as part of creating the cart. Such actions are typically achieved via dependency injection, which, in Medusa 2.0, is not available across modules.

Having more “dumb” services enables a greater level of composability. Modules can integrate more seamlessly, and how you integrate them is entirely up to you and your use case. Consider our previous example. Imagine you're not selling traditional products but rather subscriptions or licenses. Our Cart service doesn’t care. As long as you provide the required details to create line items, the Cart service and all its related functions, including total computation, will work as expected. It will also continue to work seamlessly with other modules, e.g. you can apply promotions to your license products with little to no changes needed. This is an example of the power of services as lower-level primitives and elegant abstractions.

Read more about the architectural changes in our documentation.


New and improved commerce features

As part of rewriting our commerce modules, we reevaluated each feature set to identify improvements. This led to various updates and new modules we are excited to introduce today.

Promotions engine

Our new Promotion module, @medusajs/promotion, lets you set up advanced conditional promotion logic. You can compute discounts based on coupons, cart items, customers, or custom data models. Additionally, we’ve introduced new types of promotions, such as Buy X and Get Y promotions.

Read more about the Promotions module here.

Advanced inventory management

Our new Inventory and Stock Location modules, @medusajs/inventory and @medusajs/stock-location, significantly improve inventory management in Medusa. With our Stock Location module, you can keep inventory in multiple warehouses worldwide, including physical stores, and associate those locations with shipping zones to ensure your fulfillment processes are optimized for distance to customers. With our new Inventory module, your product variants can share inventory items, enabling new use cases such as product bundles.

Read more about the Inventory and Stock Location modules here.

Flexible authentication

Our new authentication module, @medusajs/auth, is significantly more flexible than what we had in Medusa 1.0. It allows you to seamlessly introduce new authentication providers, such as Auth0, and ships with officially supported implementations for email-password, Google, and GitHub.

Here’s how the Google provider is configured:

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      options: {
        providers: [
          {
            resolve: "@medusajs/auth-google",
            options: {
	      clientID: process.env.GOOGLE_CLIENT_ID,
	      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
              callbackURL: process.env.GOOGLE_CALLBACK_URL,
             }
           }		
         ]
       } 
     }
   ]
})

Furthermore, this new authentication primitive allows you to add new authenticated identity groups, reusing all the core authentication logic.

For example, imagine we want to introduce employees and let them sign up from the storefront. We create a new authenticated endpoint in the Store API for creating employees. Then, we plug in the authenticate middleware to accept employees as valid actors.

// src/api/middlewares.ts

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      matcher: "/store/employees",
      middlewares: [
        authenticate(["employee"], "bearer", {
          allowUnregistered: true,
        }),
      ],
    },
  ]
})

This example assumes you’ve created a new data model Employee. You’ll learn more about this in the DML section of the release notes.

And just like that (and a few other steps), your employees are authenticated similarly to your customers. The full tutorial for this setup can be found here.

In addition to these three highlights, we have introduced many new improvements across our modules. Read more about these in our documentation.


Custom Modules

As described previously, Medusa 2.0 has a more flexible architecture by eliminating interdependencies between modules. Aside from the benefits outlined earlier, this flexibility also makes custom modules simpler to create. The development process is based on a range of new tools in our framework, which we’ll briefly describe in the following sections.

Data Modeling Language (DML)

We are excited to introduce a new tool, DML, for defining data models. The tool reduces decision fatigue by eliminating considerations around the ORM, defining relationships, column types, and more.

The DML is used as follows:

import { model } from "@medusajs/framework"

const Brand = model.define("brand", {
  id: model.id({ prefix: "br_" }),
  name: model.text(),
  description: model.text().nullable(),
})

Read more about the DML in our documentation.

Medusa Service factories

Querying and mutating data models previously required a full service implementation, which was a lot of grunt work just to get started. In Medusa 2.0, we’ve introduced MedusaService that gives you all the standard methods for managing your data models out of the box. This includes create, retrieve, update, delete, list, upsert, and a few more.

Read more about the service factories in our documentation.

Your module service should extend the MedusaService and specify the data models for which methods should be created:

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

export default BrandService extends MedusaService({ Brand }) {}

The MedusaService then generates partially type-safe methods on the class:

const brandModule = container.resolve("brand")
const brand = await brandModule.createBrands({ name: "Medusa Running" })

Read more about modules in our documentation.

Module Linking

Eliminating database-level dependencies between modules doesn’t come without tradeoffs. Cross-domain references no longer leverage traditional database foreign keys, so we’ve introduced our own mechanism, Module Linking, for creating relationships between two models (from two different modules).

The defineLink utility lets you link two data models and has semantics similar to those of relationship APIs in ORMs:

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

// "one-to-one"
export default defineLink(
  ProductModule.linkable.product,
  BrandModule.linkable.brand
)

// "many-to-one"
export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true
  }
  BrandModule.linkable.brand
)

Read more about Module Linking in our documentation.

Custom modules in Medusa 2.0 play a critical role in introducing new functionality in your application. Therefore, we recommend reading through the learning path to grasp the new concepts and tools fully.


Workflows

In Medusa 2.0, cross-domain business logic has been largely rewritten to adapt to our new modular architecture. Since domains no longer interact directly with each other at the service level, we have created a higher-level primitive to piece together module operations.

We are excited to introduce Workflows in Medusa 2.0.

const workflowName = "accept-quote"

const acceptQuoteWorkflow = createWorkflow(workflowName, function (input) {
  const cart = retrieveCart(input.cartQuoteId);
  reserveInventory(cart);
  authorizePayment(cart);
  createOrder(cart);
});

All business logic in Medusa 2.0 is built with Workflows, e.g., add to cart, complete cart, create fulfillment, etc. Workflows are composed of steps. Steps are reusable pieces of logic, typically isolated to a single module, and can be seen as atomic operations. If any steps fail, the entire workflow is rolled back. A rollback triggers compensating actions for each step that reverts the corresponding successful action previously run.

const stepName = "reserve-inventory"

const reserveInventory = createStep(stepName, async (cart, context) => {
    const inventoryService = context.container.resolve("inventoryService");
    await inventoryService.reserveInventory(cart.items);
    return new StepResponse({ success: true }, cart.items);
  },
  async (items, context) => {
    const inventoryService = context.container.resolve("inventoryService");
    await inventoryService.releaseInventory(items);
  }
);

Workflows are extensible by nature. Steps can be added, removed, and replaced, which opens up full customization of all pre-built opinionated business logic in Medusa. Extensions are injected via Workflow Hooks. Initially, only a few select workflows can be extended; however, we expect to introduce Hooks across all workflows in follow-up releases.

Read more about Workflows and Workflow Hooks in our documentation.

Long-running Workflows

Workflows have durable executions, which enable a range of new use cases, including long-running operations. A long-running Workflow is an asynchronous series of steps that can span hours, days, or weeks and support humans in the loop. For example, you can build a Workflow to manage the preparation of new products for sale. The Workflow starts when a product is inbounded at your warehouse. Then, tasks can kick off with taking pack shots, writing titles and descriptions, translating copy, and merchandizing the product. As each task is completed, the Workflow can proceed. All tasks would be mapped as Workflow Steps and could be completed in systems across your stack. If a task fails to complete or a configurable timeout is reached, rollback logic can help keep your data consistent or notify responsible parties.

const workflowId = "prepare-products-for-sale"
const prepareProductsForSale = createWorkflow(workflowId, function (input) {
  awaitPackShotsApprovals() // async, requires human-in-the-loop
  merchandizeProducts() // async, requires human-in-the-loop
  publishProducts()
})

Workflows are not built specifically for commerce operations. Whenever your stack has multiple systems interfacing with each other, Workflows is a useful primitive. For example, we use Workflows to provision infrastructure for Medusa Cloud, which comprises a range of long-running asynchronous operations across many systems, including third-party services.

Read more about Durable Workflows in our documentation.


Query

Similar to how Workflows tie together cross-domain logic, we have had to rethink the query mechanism in Medusa 2.0. We can no longer expand relations with traditional ORM tooling, as those relations (across domains) no longer exist.

We are excited to introduce Query in Medusa 2.0. Initially, Query replaces the mechanism for expanding cross-domain relations. In the future, we plan to improve the query engine to make it an immensely powerful tool for working with data in your application, more on that later.

const query = container.resolve("query")

const { data } = query.graph({
  entity: "product",
  fields: ["id", "brand.*"]
})

// console.log(data[0])
// { id: "prod_1234", brand: { id: "br_1234", name: "Medusa Running" } }

Upon booting your Medusa application, our loaders read all modules in your application and identify data models and links between data models. This information is used to build an internal graph representation of the data living within your application. Query uses this graph to query data across modules.

Read more about Query in our documentation.

Admin redesign

We are incredibly excited to present a fully redesigned admin dashboard in Medusa 2.0. The dashboard has been rewritten from scratch and introduces new UI and UX across all domains.

Explore the redesign in our demo here.

Aside from being redesigned, our dashboard is more customizable than ever. You can add custom UI Widgets and Routes to support your bespoke business workflows and get the full picture of your commerce operation.

import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Product Widget</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

Finally, the admin build tooling has been migrated to Vite to improve the developer experience and gain access to a thriving ecosystem of developers and plugins.

Read more about Admin in our documentation.


Framework for customization

Lastly, we are excited to announce our fully-fledged backend framework for building powerful customizations in Medusa applications. We’ve already covered many of the framework's tools in previous sections, so the following is a rundown of the tools not mentioned in previous sections.

API Routes

Expose endpoints in your Medusa application with API Routes, allowing you to run custom business logic, listen to webhooks, and more.

export const POST = async (req, res) => {
  const query = req.scope.resolve("query")
  
  const { data } = query.graph({
    entity: "product",
    fields: ["id", "brand.*"]
  })

  res.json({ product: data[0] })
}

Read more about API Routes in our documentation.

Subscribers

Use Medusa’s built-in event system to subscribe and respond to events like order.placed, product.created, and more with Subscribers.

export default async function productCreateHandler({ event }) {
  const productId = event.data.id
  console.log(`The product ${productId} was created`)
}

export const config: SubscriberConfig = {
  event: "product.created",
}

Read more about Subscribers in our documentation.

Scheduled Jobs

Perform scheduled jobs to automate repetitive work on a recurring basis.

import { syncProductsWorkflow } from "@workflows"

export default function syncProductsJob({ container }) {

  await syncProductsWorkflow.run(container)
  
}

export const config = {
  name: "sync-products-midnight",
  schedule: "0 0 * * *",
}

Read more about Scheduled Jobs in our documentation.

Middlewares

Introduce middleware functions with our new defineMiddlewares utility.

// src/api/middleswares.ts

import { defineMiddlewares } from "@medusajs/medusa"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (req, res, next) => {
          console.log("Received a request!")
          next()
        },
      ],
    },
  ],

Read more about middleware in our documentation.

Medusa Config

Configure your Medusa server at ease with our new type-safe helper in medusa-config.js.

import { defineConfig, loadEnv } from "@medusajs/framework/utils";

loadEnv(process.env.NODE_ENV || "development", process.cwd());

module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    redisUrl: process.env.REDIS_URL,
    http: {
      storeCors: process.env.STORE_CORS!,
      adminCors: process.env.ADMIN_CORS!,
      authCors: process.env.AUTH_CORS!,
      jwtSecret: process.env.JWT_SECRET,
      cookieSecret: process.env.COOKIE_SECRET,
    },
  },
});

Additionally, we’ve added TypeScript support to allow converting the config file to a ts-file.

As described in previous sections, our framework also includes:

  • Data Modeling Language (DML)
  • Modules
  • Workflows
  • Query
  • UI library

Read more about our Framework in our documentation.


Upgrading to Medusa 2.0

An upgrade guide with step-by-step instructions and a full list of breaking changes will be published within the following months.