Subscriptions Recipe

This document provides an example of implementing the subscription recipe.

NoteYou can implement the subscription recipe as you see fit for your use case. This is only an example of one way to implement it.

Features#

By following this example, you’ll have a subscription commerce store with the following features:

  1. Subscription-based purchases for a specified interval (monthly or yearly) and period.
  2. Customize the admin dashboard to view subscriptions and associated orders.
  3. Automatic renewal of the subscription.
  4. Automatic subscription expiration tracking.
  5. Allow customers to view and cancel their subscriptions.
Subscription Example Repository
Find the full code for this recipe example in this repository.
OpenApi Specs for Postman
Imported this OpenApi Specs file into tools like Postman.

Step 1: Create Subscription Module#

Medusa creates commerce features in modules. For example, product features and data models are created in the Product Module.

You also create custom commerce data models and features in custom modules. They're integrated into the Medusa application similar to Medusa's modules without side effects.

So, you'll create a subscription module that holds the data models related to a subscription and allows you to manage them.

Create the directory src/modules/subscription.

Create Data Models#

Create the file src/modules/subscription/models/subscription.ts with the following content:

src/modules/subscription/models/subscription.ts
1import { model } from "@medusajs/framework/utils"2import { SubscriptionInterval, SubscriptionStatus } from "../types"3
4const Subscription = model.define("subscription", {5  id: model.id().primaryKey(),6  status: model.enum(SubscriptionStatus)7    .default(SubscriptionStatus.ACTIVE),8  interval: model.enum(SubscriptionInterval),9  period: model.number(),10  subscription_date: model.dateTime(),11  last_order_date: model.dateTime(),12  next_order_date: model.dateTime().index().nullable(),13  expiration_date: model.dateTime().index(),14  metadata: model.json().nullable(),15})16
17export default Subscription

This creates a Subscription data model that holds a subscription’s details, including:

  • interval: indicates whether the subscription is renewed monthly or yearly.
  • period: a number indicating how many months/years before a new order is created for the subscription. For example, if period is 3 and interval is monthly, then a new order is created every three months.
  • subscription_date: when the subscription was created.
  • last_order_date: when the last time a new order was created for the subscription.
  • next_order_date : when the subscription’s next order should be created. This property is nullable in case the subscription doesn’t have a next date or has expired.
  • expiration_date: when the subscription expires.
  • metadata: any additional data can be held in this JSON property.

Notice that the data models use enums defined in another file. So, create the file src/modules/subscription/types/index.ts with the following content:

src/modules/subscription/types/index.ts
1export enum SubscriptionStatus {2  ACTIVE = "active",3  CANCELED = "canceled",4  EXPIRED = "expired",5  FAILED = "failed"6}7
8export enum SubscriptionInterval {9  MONTHLY = "monthly",10  YEARLY = "yearly"11}

Create Main Service#

Create the module’s main service in the file src/modules/subscription/service.ts with the following content:

src/modules/subscription/service.ts
1import { MedusaService } from "@medusajs/framework/utils"2import Subscription from "./models/subscription"3
4class SubscriptionModuleService extends MedusaService({5  Subscription,6}) {7}8
9export default SubscriptionModuleService

The main service extends the service factory to provide data-management features on the Subscription data model.

Create Module Definition File#

Create the file src/modules/subscription/index.ts that holds the module’s definition:

src/modules/subscription/index.ts
1import { Module } from "@medusajs/framework/utils"2import SubscriptionModuleService from "./service"3
4export const SUBSCRIPTION_MODULE = "subscriptionModuleService"5
6export default Module(SUBSCRIPTION_MODULE, {7  service: SubscriptionModuleService,8})

This sets the module’s name to subscriptionModuleService and its main service to SubscriptionModuleService.

Register Module in Medusa’s Configuration#

Finally, add the module into medusa-config.ts:

