Documentation
supastarter for Next.jssupastarter for Next.jsPayments

Check for purchases or subscriptions

Learn how to check for purchases or subscriptions in your application to provide access to premium features.

One of the most common use cases for payments is to provide access to premium features.

Plan ID

The plan id is used to identify a plan. It is defined in the key property of the plan object from the config file like free, pro, enterprise or lifetime from the example config.

Check for purchases or subscriptions on client side

You can check if a user has a purchase or subscription by using the usePurchases hook.

The usePurchases hook returns the following properties:

  • activePlan: The active plan of the user, including its status and trialEndsAt (see Subscription status)
  • purchases: An array of all the purchases of the user
  • hasAccess: Whether the user can access the app. Always true when requireActiveSubscription is false. When the paywall is enabled, it is only true if the active plan's status grants access.
  • hasSubscription: A function to check if the user has an active subscription for a plan id or an array of plan ids. When the paywall is enabled, it returns false if the subscription status does not grant access.
  • hasPurchase: A function to check if the user has a specific purchase for a given plan id
function SomeComponent() {
  const { activePlan, hasAccess, hasSubscription, hasPurchase } =
    usePurchases();

  // check if the user has a subscription for the pro plan
  const hasProSubscription = hasSubscription("pro");
  // or check if the user has a subscription for the pro plan or the enterprise plan
  const hasProOrEnterpriseSubscription = hasSubscription(["pro", "enterprise"]);

  // check if the user has a purchase of the lifetime plan
  const hasLifetimeAccess = hasPurchase("lifetime");
}

Check for purchases on server side

You can also check for purchases and get the active plan on the server side by utilizing the createPurchasesHelper function, which will generate a bunch of helper functions based on the purchases you pass to it.

The createPurchasesHelper can be used in both server components and API procedures, but the way to fetch the purchases is different:

Server components

When you are inside your Next.js application in a server component, you can use the listPurchases function from @payments/lib/server.

import { listPurchases } from "@payments/lib/server";
import { createPurchasesHelper } from "@repo/payments/lib/helper";

const purchases = await listPurchases();

const { activePlan, hasAccess, hasSubscription, hasPurchase } =
  createPurchasesHelper(purchases);

API procedures

When you are inside an oRPC procedure, you can load the purchases with the getPurchasesByUserId or getPurchasesByOrganizationId query from @repo/database.

import { getPurchasesByUserId } from "@repo/database";
import { createPurchasesHelper } from "@repo/payments/lib/helper";

import { protectedProcedure } from "../../../orpc/procedures";

export const someProcedure = protectedProcedure.handler(
  async ({ context: { user } }) => {
    const purchases = await getPurchasesByUserId(user.id);
    const { activePlan, hasAccess, hasSubscription, hasPurchase } =
      createPurchasesHelper(purchases);

    // do something with the helpers here...
  },
);

When you load the purchases of an organization with getPurchasesByOrganizationId, verify that the user is a member of the organization first. See packages/api/modules/payments/procedures/list-purchases.ts for an example.

Check for purchases of organization

Both usePurchases and listPurchases also support organizations. Simply pass the organization id as an argument.

// ... get the organization slug from the url
const organization = await getActiveOrganization(organizationSlug);
const purchases = await listPurchases(organization.id);
const { activeOrganization } = useActiveOrganization();
const { activePlan, hasAccess, hasSubscription, hasPurchase } = usePurchases(
  activeOrganization!.id,
);

Subscription status

Each payment provider uses its own subscription statuses. The webhook handlers map them to a shared set of statuses before saving the purchase, so your application code only has to deal with these values for activePlan.status:

StatusDescriptionGrants access
activeThe subscription is paid and runningYes
trialingThe subscription is in its trial periodYes
cancelingThe subscription is canceled at the end of the current billing periodYes
incompleteThe first payment has not completed yetNo
past_dueA renewal payment failed and the provider is retryingNo
unpaidRenewal payments failed and the provider stopped retryingNo
pausedThe subscription is pausedNo
canceledThe subscription has been canceledNo
expiredThe subscription has endedNo

One-time purchases and the implicit free plan always have the status active.

For trials, the webhooks also store the end of the trial in the trialEndsAt field of the purchase. It is available as activePlan.trialEndsAt (a Date or null) and the billing page shows when the trial ends.

The "Grants access" column only affects the app when requireActiveSubscription is enabled (see Set up a paywall). If you want to gate a feature on a healthy subscription status without enabling the paywall, use planStatusGrantsAccess:

import { planStatusGrantsAccess } from "@repo/payments/lib/subscription-status";

const canUsePremiumFeature =
  activePlan?.id === "pro" && planStatusGrantsAccess(activePlan.status);

The shared statuses and helpers live in packages/payments/lib/subscription-status.ts. Each provider defines how its statuses map to them in packages/payments/provider/[provider]/subscription-status.ts, so removing a provider folder also removes its mapping.