Documentation
Docs

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 composable.

The usePurchases composable 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
<script setup lang="ts">
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");
</script>

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.

Load the purchases with the getPurchasesByUserId or getPurchasesByOrganizationId query from @repo/database:

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

const purchases = organizationId
  ? await getPurchasesByOrganizationId(organizationId)
  : await getPurchasesByUserId(user.id);

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

When you load the purchases of an organization, 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

usePurchases also supports organizations. Simply pass the organization id as an argument.

<script setup lang="ts">
const { activeOrganization } = useActiveOrganization();
const { activePlan, hasAccess, hasSubscription, hasPurchase } = usePurchases(
  () => activeOrganization.value?.id,
);
</script>

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 = computed(
  () =>
    activePlan.value?.id === "pro" &&
    planStatusGrantsAccess(activePlan.value.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.