medusa-config.ts
1module.exports = defineConfig({2  // ...3  modules: [4    {5      resolve: "./src/modules/subscription",6    },7  ],8})

Further Read#


Modules are isolated in Medusa, making them reusable, replaceable, and integrable in your application without side effects.

So, you can't have relations between data models in modules. Instead, you define a link between them.

Links are relations between data models of different modules that maintain the isolation between the modules.

In this step, you’ll define links between the Subscription Module’s Subscription data model and the data models of Medusa’s commerce modules:

  1. Link between the Subscription data model and the Cart Module's Cart model.
  2. Link between the Subscription data model and the Customer Module's Customer model.
  3. Link between the Subscription data model and the Order Module's Order model.

To link a subscription to the cart used to make the purchase, create the file src/links/subscription-cart.ts with the following content:

src/links/subscription-cart.ts
1import { defineLink } from "@medusajs/framework/utils"2import SubscriptionModule from "../modules/subscription"3import CartModule from "@medusajs/medusa/cart"4
5export default defineLink(6  SubscriptionModule.linkable.subscription,7  CartModule.linkable.cart8)

This defines a link between the Subscription data model and the Cart Module’s Cart data model.

TipWhen you create a new order for the subscription, you’ll retrieve the linked cart and use the same shipping and payment details the customer supplied when the purchase was made.

To link a subscription to the customer who purchased it, create the file src/links/subscription-customer.ts with the following content:

src/links/subscription-customer.ts
1import { defineLink } from "@medusajs/framework/utils"2import SubscriptionModule from "../modules/subscription"3import CustomerModule from "@medusajs/medusa/customer"4
5export default defineLink(6  {7    linkable: SubscriptionModule.linkable.subscription,8    isList: true,9  },10  CustomerModule.linkable.customer11)

This defines a list link to the Subscription data model, since a customer may have multiple subscriptions.

To link a subscription to the orders created for it, create the file src/links/subscription-order.ts with the following content:

src/links/subscription-order.ts
1import { defineLink } from "@medusajs/framework/utils"2import SubscriptionModule from "../modules/subscription"3import OrderModule from "@medusajs/medusa/order"4
5export default defineLink(6  SubscriptionModule.linkable.subscription,7  {8    linkable: OrderModule.linkable.order,9    isList: true,10  }11)

This defines a list link to the Order data model since a subscription has multiple orders.

Further Reads#


Step 3: Run Migrations#

To create a table for the Subscription data model in the database, start by generating the migrations for the Subscription Module with the following command:

Terminal
npx medusa db:generate subscriptionModuleService

This generates a migration in the src/modules/subscriptions/migrations directory.

Then, to reflect the migration and links in the database, run the following command:

Terminal
npx medusa db:migrate

Step 4: Override createSubscriptions Method in Service#

Since the Subscription Module’s main service extends the service factory, it has a generic createSubscriptions method that creates one or more subscriptions.

In this step, you’ll override it to add custom logic to the subscription creation that sets its date properties.

Install moment Library#

Before you start, install the Moment.js library to help manipulate and format dates with the following command:

Add getNextOrderDate Method#

In src/modules/subscription/service.ts, add the following method to SubscriptionModuleService:

src/modules/subscription/service.ts
1// ...2import moment from "moment"3import { 4  CreateSubscriptionData, 5  SubscriptionData, 6  SubscriptionInterval,7} from "./types"8
9class SubscriptionModuleService extends MedusaService({10  Subscription,11}) {12  getNextOrderDate({13    last_order_date,14    expiration_date,15    interval,16    period,17  }: {18    last_order_date: Date19    expiration_date: Date20    interval: SubscriptionInterval,21    period: number22  }): Date | null {23    const nextOrderDate = moment(last_order_date)24      .add(25        period, 26        interval === SubscriptionInterval.MONTHLY ? 27          "month" : "year"28      )29    const expirationMomentDate = moment(expiration_date)30
31    return nextOrderDate.isAfter(expirationMomentDate) ? 32      null : nextOrderDate.toDate()33  }34}

This method accepts a subscription’s last order date, expiration date, interval, and period, and uses them to calculate and return the next order date.

If the next order date, calculated from the last order date, exceeds the expiration date, null is returned.

Add getExpirationDate Method#

In the same file, add the following method to SubscriptionModuleService:

src/modules/subscription/service.ts
1class SubscriptionModuleService extends MedusaService({2  Subscription,3}) {4  // ...5  getExpirationDate({6    subscription_date,7    interval,8    period,9  }: {10    subscription_date: Date,11    interval: SubscriptionInterval,12    period: number13  }) {14    return moment(subscription_date)15      .add(16        period,17        interval === SubscriptionInterval.MONTHLY ?18          "month" : "year"19      ).toDate()20  }21}

The getExpirationDate method accepts a subscription’s date, interval, and period to calculate and return its expiration date.

Override the createSubscriptions Method#

Before overriding the createSubscriptions method, add the following types to src/modules/subscription/types/index.ts:

src/modules/subscription/types/index.ts
1import { InferTypeOf } from "@medusajs/framework/types"2import Subscription from "../models/subscription"3
4// ...5
6export type CreateSubscriptionData = {7  interval: SubscriptionInterval8  period: number9  status?: SubscriptionStatus10  subscription_date?: Date11  metadata?: Record<string, unknown>12}13
14export type SubscriptionData = InferTypeOf<typeof Subscription>
TipSince the Subscription data model is a variable, use the InferTypeOf utility imported from @medusajs/framework/types to infer its type.

Then, in src/modules/subscription/service.ts, add the following to override the createSubscriptions method:

src/modules/subscription/service.ts
1class SubscriptionModuleService extends MedusaService({2  Subscription,3}) {4  // ...5    6  // @ts-expect-error7  async createSubscriptions(8    data: CreateSubscriptionData | CreateSubscriptionData[]9  ): Promise<SubscriptionData[]> {10    const input = Array.isArray(data) ? data : [data]11
12    const subscriptions = await Promise.all(13      input.map(async (subscription) => {14        const subscriptionDate = subscription.subscription_date || new Date()15        const expirationDate = this.getExpirationDate({16          subscription_date: subscriptionDate,17          interval: subscription.interval,18          period: subscription.period,19        })20
21        return await super.createSubscriptions({22          ...subscription,23          subscription_date: subscriptionDate,24          last_order_date: subscriptionDate,25          next_order_date: this.getNextOrderDate({26            last_order_date: subscriptionDate,27            expiration_date: expirationDate,28            interval: subscription.interval,29            period: subscription.period,30          }),31          expiration_date: expirationDate,32        })33      })34    )35    36    return subscriptions37  }38}

The createSubscriptions calculates for each subscription the expiration and next order dates using the methods created earlier. It creates and returns the subscriptions.

This method is used in the next step.


Step 5: Create Subscription Workflow#

To implement and expose a feature that manipulates data, you create a workflow that uses services to implement the functionality, then create an API route that executes that workflow.

In this step, you’ll create the workflow that you’ll execute when a customer purchases a subscription.

The workflow accepts a cart’s ID, and it has three steps:

  1. Create the order from the cart.
  2. Create a subscription.
  3. Link the subscription to the order, cart, and customer.

Medusa provides the first and last steps in the @medusajs/medusa/core-flows package, so you only need to implement the second step.

Create a Subscription Step (Second Step)#

Create the file src/workflows/create-subscription/steps/create-subscription.ts with the following content:

src/workflows/create-subscription/steps/create-subscription.ts
6import { SUBSCRIPTION_MODULE } from "../../../modules/subscription"7
8type StepInput = {9  cart_id: string10  order_id: string11  customer_id?: string12  subscription_data: {13    interval: SubscriptionInterval14    period: number15  }16}17
18const createSubscriptionStep = createStep(19  "create-subscription",20  async ({ 21    cart_id, 22    order_id, 23    customer_id,24    subscription_data,25  }: StepInput, { container }) => {26    const subscriptionModuleService: SubscriptionModuleService = 27      container.resolve(SUBSCRIPTION_MODULE)28    const linkDefs: LinkDefinition[] = []29
30    const subscription = await subscriptionModuleService.createSubscriptions({31      ...subscription_data,32      metadata: {33        main_order_id: order_id,34      },35    })36
37    // TODO add links38
39    return new StepResponse({40      subscription: subscription[0],41      linkDefs,42    }, {43      subscription: subscription[0],44    })45  }, async ({ subscription }, { container }) => {46    // TODO implement compensation47  }48)49
50export default createSubscriptionStep

This step receives the IDs of the cart, order, and customer, along with the subscription’s details.

In this step, you use the createSubscriptions method to create the subscription. In the metadata property, you set the ID of the order created on purchase.

The step returns the created subscription as well as an array of links to create. To add the links to be created in the returned array, replace the first TODO with the following:

src/workflows/create-subscription/steps/create-subscription.ts
1linkDefs.push({2  [SUBSCRIPTION_MODULE]: {3    "subscription_id": subscription[0].id,4  },5  [Modules.ORDER]: {6    "order_id": order_id,7  },8})9
10linkDefs.push({11  [SUBSCRIPTION_MODULE]: {12    "subscription_id": subscription[0].id,13  },14  [Modules.CART]: {15    "cart_id": cart_id,16  },17})18
19if (customer_id) {20  linkDefs.push({21    [SUBSCRIPTION_MODULE]: {22      "subscription_id": subscription[0].id,23    },24    [Modules.CUSTOMER]: {25      "customer_id": customer_id,26    },27  })28}

This adds links between:

  1. The subscription and the order.
  2. The subscription and the cart.
  3. The subscription and the customer, if a customer is associated with the cart.

The step also has a compensation function to undo the step’s changes if an error occurs. So, replace the second TODO with the following:

src/workflows/create-subscription/steps/create-subscription.ts
1const subscriptionModuleService: SubscriptionModuleService = 2    container.resolve(SUBSCRIPTION_MODULE)3
4await subscriptionModuleService.cancelSubscriptions(subscription.id)

The compensation function receives the subscription as a parameter. It cancels the subscription.

Create Workflow#

Create the file src/workflows/create-subscription/index.ts with the following content:

src/workflows/create-subscription/index.ts
12} from "../../modules/subscription/types"13import createSubscriptionStep from "./steps/create-subscription"14
15type WorkflowInput = {16  cart_id: string,17  subscription_data: {18    interval: SubscriptionInterval19    period: number20  }21}22
23const createSubscriptionWorkflow = createWorkflow(24  "create-subscription",25  (input: WorkflowInput) => {26    const { id } = completeCartWorkflow.runAsStep({27      input: {28        id: input.cart_id,29      },30    })31
32    const { data: orders } = useQueryGraphStep({33      entity: "order",34      fields: ["*", "id", "customer_id"],35      filters: {36        id,37      },38      options: {39        throwIfKeyNotFound: true,40      },41    })42
43    const { subscription, linkDefs } = createSubscriptionStep({44      cart_id: input.cart_id,45      order_id: orders[0].id,46      customer_id: orders[0].customer_id,47      subscription_data: input.subscription_data,48    })49
50    createRemoteLinkStep(linkDefs)51
52    return new WorkflowResponse({53      subscription: subscription,54      order: orders[0],55    })56  }57)58
59export default createSubscriptionWorkflow

This workflow accepts the cart’s ID, along with the subscription details. It executes the following steps:

  1. completeCartWorkflow from @medusajs/medusa/core-flows that completes a cart and creates an order.
  2. useQueryGraphStep from @medusajs/medusa/core-flows to retrieve the order's details.
  3. createSubscriptionStep, which is the step you created previously.
  4. createRemoteLinkStep from @medusajs/medusa/core-flows, which accepts links to create. These links are in the linkDefs array returned by the previous step.

The workflow returns the created subscription and order.

Further Reads#


Step 6: Override Complete Cart API Route#

To expose custom commerce features to frontend applications, such as the Medusa Admin dashboard or a storefront, you expose an endpoint by creating an API route.

In this step, you’ll change what happens when the Complete Cart API route is used to complete the customer’s purchase and place an order.

Create the file src/api/store/carts/[id]/complete/route.ts with the following content:

src/api/store/carts/[id]/complete/route.ts
9import createSubscriptionWorkflow from "../../../../../workflows/create-subscription"10
11export const POST = async (12  req: MedusaRequest,13  res: MedusaResponse14) => {15  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)16
17  const { data: [cart] } = await query.graph({18    entity: "cart",19    fields: [20      "metadata",21    ],22    filters: {23      id: [req.params.id],24    },25  })26  27  const { metadata } = cart28
29  if (!metadata?.subscription_interval || !metadata.subscription_period) {30    throw new MedusaError(31      MedusaError.Types.INVALID_DATA,32      "Please set the subscription's interval and period first."33    )34  }35
36  const { result } = await createSubscriptionWorkflow(37    req.scope38  ).run({39    input: {40      cart_id: req.params.id,41      subscription_data: {42        interval: metadata.subscription_interval,43        period: metadata.subscription_period,44      },45    },46  })47
48  res.json({49    type: "order",50    ...result,51  })52}

This overrides the API route at /store/carts/[id]/complete.

In the route handler, you retrieve the cart to access it's metadata property. If the subscription details aren't stored there, you throw an error.

Then, you use the createSubscriptionWorkflow you created to create the order, and return the created order and subscription in the response.

Storefront Customization#

In this section, you'll customize the checkout flow in the Next.js Starter storefront to include a subscription form.

After installation, create the file src/modules/checkout/components/subscriptions/index.tsx with the following content:

Storefront
src/modules/checkout/components/subscriptions/index.tsx
12import { updateSubscriptionData } from "../../../../lib/data/cart"13
14export enum SubscriptionInterval {15  MONTHLY = "monthly",16  YEARLY = "yearly"17}18
19const SubscriptionForm = () => {20  const [interval, setInterval] = useState<SubscriptionInterval>(21    SubscriptionInterval.MONTHLY22  )23  const [period, setPeriod] = useState(1)24  const [isLoading, setIsLoading] = useState(false)25
26  const searchParams = useSearchParams()27  const router = useRouter()28  const pathname = usePathname()29
30  const isOpen = searchParams.get("step") === "subscription"31
32  const createQueryString = useCallback(33    (name: string, value: string) => {34      const params = new URLSearchParams(searchParams)35      params.set(name, value)36
37      return params.toString()38    },39    [searchParams]40  )41
42  const handleEdit = () => {43    router.push(pathname + "?" + createQueryString("step", "subscription"), {44      scroll: false,45    })46  }47
48  const handleSubmit = async () => {49    setIsLoading(true)50    51    updateSubscriptionData(interval, period)52    .then(() => {53      setIsLoading(false)54      router.push(pathname + "?step=delivery", { scroll: false })55    })56  }57
58  return (59    <div className="bg-white">60      <div className="flex flex-row items-center justify-between mb-6">61        <Heading62          level="h2"63          className={clx(64            "flex flex-row text-3xl-regular gap-x-2 items-baseline",65            {66              "opacity-50 pointer-events-none select-none":67                !isOpen,68            }69          )}70        >71          Subscription Details72          {!isOpen && <CheckCircleSolid />}73        </Heading>74        {!isOpen && (75          <Text>76            <button77              onClick={handleEdit}78              className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"79              data-testid="edit-payment-button"80            >81              Edit82            </button>83          </Text>84        )}85      </div>86      <div>87        <div className={isOpen ? "block" : "hidden"}>88          <NativeSelect 89            placeholder="Interval" 90            value={interval} 91            onChange={(e) => 92              setInterval(e.target.value as SubscriptionInterval)93            }94            required95            autoComplete="interval"96          >97            {Object.values(SubscriptionInterval).map(98              (intervalOption, index) => (99                <option key={index} value={intervalOption}>100                  {capitalize(intervalOption)}101                </option>102              )103            )}104          </NativeSelect>105          <Input106            label="Period"107            name="period"108            autoComplete="period"109            value={period}110            onChange={(e) => 111              setPeriod(parseInt(e.target.value))112            }113            required114            type="number"115          />116
117          <Button118            size="large"119            className="mt-6"120            onClick={handleSubmit}121            isLoading={isLoading}122            disabled={!interval || !period}123          >124            Continue to review125          </Button>126        </div>127      </div>128      <Divider className="mt-8" />129    </div>130  )131}132
133export default SubscriptionForm

This adds a component that displays a form to choose the subscription's interval and period during checkout.

In the component, you use a updateSubscriptionData function that sends a request to the Medusa application to update the cart.

To implement it, add to the file src/lib/data/cart.ts the following:

Storefront
src/lib/data/cart.ts
1// other imports...2import { SubscriptionInterval } from "../../modules/checkout/components/subscriptions"3
4// other functions...5
6export async function updateSubscriptionData(7  subscription_interval: SubscriptionInterval,8  subscription_period: number9) {10  const cartId = getCartId()11  12  if (!cartId) {13    throw new Error("No existing cart found when placing an order")14  }15
16  await updateCart({17    metadata: {18      subscription_interval,19      subscription_period,20    },21  })22  revalidateTag("cart")23}

This updates the cart's metadata with the subscription details.

Next, change the last line of the setAddresses function in src/lib/data/cart.ts to redirect to the subscription step once the customer enters their address:

Storefront
src/lib/data/cart.ts
1export async function setAddresses(currentState: unknown, formData: FormData) {2  // ...3  redirect(4    `/${formData.get("shipping_address.country_code")}/checkout?step=subscription`5  )6}

Finally, add the SubscriptionForm in src/modules/checkout/templates/checkout-form/index.tsx after the Addresses wrapper component:

Storefront
src/modules/checkout/templates/checkout-form/index.tsx
1// other imports...2import SubscriptionForm from "@modules/checkout/components/subscriptions"3
4export default async function CheckoutForm({5  cart,6  customer,7}: {8  cart: HttpTypes.StoreCart | null9  customer: HttpTypes.StoreCustomer | null10}) {11  // ...12
13  return (14    <div>15      {/* ... */}16      {/* After Addresses, before Shipping */}17      <div>18        <SubscriptionForm />19      </div>20      {/* ... */}21    </div>22  )23}

Test Cart Completion and Subscription Creation#

To test out the cart completion flow:

  1. In the Medusa application's directory, run the following command to start the application:
  1. In the Next.js Starter's directory, run the following command to start the storefront:
  1. Add a product to the cart and place an order. During checkout, you'll see a Subscription Details step to fill out the interval and period.

Further Reads#


Step 7: Add Admin API Routes for Subscription#

In this step, you’ll add two API routes for admin users:

  1. One to list all subscriptions.
  2. One to retrieve a subscription.

List Subscriptions Admin API Route#

Create the file src/api/admin/subscriptions/route.ts with the following content:

src/api/admin/subscriptions/route.ts
1import { 2  AuthenticatedMedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import { ContainerRegistrationKeys } from "@medusajs/framework/utils"6
7export const GET = async (8  req: AuthenticatedMedusaRequest,9  res: MedusaResponse10) => {11  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)12
13  const {14    limit = 20,15    offset = 0,16  } = req.validatedQuery || {}17
18  const { 19    data: subscriptions,20    metadata: { count, take, skip },21  } = await query.graph({22    entity: "subscription",23    fields: [24      "*",25      "orders.*",26      "customer.*",27      ...(req.validatedQuery?.fields.split(",") || []),28    ],29    pagination: {30      skip: offset,31      take: limit,32      order: {33        subscription_date: "DESC",34      },35    },36  })37
38  res.json({39    subscriptions,40    count,41    limit: take,42    offset: skip,43  })44}

This adds a GET API route at /admin/subscriptions.

In the route handler, you use Query to retrieve a subscription with its orders and customer.

The API route accepts pagination parameters to paginate the subscription list. It returns the subscriptions with pagination parameters in the response.

Get Subscription Admin API Route#

Create the file src/api/admin/subscriptions/[id]/route.ts with the following content:

src/api/admin/subscriptions/[id]/route.ts
1import { 2  AuthenticatedMedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import { ContainerRegistrationKeys } from "@medusajs/framework/utils"6
7export const GET = async (8  req: AuthenticatedMedusaRequest,9  res: MedusaResponse10) => {11  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)12
13  const { data: [subscription] } = await query.graph({14    entity: "subscription",15    fields: [16      "*",17      "orders.*",18      "customer.*",19      ...(req.validatedQuery?.fields.split(",") || []),20    ],21    filters: {22      id: [req.params.id],23    },24  })25
26  res.json({27    subscription,28  })29}

This adds a GET API route at /admin/subscriptions/[id], where [id] is the ID of the subscription to retrieve.

In the route handler, you retrieve a subscription by its ID using Query and return it in the response.

In the next section, you’ll extend the Medusa admin and use these API routes to show the subscriptions.


Step 8: Extend Admin#

The Medusa Admin is customizable, allowing you to inject widgets into existing pages or add UI routes to create new pages.

In this step, you’ll add two UI routes:

  1. One to view all subscriptions.
  2. One to view a single subscription.

Create Types File#

Before creating the UI routes, create the file src/admin/types/index.ts that holds types used by the UI routes:

src/admin/types/index.ts
1import { 2  OrderDTO,3  CustomerDTO,4} from "@medusajs/framework/types"5
6export enum SubscriptionStatus {7  ACTIVE = "active",8  CANCELED = "canceled",9  EXPIRED = "expired",10  FAILED = "failed"11}12
13export enum SubscriptionInterval {14  MONTHLY = "monthly",15  YEARLY = "yearly"16}17
18export type SubscriptionData = {19  id: string20  status: SubscriptionStatus21  interval: SubscriptionInterval22  subscription_date: string23  last_order_date: string24  next_order_date: string | null25  expiration_date: string26  metadata: Record<string, unknown> | null27  orders?: OrderDTO[]28  customer?: CustomerDTO29}

Create Subscriptions List UI Route#

To create the subscriptions list UI route, create the file src/admin/routes/subscriptions/page.tsx with the following content:

src/admin/routes/subscriptions/page.tsx
6import { Link } from "react-router-dom"7
8const SubscriptionsPage = () => {9  const [subscriptions, setSubscriptions] = useState<10    SubscriptionData[]11  >([])12  13  // TODO add pagination + fetch subscriptions14
15  const getBadgeColor = (status: SubscriptionStatus) => {16    switch(status) {17      case SubscriptionStatus.CANCELED:18        return "orange"19      case SubscriptionStatus.FAILED:20        return "red"21      case SubscriptionStatus.EXPIRED:22        return "grey"23      default:24        return "green"25    }26  }27  28  const getStatusTitle = (status: SubscriptionStatus) => {29    return status.charAt(0).toUpperCase() + 30      status.substring(1)31  }32
33  return (34    <Container>35      <Heading level="h1">Subscriptions</Heading>36      <Table>37        <Table.Header>38          <Table.Row>39            <Table.HeaderCell>#</Table.HeaderCell>40            <Table.HeaderCell>Main Order</Table.HeaderCell>41            <Table.HeaderCell>Customer</Table.HeaderCell>42            <Table.HeaderCell>Subscription Date</Table.HeaderCell>43            <Table.HeaderCell>Expiry Date</Table.HeaderCell>44            <Table.HeaderCell>Status</Table.HeaderCell>45          </Table.Row>46        </Table.Header>47        <Table.Body>48          {subscriptions.map((subscription) => (49            <Table.Row key={subscription.id}>50              <Table.Cell>51                <Link to={`/subscriptions/${subscription.id}`}>52                {subscription.id}53                </Link>54              </Table.Cell>55              <Table.Cell>56                <Link to={`/orders/${subscription.metadata?.main_order_id}`}>57                  View Order58                </Link>59              </Table.Cell>60              <Table.Cell>61                {subscription.customer && (62                  <Link to={`/customers/${subscription.customer.id}`}>63                    {subscription.customer.email}64                  </Link>65                )}66              </Table.Cell>67              <Table.Cell>68                {(new Date(subscription.subscription_date)).toDateString()}69              </Table.Cell>70              <Table.Cell>71              {(new Date(subscription.expiration_date)).toDateString()}72              </Table.Cell>73              <Table.Cell>74                <Badge color={getBadgeColor(subscription.status)}>75                  {getStatusTitle(subscription.status)}76                </Badge>77              </Table.Cell>78            </Table.Row>79          ))}80        </Table.Body>81      </Table>82      {/* TODO add pagination */}83    </Container>84  )85}86
87export const config = defineRouteConfig({88  label: "Subscriptions",89  icon: ClockSolid,90})91
92export default SubscriptionsPage

This creates a React component that displays a table of subscriptions to the admin. It also adds a new “Subscriptions” sidebar item to access the page.

To fetch the subscriptions from the API route created in the previous step, replace the first TODO with the following:

src/admin/routes/subscriptions/page.tsx
1// other imports...2import { useMemo } from "react"3
4const SubscriptionsPage = () => {5  // ...6    7  const [currentPage, setCurrentPage] = useState(0)8  const pageLimit = 209  const [count, setCount] = useState(0)10  const pagesCount = useMemo(() => {11    return count / pageLimit12  }, [count])13  const canNextPage = useMemo(14    () => currentPage < pagesCount - 1, 15    []16  )17  const canPreviousPage = useMemo(18    () => currentPage > 0, 19    []20  )21
22  const nextPage = () => {23    if (canNextPage) {24      setCurrentPage((prev) => prev + 1)25    }26  }27
28  const previousPage = () => {29    if (canPreviousPage) {30      setCurrentPage((prev) => prev - 1)31    }32  }33  34  // TODO fetch subscriptions35    36  // ...37}

You now implement the pagination mechanism with the following variables:

  • currentPage: a state variable that holds the current page number.
  • pageLimit: the number of subscriptions to retrieve per page.
  • count: A state variable that holds the total count of subscriptions.
  • pagesCount: A memoized variable that holds the number of pages based on count and pageLimit.
  • canNextPage: A memoized variable indicating whether there’s a next page based on whether currentPage is greater than pagesCount - 1.
  • canPreviousPage: A memoized variable indicating whether there’s a previous page based on whether currentPage is greater than 0.

You also implement a nextPage function to increment currentPage, and a previousPage function to decrement currentPage.

To fetch the subscriptions, replace the new TODO with the following:

src/admin/routes/subscriptions/page.tsx
1// other imports...2import { useEffect } from "react"3
4const SubscriptionsPage = () => {5  // ...6    7   useEffect(() => {8    const query = new URLSearchParams({9      limit: `${pageLimit}`,10      offset: `${pageLimit * currentPage}`,11    })12    13    fetch(`/admin/subscriptions?${query.toString()}`, {14      credentials: "include",15    })16    .then((res) => res.json())17    .then(({ 18      subscriptions: data, 19      count,20    }) => {21      setSubscriptions(data)22      setCount(count)23    })24  }, [currentPage])25    26  // ...27}

You fetch the subscriptions in useEffect whenever currentPage changes. You send a request to the /admin/subscriptions API route with pagination parameters, then set the subscriptions state variable with the received data.

Finally, replace the TODO in the return statement with the following:

src/admin/routes/subscriptions/page.tsx
1// other imports...2import { useEffect } from "react"3
4const SubscriptionsPage = () => {5  // ...6    7  return (8    <Container>9      {/* ... */}10      <Table.Pagination11        count={count}12        pageSize={pageLimit}13        pageIndex={currentPage}14        pageCount={pagesCount}15        canPreviousPage={canPreviousPage}16        canNextPage={canNextPage}17        previousPage={previousPage}18        nextPage={nextPage}19      />20    </Container>21  )22}

You show the pagination controls to switch between pages at the end of the table.

Create a Single Subscription UI Route#

To create the UI route or page that shows the details of a single subscription, create the file src/admin/routes/subscriptions/[id]/page.tsx with the following content:

src/admin/routes/subscriptions/[id]/page.tsx
1import { 2  Container,3  Heading,4  Table,5} from "@medusajs/ui"6import { useEffect, useState } from "react"7import { useParams, Link } from "react-router-dom"8import { SubscriptionData } from "../../../types/index.js"9
10const SubscriptionPage = () => {11  const { id } = useParams()12  const [subscription, setSubscription] = useState<13    SubscriptionData | undefined14  >()15
16  useEffect(() => {17    fetch(`/admin/subscriptions/${id}`, {18      credentials: "include",19    })20    .then((res) => res.json())21    .then(({ subscription: data }) => {22      setSubscription(data)23    })24  }, [id])25
26  return <Container>27    {subscription && (28      <>29        <Heading level="h1">Orders of Subscription #{subscription.id}</Heading>30        <Table>31          <Table.Header>32            <Table.Row>33              <Table.HeaderCell>#</Table.HeaderCell>34              <Table.HeaderCell>Date</Table.HeaderCell>35              <Table.HeaderCell>View Order</Table.HeaderCell>36            </Table.Row>37          </Table.Header>38          <Table.Body>39            {subscription.orders?.map((order) => (40              <Table.Row key={order.id}>41                <Table.Cell>{order.id}</Table.Cell>42                <Table.Cell>{(new Date(order.created_at)).toDateString()}</Table.Cell>43                <Table.Cell>44                  <Link to={`/orders/${order.id}`}>45                    View Order46                  </Link>47                </Table.Cell>48              </Table.Row>49            ))}50          </Table.Body>51        </Table>52      </>53    )}54  </Container>55}56
57export default SubscriptionPage

This creates the React component used to display a subscription’s details page.

In this component, you retrieve the subscription’s details using the /admin/subscriptions/[id] API route that you created in the previous section.

The component renders a table of the subscription’s orders.

Test the UI Routes#

To test the UI routes, run the Medusa application and go to http://localhost:9000/app.

After you log-in, you’ll find a new sidebar item “Subscriptions”. Once you click on it, you’ll see the list of subscription purchases.

To view a subscription’s details, click on its ID, which opens the subscription details page. This page contains the subscription’s orders.

Further Reads#


Step 9: Create New Subscription Orders Workflow#

In this step, you’ll create a workflow to create a new subscription order. Later, you’ll execute this workflow in a scheduled job.

The workflow has eight steps:

  1. Retrieve the subscription’s linked cart. Medusa provides a useQueryGraphStep in the @medusajs/medusa/core-flows package that can be used as a step.
  2. Create a payment collection for the new order. Medusa provides a createPaymentCollectionsStep in the @medusajs/medusa/core-flows package that you can use.
  3. Create payment sessions in the payment collection. Medusa provides a createPaymentSessionsWorkflow in the @medusajs/medusa/core-flows package that can be used as a step.
  4. Authorize the payment session. Medusa also provides the authorizePaymentSessionStep in the @medusajs/medusa/core-flows package, which can be used.
  5. Create the subscription’s new order.
  6. Create links between the subscription and the order using the createRemoteLinkStep provided in the @medusajs/medusa/core-flows package.
  7. Capture the order’s payment using the capturePaymentStep provided by Medusa in the @medusajs/medusa/core-flows package.
  8. Update the subscription’s last_order_date and next_order_date properties.

You’ll only implement the fifth and eighth steps.

NoteThis guide doesn’t explain payment-related flows and concepts in detail. For more details, refer to the Payment Module .

Create createSubscriptionOrderStep (Fifth Step)#

Create the file src/workflows/create-subscription-order/steps/create-subscription-order.ts with the following content:

src/workflows/create-subscription-order/steps/create-subscription-order.ts
14
15type StepInput = {16  subscription: SubscriptionData17  cart: CartWorkflowDTO18  payment_collection: PaymentCollectionDTO19}20
21function getOrderData(cart: CartWorkflowDTO) {22  // TODO format order's data23}24
25const createSubscriptionOrderStep = createStep(26  "create-subscription-order",27  async ({ 28    subscription, cart, payment_collection,29  }: StepInput, 30  { container, context }) => {31    const linkDefs: LinkDefinition[] = []32
33    const { result: order } = await createOrdersWorkflow(container)34      .run({35        input: getOrderData(cart),36        context,37      })38
39    // TODO add links to linkDefs40
41    return new StepResponse({42      order,43      linkDefs,44    }, {45      order,46    })47  },48  async ({ order }, { container }) => {49    // TODO add compensation function50  }51)52
53export default createSubscriptionOrderStep

This creates a createSubscriptionOrderStep that uses the createOrdersWorkflow, which Medusa provides in the @medusajs/medusa/core-flows package. The step returns the created order and an array of links to be created.

In this step, you use a getOrderData function to format the order’s input data.

Replace the getOrderData function definition with the following:

src/workflows/create-subscription-order/steps/create-subscription-order.ts
1function getOrderData(cart: CartWorkflowDTO) {2  return {3    region_id: cart.region_id,4    customer_id: cart.customer_id,5    sales_channel_id: cart.sales_channel_id,6    email: cart.email,7    currency_code: cart.currency_code,8    shipping_address: {9      ...cart.shipping_address,10      id: null,11    },12    billing_address: {13      ...cart.billing_address,14      id: null,15    },16    items: cart.items,17    shipping_methods: cart.shipping_methods.map((method) => ({18      name: method.name,19      amount: method.amount,20      is_tax_inclusive: method.is_tax_inclusive,21      shipping_option_id: method.shipping_option_id,22      data: method.data,23      tax_lines: method.tax_lines.map((taxLine) => ({24        description: taxLine.description,25        tax_rate_id: taxLine.tax_rate_id,26        code: taxLine.code,27        rate: taxLine.rate,28        provider_id: taxLine.provider_id,29      })),30      adjustments: method.adjustments.map((adjustment) => ({31        code: adjustment.code,32        amount: adjustment.amount,33        description: adjustment.description,34        promotion_id: adjustment.promotion_id,35        provider_id: adjustment.provider_id,36      })),37    })),38  }39}

This formats the order’s data using the cart originally used to make the subscription purchase.

Next, to add links to the returned linkDefs array, replace the TODO in the step with the following:

src/workflows/create-subscription-order/steps/create-subscription-order.ts
1linkDefs.push({2  [Modules.ORDER]: {3    order_id: order.id,4  },5  [Modules.PAYMENT]: {6    payment_collection_id: payment_collection.id,7  },8},9{10  [SUBSCRIPTION_MODULE]: {11    subscription_id: subscription.id,12  },13  [Modules.ORDER]: {14    order_id: order.id,15  },16})

This adds links to be created into the linkDefs array between the new order and payment collection, and the new order and its subscription.

Finally, replace the TODO in the compensation function to cancel the order in case of an error:

src/workflows/create-subscription-order/steps/create-subscription-order.ts
1const orderModuleService: IOrderModuleService = container.resolve(2  Modules.ORDER3)4
5await orderModuleService.cancel(order.id)

Create updateSubscriptionStep (Eighth Step)#

Before creating the seventh step, add in src/modules/subscription/service.ts the following new method:

src/modules/subscription/service.ts
1class SubscriptionModuleService extends MedusaService({2  Subscription,3}) {4  // ...5  async recordNewSubscriptionOrder(id: string): Promise<SubscriptionData[]> {6    const subscription = await this.retrieveSubscription(id)7
8    const orderDate = new Date()9
10    return await this.updateSubscriptions({11      id,12      last_order_date: orderDate,13      next_order_date: this.getNextOrderDate({14        last_order_date: orderDate,15        expiration_date: subscription.expiration_date,16        interval: subscription.interval,17        period: subscription.period,18      }),19    })20  }21}

The recordNewSubscriptionOrder method updates a subscription’s last_order_date with the current date and calculates the next order date using the getNextOrderDate method added previously.

Then, to create the step that updates a subscription after its order is created, create the file src/workflows/create-subscription-order/steps/update-subscription.ts with the following content:

src/workflows/create-subscription-order/steps/update-subscription.ts
8import SubscriptionModuleService from "../../../modules/subscription/service"9
10type StepInput = {11  subscription_id: string12}13
14const updateSubscriptionStep = createStep(15  "update-subscription",16  async ({ subscription_id }: StepInput, { container }) => {17    const subscriptionModuleService: SubscriptionModuleService = 18      container.resolve(19        SUBSCRIPTION_MODULE20      )21
22    const prevSubscriptionData = await subscriptionModuleService23      .retrieveSubscription(24        subscription_id25      )26
27    const subscription = await subscriptionModuleService28      .recordNewSubscriptionOrder(29        subscription_id30      )31
32    return new StepResponse({33      subscription,34    }, {35      prev_data: prevSubscriptionData,36    })37  },38  async ({ 39    prev_data,40  }, { container }) => {41    // TODO add compensation42  }43)44
45export default updateSubscriptionStep

This creates the updateSubscriptionStep that updates the subscriber using the recordNewSubscriptionOrder method of the Subscription Module’s main service. It returns the updated subscription.

Before updating the subscription, the step retrieves the old data and passes it to the compensation function to undo the changes on the subscription.

So, replace the TODO in the compensation function with the following:

src/workflows/create-subscription-order/steps/update-subscription.ts
1const subscriptionModuleService: SubscriptionModuleService = 2  container.resolve(3    SUBSCRIPTION_MODULE4  )5
6await subscriptionModuleService.updateSubscriptions({7  id: prev_data.id,8  last_order_date: prev_data.last_order_date,9  next_order_date: prev_data.next_order_date,10})

This updates the subscription’s last_order_date and next_order_date properties to the values before the update.

Create Workflow#

Finally, create the file src/workflows/create-subscription-order/index.ts with the following content:

src/workflows/create-subscription-order/index.ts
24  (input: WorkflowInput) => {25    const { data: carts } = useQueryGraphStep({26      entity: "subscription",27      fields: [28        "*",29        "cart.*",30        "cart.items.*",31        "cart.items.tax_lines.*",32        "cart.items.adjustments.*",33        "cart.shipping_address.*",34        "cart.billing_address.*",35        "cart.shipping_methods.*",36        "cart.shipping_methods.tax_lines.*",37        "cart.shipping_methods.adjustments.*",38        "cart.payment_collection.*",39        "cart.payment_collection.payment_sessions.*",40      ],41      filters: {42        id: [input.subscription.id],43      },44      options: {45        throwIfKeyNotFound: true,46      },47    })48
49    const payment_collection = createPaymentCollectionsStep([{50      region_id: carts[0].region_id,51      currency_code: carts[0].currency_code,52      amount: carts[0].payment_collection.amount,53      metadata: carts[0].payment_collection.metadata,54    }])[0]55
56    const paymentSession = createPaymentSessionsWorkflow.runAsStep({57      input: {58        payment_collection_id: payment_collection.id,59        provider_id: carts[0].payment_collection.payment_sessions[0].provider_id,60        data: carts[0].payment_collection.payment_sessions[0].data,61        context: carts[0].payment_collection.payment_sessions[0].context,62      },63    })64
65    const payment = authorizePaymentSessionStep({66      id: paymentSession.id,67      context: paymentSession.context,68    })69
70    const { order, linkDefs } = createSubscriptionOrderStep({71      subscription: input.subscription,72      cart: carts[0],73      payment_collection,74    })75
76    createRemoteLinkStep(linkDefs)77
78    capturePaymentStep({79      payment_id: payment.id,80      amount: payment.amount,81    })82
83    updateSubscriptionStep({84      subscription_id: input.subscription.id,85    })86
87    return new WorkflowResponse({88      order,89    })90  }91)92
93export default createSubscriptionOrderWorkflow

The workflow runs the following steps:

  1. useQueryGraphStep to retrieve the details of the cart linked to the subscription.
  2. createPaymentCollectionsStep to create a payment collection using the same information in the cart.
  3. createPaymentSessionsWorkflow to create a payment session in the payment collection from the previous step.
  4. authorizePaymentSessionStep to authorize the payment session created from the first step.
  5. createSubscriptionOrderStep to create the new order for the subscription.
  6. createRemoteLinkStep to create links returned by the previous step.
  7. capturePaymentStep to capture the order’s payment.
  8. updateSubscriptionStep to update the subscription’s last_order_date and next_order_date.

In the next step, you’ll execute the workflow in a scheduled job.

Further Reads#


Step 10: Create New Subscription Orders Scheduled Job#

A scheduled job is an asynchronous function executed at a specified interval pattern. Use scheduled jobs to execute a task at a regular interval.

In this step, you’ll create a scheduled job that runs once a day. It finds all subscriptions whose next_order_date property is the current date and uses the workflow from the previous step to create an order for them.

Create the file src/jobs/create-subscription-orders.ts with the following content:

src/jobs/create-subscription-orders.ts
6import { SubscriptionStatus } from "../modules/subscription/types"7
8export default async function createSubscriptionOrdersJob(9  container: MedusaContainer10) {11  const subscriptionModuleService: SubscriptionModuleService =12    container.resolve(SUBSCRIPTION_MODULE)13  const logger = container.resolve("logger")14
15  let page = 016  const limit = 2017  let pagesCount = 018
19  do {20    const beginningToday = moment(new Date()).set({21      second: 0,22      minute: 0,23      hour: 0,24    })25    .toDate()26    const endToday = moment(new Date()).set({27      second: 59,28      minute: 59,29      hour: 23,30    })31    .toDate()32  33    const [subscriptions, count] = await subscriptionModuleService34      .listAndCountSubscriptions({35        next_order_date: {36          $gte: beginningToday,37          $lte: endToday,38        },39        status: SubscriptionStatus.ACTIVE,40      }, {41        skip: page * limit,42        take: limit,43      })    44
45      // TODO create orders for subscriptions46
47    if (!pagesCount) {48      pagesCount = count / limit49    }50  51    page++52  } while (page < pagesCount - 1)53}54
55export const config = {56  name: "create-subscription-orders",57  schedule: "0 0 * * *", // Every day at midnight58}

This creates a scheduled job that runs once a day.

In the scheduled job, you retrieve subscriptions whose next_order_date is between the beginning and end of today, and whose status is active. You also support paginating the subscriptions in case there are more than 20 matching those filters.

To create orders for the subscriptions returned, replace the TODO with the following:

src/jobs/create-subscription-orders.ts
1await Promise.all(2  subscriptions.map(async (subscription) => {3    try {4      const  { result } = await createSubscriptionOrderWorkflow(container)5        .run({6          input: {7            subscription,8          },9        })10
11      logger.info(`Created new order ${12        result.order.id13      } for subscription ${subscription.id}`)14    } catch (e) {15      logger.error(16        `Error creating a new order for subscription ${subscription.id}`,17        e18      )19    }20  })21)

This loops over the returned subscriptions and executes the createSubscriptionOrderWorkflow from the previous step to create the order.

Further Reads#


Step 11: Expire Subscriptions Scheduled Job#

In this step, you’ll create a scheduled job that finds subscriptions whose expiration_date is the current date and marks them as expired.

Before creating the scheduled job, add in src/modules/subscription/service.ts a new method:

src/modules/subscription/service.ts
1class SubscriptionModuleService extends MedusaService({2  Subscription,3}) {4  // ...5  async expireSubscription(id: string | string[]): Promise<SubscriptionData[]> {6    const input = Array.isArray(id) ? id : [id]7
8    return await this.updateSubscriptions({9      selector: {10        id: input,11      },12      data: {13        next_order_date: null,14        status: SubscriptionStatus.EXPIRED,15      },16    })17  }18}

The expireSubscription updates the following properties of the specified subscriptions:

  1. Set next_order_date to null as there are no more orders.
  2. Set the status to expired.

Then, create the file src/jobs/expire-subscription-orders.ts with the following content:

src/jobs/expire-subscription-orders.ts
5import { SubscriptionStatus } from "../modules/subscription/types"6
7export default async function expireSubscriptionOrdersJob(8  container: MedusaContainer9) {10  const subscriptionModuleService: SubscriptionModuleService =11    container.resolve(SUBSCRIPTION_MODULE)12  const logger = container.resolve("logger")13
14  let page = 015  const limit = 2016  let pagesCount = 017
18  do {19    const beginningToday = moment(new Date()).set({20      second: 0,21      minute: 0,22      hour: 0,23    })24    .toDate()25    const endToday = moment(new Date()).set({26      second: 59,27      minute: 59,28      hour: 23,29    })30    .toDate()31  32    const [subscriptions, count] = await subscriptionModuleService33      .listAndCountSubscriptions({34        expiration_date: {35          $gte: beginningToday,36          $lte: endToday,37        },38        status: SubscriptionStatus.ACTIVE,39      }, {40        skip: page * limit,41        take: limit,42      })    43
44    const subscriptionIds = subscriptions.map((subscription) => subscription.id)45
46    await subscriptionModuleService.expireSubscription(subscriptionIds)47
48    logger.log(`Expired ${subscriptionIds}.`)49
50    if (!pagesCount) {51      pagesCount = count / limit52    }53  54    page++55  } while (page < pagesCount - 1)56}57
58export const config = {59  name: "expire-subscriptions",60  schedule: "0 0 * * *", // Every day at midnight61}

This scheduled job runs once a day.

In the scheduled job, you find all subscriptions whose expiration_date is between the beginning and end of today and their status is active. Then, you use the expireSubscription method to expire those subscriptions.

You also implement pagination in case there are more than 20 expired subscriptions.


Step 12: Add Customer API Routes#

In this step, you’ll add two API routes for authenticated customers:

  1. View their list of subscriptions.
  2. Cancel a subscription.

Create Subscriptions List API Route#

Create the file src/api/store/customers/me/subscriptions/route.ts with the following content:

src/api/store/customers/me/subscriptions/route.ts
1import { 2  AuthenticatedMedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import { ContainerRegistrationKeys } from "@medusajs/framework/utils"6
7export const GET = async (8  req: AuthenticatedMedusaRequest,9  res: MedusaResponse10) => {11  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)12
13  const { data: [customer] } = await query.graph({14    entity: "customer",15    fields: [16      "subscriptions.*",17    ],18    filters: {19      id: [req.auth_context.actor_id],20    },21  })22
23  res.json({24    subscriptions: customer.subscriptions,25  })26}

This adds an API route at /store/customers/me/subscriptions.

In the route handler, you retrieve the authenticated customer’s subscriptions using Query and return them in the response.

Cancel Subscription API Route#

Before creating this API route, add in src/modules/subscription/service.ts the following new method:

src/modules/subscription/service.ts
1class SubscriptionModuleService extends MedusaService({2  Subscription,3}) {4  // ...5    6  async cancelSubscriptions(7    id: string | string[]): Promise<SubscriptionData[]> {8    const input = Array.isArray(id) ? id : [id]9
10    return await this.updateSubscriptions({11      selector: {12        id: input,13      },14      data: {15        next_order_date: null,16        status: SubscriptionStatus.CANCELED,17      },18    })19  }20}

The cancelSubscriptions method updates the specified subscribers to set their next_order_date to null and their status to canceled.

Then, create the file src/api/store/customers/me/subscriptions/[id]/route.ts with the following content:

src/api/store/customers/me/subscriptions/[id]/route.ts
1import { 2  AuthenticatedMedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import SubscriptionModuleService from "../../../../../../modules/subscription/service"6import { 7  SUBSCRIPTION_MODULE,8} from "../../../../../../modules/subscription"9
10export const POST = async (11  req: AuthenticatedMedusaRequest,12  res: MedusaResponse13) => {14  const subscriptionModuleService: SubscriptionModuleService =15    req.scope.resolve(SUBSCRIPTION_MODULE)16
17  const subscription = await subscriptionModuleService.cancelSubscriptions(18    req.params.id19  )20
21  res.json({22    subscription,23  })24}

This adds an API route at /store/customers/me/subscriptions/[id]. In the route handler, you use the cancelSubscriptions method added above to cancel the subscription whose ID is passed as a path parameter.

Test it Out#

To test out the above API routes, first, log in as a customer with the following request:

Code
1curl -X POST 'http://localhost:9000/auth/customer/emailpass' \2-H 'Content-Type: application/json' \3--data-raw '{4    "email": "customer@gmail.com",5    "password": "supersecret"6}'

Make sure to replace the email and password with the correct credentials.

NoteIf you don’t have a customer account, create one either using the Next.js Starter storefront or by following this guide .

Then, send a GET request to /store/customers/me/subscriptions to retrieve the customer’s subscriptions:

Code
1curl 'http://localhost:9000/store/customers/me/subscriptions' \2-H 'Authorization: Bearer {token}' \3-H 'x-publishable-api-key: {your_publishable_api_key}'

Where {token} is the token retrieved from the previous request.

To cancel a subscription, send a POST request to /store/customers/me/subscriptions/[id], replacing the [id] with the ID of the subscription to cancel:

Code
1curl -X POST 'http://localhost:9000/store/customers/me/subscriptions/01J2VB8TVC14K29FREQ2DRS6NA' \2-H 'Authorization: Bearer {token}' \3-H 'x-publishable-api-key: {your_publishable_api_key}'

Next Steps#

The next steps of this example depend on your use case. This section provides some insight into implementing them.

Use Existing Features#

To manage the orders created for a subscription, or other functionalities, use Medusa’s existing Admin API routes.

If your use case requires a subscription to have relations to other existing data models, you can create links to them, similar to step 2.

For example, you can link a subscription to a promotion to offer a subscription-specific discount.

Storefront Development#

Medusa provides a Next.js Starter storefront that you can customize to your use case.

You can also create a custom storefront. To learn how visit the Storefront Development section.

Was this page helpful?
Edit this page