Blog

July 22, 2025·Product

Building Medusa with Medusa: Billing

Oliver Juhl

Oliver avatar

Oliver Juhl

Explore how we built the subscription billing engine behind Medusa Cloud using our own Commerce Modules and Framework.

Image modal

At Medusa, we’re building Medusa with Medusa.

Six months ago, we launched our hosting platform. At the time, I shared a post explaining how we used our Framework to build our platform, focusing on how Workflows provision infrastructure for customer workloads. If you haven’t read it, I recommend starting there.

Our platform helps teams build and deploy unique commerce experiences fast. Connect your project via GitHub and push to deploy. Within minutes, you have a production-ready environment running on infrastructure optimized for Medusa applications.

A lot has happened since then. Hundreds of customer applications have been deployed to our platform, including those of billion-dollar companies. As our customer base grew, so did the complexity and requirements of our internal operations, especially around managing plans, subscriptions, and pricing.

In this post, I will share how we used our Commerce Modules and Framework to build our entire billing system; the same tools used by our users to build custom commerce experiences.

How it works

Before we dive into the implementation details, here’s a brief overview of how our billing system works.

  1. A customer signs up on
  2. During their onboarding, they choose a plan
  3. Plans have a price
  4. Customers are billed on a monthly basis
  5. Every month, we create an order, charge the card on file, and send out an invoice

While the flow is simple on the surface, it involves many moving pieces, all of which must function reliably to avoid overcharging, undercharging, double charging, and more. This includes managing billing cycles, running background jobs for issuing charges, and integrating with third-party payment providers. Building such a system is not trivial.

However, this is where Medusa shines.

Digital commerce experiences are everywhere. Even in products you would not traditionally think of as commerce-related. Our platform is a good example of this. Despite being a hosting and developer platform, we still need orders, customers, payments, refunds, and discounts to manage and use for our customer subscriptions.

With Medusa’s commerce primitives and framework for customization, we built our entire billing system with a relatively low code footprint.

Let’s dig in.

Defining Billing concepts with DML

Subscriptions and plans are commerce-adjacent concepts but are not part of Medusa’s default offering. To introduce these concepts, we created a billing module with new data models using our Data Model Language:

import { model } from "@medusajs/framework/utils"
export default model.define("subscription", {
id: model.id({ prefix: "sub" }).primaryKey(),
organization_id: model.text(),
plan: model.belongsTo(() => Plan),
current_period_start: model.dateTime(),
current_period_end: model.dateTime(),
started_at: model.dateTime(),
})
import { model } from "@medusajs/framework/utils"
export default model.define("plan", {
id: model.id({ prefix: "plan" }).primaryKey(),
name: model.text().searchable(),
subscriptions: model.hasMany(() => Subscription, {
mappedBy: "plan",
}),
})
Data models have been simplified for the sake of the post

When you create data models with our Framework, services with CRUD functionality are autogenerated under the hood. These are the services used in workflows alongside services from core modules to compose the business logic of the billing system.

Creating subscriptions with Workflows

With the data models in place, we can now create customer subscriptions and plans.

Let’s look at how we onboard a new customer:

export const createOrganisationSubscriptionWorkflow = createWorkflow(
"create-organisation-subscription-workflow",
(input) => {
const plan = retrieveEntityStep({
entity: "plan",
fields: ["id"],
filters: { id: input.plan_id },
})
const subscriptionInput = transform({ plan }, (data) => ({
plan_id: data.plan.id
}))
const subscription = createSubscriptionStep(subscriptionInput)
createRemoteLinkStep([
{
organization: {
organization_id: input.organization_id,
},

In this workflow above, we pass the selected plan ID and the customer’s organization ID to create a new subscription. The created subscription is then associated with the customer’s organization using our links. Links are a tool to create relations between entities of two separate and isolated modules. You can read more about them here.

Once this workflow completes, the customer is subscribed to a plan, and our system begins collecting payments automatically.

Powering monthly charges with Scheduled Jobs

Payments are a core part of Medusa’s default offering, so we didn’t need to build much to start collecting payments. All the required data models, domain logic, and third-party integrations were already in place. We simply installed the Payment and Order modules in our platform application and built workflows to handle the monthly charges.

The end-to-end payment collection flow involves many steps and nested workflows, so for the sake of this post, we will only cover the high-level flow of events:

  1. Fetch all active subscriptions that have reached the end of their billing cycle
  2. For each subscription
    1. Create an order
    2. Create a payment collection
  3. For each payment collection
    1. Fetch the customer’s saved payment method
    2. Create a payment session
    3. Capture the payment
  4. For each order
    1. Mark it as paid

All of these operations were already part of Medusa’s core. We just remixed them into a new workflow tailored to subscription billing.

The workflow is triggered daily using our Scheduled Jobs. The job identifies and processes subscriptions that have reached the end of their billing period:

const EVERY_5_MINUTES = 5 * 60 * 1000
export default async function collectSubscriptionPayments(container) {
await collectSubscriptionPaymentsWorkflow(container).run({
input: {
collectBeforeDate: new Date().getTime()
}
})
}
export const config = {
name: "collection-subscription-payments",
schedule: {
interval: EVERY_5_MINUTES,
},
}

These are the most critical flows in our billing system. Not that complex, right?

Enabling custom commerce experiences

Our core Commerce Modules were originally built to power conventional commerce applications, such as the DTC model. However, in Medusa 2.0, we doubled down on redesigning our module architecture and building tools to expand digital commerce to include more custom commerce experiences.

You can mix and match our existing modules with your own business logic to create bespoke commerce applications, similar to what we have done with our billing system. This not only saves you and us from building a lot of boilerplate code, but we also get discounts, taxes, refunds, store credits, gift cards, and more out of the box.

If you are curious to know more about Medusa Cloud, you can sign up and book a demo here.

Share this post

Ready to build your custom commerce setup?