---
title: "Subscriptions"
description: "Plans, checkout, the customer portal, and plan changes — powered by the Polar component and linked to your auth users, with the reactive entitlement cache inside the backend component."
canonical_url: "https://nuxt-backend.dev/docs/billing/subscriptions"
---
# Subscriptions

> Plans, checkout, the customer portal, and plan changes — powered by the Polar component and linked to your auth users, with the reactive entitlement cache inside the backend component.

`setupBilling(components, { email })` wires subscriptions, discounts, **prepaid credits**, and **gift purchases** via the [Polar](https://www.convex.dev/components/polar) component, linked to your auth users. The reactive **feature + credit cache** and the gift records live inside the all-in-one `backend` component (`components.backend.billing` / `components.backend.gifts`), so **you add nothing to your schema**. This page covers subscriptions; the siblings cover the [catalog](https://nuxt-backend.dev/docs/billing/catalog), [credits](https://nuxt-backend.dev/docs/billing/credits), [gifts](https://nuxt-backend.dev/docs/billing/credits/gifts), and [feature gating](https://nuxt-backend.dev/docs/billing/feature-gating).

## Server setup

`setupBilling` needs **one option**, `email`, the app's `setupEmail` instance, which sends the gift notification. The access token, environment, and webhook secret default to the `BILLING_ACCESS_TOKEN` / `BILLING_ENVIRONMENT` / `BILLING_WEBHOOK_SECRET` env vars, and the billing entity resolves from identity claims — the active workspace by default (`billTo: 'organization'`, so members share the workspace's plan and credits), or the signed-in user with `billTo: 'user'` for B2C apps. The scaffolded `billing.ts`, as generated:

```ts [backend/billing.ts]
import { setupBilling, type DiscountInput } from 'nuxt-backend/billing'
import { v } from 'convex/values'
import { components } from './_generated/api'
import { internalAction } from './_generated/server'
import { catalog } from './billing.generated'
import { email } from './email'

// The catalog id map (plans, packs, credit meters) comes from billing.generated.ts,
// written by `npx nuxt-backend billing sync`. Exported for ai.ts (metered actions spend through it).
export const billing = setupBilling(components, { catalog, email })

export const {
  generateCheckoutLink,
  generateCustomerPortalUrl,
  getConfiguredProducts,
  listAllSubscriptions,
  giftCheckout,
} = billing.api
export const {
  getCurrentSubscription,
  getFeatures,
  getCredits,
  syncEntitlements,
  syncProducts,
  getReceivedGifts,
  claimGift,
  getWebhookDeliveries,
  // Subscription lifecycle — each resolves the caller's own billing entity,
  // so a client can only ever act on its own subscription.
  updateSubscription,
  cancelSubscription,
  uncancelSubscription,
  pauseSubscription,
  resumeSubscription,
  // Order history, invoices and metered usage, read live from the provider —
  // this package keeps no local order or usage table.
  getOrders,
  getInvoiceUrl,
  getUsageHistory,
  // Admin-tier: gated by `setupBilling({ requireAdmin })`, which defaults to
  // an `admin` role claim on the caller's identity.
  refundOrder,
} = billing.functions
export const { webhookEvents } = billing

// Discounts are privileged — an internalAction, run from ops:
//   npx convex run billing:createDiscount '{"name":"Launch","percent":20}'
export const createDiscount = internalAction({
  args: { name: v.string(), percent: v.number(), code: v.optional(v.string()) },
  handler: async (ctx, { name, percent, code }) => {
    const basisPoints = Math.round(Math.min(Math.max(percent, 0), 100) * 100)
    const discount: DiscountInput = { type: 'percentage', name, code, duration: 'once', basisPoints }
    return billing.discounts.create(discount)
  },
})
```

<warning>

Keep these exports. The composables bind to them by name (`useBilling` → `getConfiguredProducts` / `listAllSubscriptions` / `getCurrentSubscription` / `generateCheckoutLink` / …), and `npx nuxt-backend doctor` verifies the contract against the deployment — a trimmed export degrades the matching composable to `undefined`s.

</warning>

`email` is the one required option; every other option overrides a default:

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        email
      </code>
    </td>
    
    <td>
      Required. The app's <code>
        setupEmail
      </code>
      
       instance: a paid gift's recipient hears of it through its <code>
        gift
      </code>
      
       <a href="https://nuxt-backend.dev/docs/email/templates">
        template
      </a>
      
      . <code>
        setupBilling
      </code>
      
       checks it when it runs, so an <code>
        email
      </code>
      
       that arrives undefined — an import cycle through <code>
        email.ts
      </code>
      
       does that despite the type — fails the deploy instead of the first paid gift.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        accessToken
      </code>
      
       · <code>
        environment
      </code>
      
       · <code>
        webhookSecret
      </code>
    </td>
    
    <td>
      Override the <code>
        BILLING_ACCESS_TOKEN
      </code>
      
       / <code>
        BILLING_ENVIRONMENT
      </code>
      
       / <code>
        BILLING_WEBHOOK_SECRET
      </code>
      
       env vars.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        billTo
      </code>
    </td>
    
    <td>
      Who owns subscriptions and credits: <code>
        'organization'
      </code>
      
       (the default — members share the workspace's plan and credits) or <code>
        'user'
      </code>
      
       (B2C apps without shared billing).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        resolveEntity
      </code>
    </td>
    
    <td>
      Resolve the billing entity yourself, in place of the claims <code>
        billTo
      </code>
      
       reads: <code>
        (ctx) => Promise<{ userId, email } | null>
      </code>
      
      , where <code>
        ctx
      </code>
      
       has <code>
        auth
      </code>
      
       and <code>
        runQuery
      </code>
      
      . One resolver serves every context — the reactive reads, checkout and the portal, spends and the subscription lifecycle. Return <code>
        null
      </code>
      
       when nobody is signed in: reads answer <code>
        null
      </code>
      
       and provider operations refuse.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        catalog
      </code>
    </td>
    
    <td>
      The environment-keyed id map from <code>
        billing.generated.ts
      </code>
      
       — fills <code>
        products
      </code>
      
       and <code>
        credits
      </code>
      
       for the active <code>
        BILLING_ENVIRONMENT
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        products
      </code>
      
       · <code>
        credits
      </code>
    </td>
    
    <td>
      An explicit product map (catalog key → product id) and named credit meters (<code>
        spendCredits({ meter: 'credits' })
      </code>
      
      , <code>
        useCredits('credits')
      </code>
      
      ). Explicit entries win over <code>
        catalog
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rateLimiter
      </code>
    </td>
    
    <td>
      Your <code>
        setupRateLimiter(...)
      </code>
      
       instance. Throttles <strong>
        every client-callable function that reaches the live provider
      </strong>
      
       — <code>
        syncEntitlements
      </code>
      
      , <code>
        syncProducts
      </code>
      
      , checkout, the subscription-lifecycle actions, order history and invoices — against the <code>
        billingSync
      </code>
      
       limit (10/min, keyed by the workspace or user), so a caller can't loop one to amplify the provider fan-out. <code>
        giftCheckout
      </code>
      
       counts against the same limit, keyed by the purchaser. Omit to leave them unthrottled.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        multipleSubscriptions
      </code>
    </td>
    
    <td>
      Let one entity hold several live subscriptions at once (a plan plus add-ons). Switches <code>
        getCurrentSubscription
      </code>
      
       to the array-first <code>
        CurrentSubscriptions
      </code>
      
       shape — <code>
        subscriptions
      </code>
      
       leads, and the primary subscription's fields ride alongside so <code>
        subscription.productId
      </code>
      
       / <code>
        .status
      </code>
      
       keep reading unchanged — and makes <code>
        subscriptionId
      </code>
      
       the way the lifecycle actions pick their target. Off, the upstream single-subscription read throws the moment a second one exists.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        requireAdmin
      </code>
    </td>
    
    <td>
      Gate for the admin-tier actions (<code>
        refundOrder
      </code>
      
       — moving real money): throw to refuse. The default requires an <code>
        admin
      </code>
      
       role claim on the caller's identity; supply your own to check permissions, a workspace role, or an allowlist.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        canGift
      </code>
      
       · <code>
        giftLimit
      </code>
    </td>
    
    <td>
      Who may receive a gift, on top of the package's own rules, and how many gifts one purchaser may buy in a window (10 in 30 days by default). See <a href="https://nuxt-backend.dev/docs/billing/credits/gifts">
        Gifts
      </a>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        events
      </code>
    </td>
    
    <td>
      Webhook handlers keyed by the provider's own event names (<code>
        'order.paid'
      </code>
      
      , <code>
        'subscription.active'
      </code>
      
      , …). They run <strong>
        after
      </strong>
      
       the built-in entitlement-cache refresh and gift fulfilment, so features and credits read fresh inside them; events outside the built-in refresh set are mounted too.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        onUnknownEvent
      </code>
    </td>
    
    <td>
      Called for an <strong>
        authentic
      </strong>
      
       (signature-verified) webhook event whose type the installed provider SDK cannot parse — one newer than the package, say. The delivery is acknowledged with 202 either way, so an unknown type never puts the endpoint into a retry loop.
    </td>
  </tr>
</tbody>
</table>

`setupBilling(...)` returns:

<field-group>
<field name="api">

Checkout / portal / product / subscription functions to re-export, plus `giftCheckout` (the purchaser's checkout of a credit pack for someone else). `listAllSubscriptions` is wrapped to return `null` instead of throwing for claimless callers.

</field>

<field name="functions">

Reactive functions to re-export so the composables work with zero wiring: `getCurrentSubscription` (→ `useBilling`), `getFeatures` (→ `useFeatures`), `getCredits` (→ `useCredits`), `getReceivedGifts` / `claimGift` (→ `useGifts`), `syncEntitlements` (refresh the cache on demand), `syncProducts` (pull the provider's product catalog into the reactive products table — fresh deployments render empty pricing until it runs once; webhooks keep it fresh afterwards), `getWebhookDeliveries` (the delivery log behind DevTools; admins only, `null` for anyone else), the subscription lifecycle actions `updateSubscription` / `cancelSubscription` / `uncancelSubscription` / `pauseSubscription` / `resumeSubscription`, the live provider reads `getOrders` / `getInvoiceUrl` (→ `useOrders`, `<BillingHistory>`) and `getUsageHistory` (→ `useUsage`, `<UsageHistory>`), and the admin-tier `refundOrder`.

</field>

<field name="forgetEntity">

`forgetEntity(ctx, userId)` drops one entity's cached entitlements (plans, benefits, credit balances, reservations) — the app-side erasure step for account deletion, called from the `onUserDeleted` auth hook with the deleted user's id, or with a dissolved workspace's id. The provider's customer record is untouched.

</field>

<field name="webhookEvents / webhookHandler">

Typed handlers over the provider's full event catalog that keep the cache fresh (subscriptions, benefit grants, credit balances), patch benefit metadata, and fulfil gifts — and the guarded endpoint [`registerBackendRoutes`](https://nuxt-backend.dev/docs/ship/production/webhooks) mounts at `/billing/events`.

</field>

<field name="spendCredits · reserveCredits · settleSpend · releaseSpend · refundCredits">

The prepaid-credit primitives — see [Credits & top-ups](https://nuxt-backend.dev/docs/billing/credits).

</field>

<field name="updateSubscription · cancelSubscription · uncancelSubscription · pauseSubscription · resumeSubscription">

The same lifecycle operations as server-side methods, for your own actions. These accept the full `proration` set (`invoice`, `prorate`, `next_period`, `reset`); the registered action deliberately accepts only `invoice` / `prorate` from a client, because `next_period` and `reset` hand over a new plan immediately while deferring or waiving the charge.

</field>

<field name="getOrders · getInvoiceUrl · getUsageHistory · refundOrder">

Order history, invoice URLs and metered consumption, read live from the provider — there is no local order or usage table, because the provider already is the ledger. Each returns `null` when no access token is configured. `refundOrder` is admin-tier (`requireAdmin`).

</field>

<field name="checkout · portal">

The checkout and the customer portal from server code, for your own actions: `checkout` takes what `generateCheckoutLink` takes plus the server-only `trial` and `discountId` (see [Checkout](https://nuxt-backend.dev/docs/billing/subscriptions/checkout)); `portal(ctx, { returnUrl? })` opens the portal for the caller's billing entity, as the caller's own member of a workspace's billing customer.

</field>

<field name="getCustomerState · resolveEntity · discounts">

Live customer state, the caller's billing entity resolved the way every billing function resolves it, and discount management (`discounts.create` / `.list` / `.remove` — privileged, so server-side methods rather than registered functions).

</field>
</field-group>

## Reading subscription state

```vue
<script setup lang="ts">
const billing = useBilling()
// billing.products      — configured products keyed by catalog key (name, id, prices live from the provider)
// billing.subscription  — the active subscription, null on the free plan, undefined while loading
// billing.subscriptions — every subscription incl. ended/expired trials
// billing.isSubscribed / billing.isFree / billing.isLoading
// billing.status        — the provider status (active, trialing, past_due, paused, …)
// billing.cancelAtPeriodEnd / billing.trialEnd / billing.isTrialing / billing.isPaused
</script>

<template>
  <p v-if="billing.isSubscribed.value">On {{ billing.subscription.value?.productId }}</p>
</template>
```

Signed-out visitors are simply on the free plan: the user-scoped queries are skipped and `subscription` resolves to `null`.

## One customer per billing entity

Every billing entity pays through a provider customer of its own, keyed by the entity's id as the provider's external id: a workspace's id, or a user's with `billTo: 'user'`. The first checkout creates it, and every live call after that finds it by that id: customer state, usage events, orders and the portal. Webhooks resolve the entity from the customer's external id first, then the entity tag in its metadata, then the cache.

- **A workspace** is a team customer named after it, with the workspace's owner as its owner member. One person's email can stand behind every workspace they own, and each still bills through its own customer, with its own plan, credits and orders. Members are never seats: the customer pays, and its owner opens the portal.
- **A user** (`billTo: 'user'`) is an individual customer with the user's email. An email the provider already keeps for another account is refused with `forbidden` (reason `customer_taken`), since the provider keeps an individual customer's email unique.
- **An earlier customer** of the owner's verified email that no entity keyed, one the provider made from a checkout that named no customer, say, becomes the entity's own for a personal workspace or a user. Never for a shared workspace, never one that another entity holds, and never for an email its owner has not proved by a sign-in code.
- **A customer from before 0.2**, mapped by the old email lookup and carrying no external id, gets the entity's id on its first checkout or entitlement read: production billing from 0.1 carries over with nothing to run. One that two entities shared is refused for the second as `forbidden` (reason `customer_taken`), and the deployment logs `billing.customer_mapping_shared` until its billing data is reset.

Trials run once per owner: a checkout for a workspace whose owner already had a trial starts without one, whatever the product or the server offers. The package records a trial by the owner's user id when a subscription starts one, so deleting the workspace that trialed, or handing it over, changes nothing. A checkout that offers a trial holds it for its workspace for an hour, so two first checkouts of one owner's workspaces at once can't both start one.

The billing portal opens as the caller's own member of the workspace's customer, so it shows who is signed in and what they may do there. Only its owner or a billing manager gets one: another member is refused with `forbidden` (reason `not_billing_manager`). Workspace admins are not billing managers yet. The member is found by the caller's user id; an adopted customer's owner member has none (the provider sets it only at creation), so it is matched by the caller's verified email instead, and only a member no account is keyed to. That owner loses the portal if they change their email, until the operator updates the member at the provider.

## Environment

<table>
<thead>
  <tr>
    <th>
      Variable
    </th>
    
    <th>
      Purpose
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        BILLING_ACCESS_TOKEN
      </code>
    </td>
    
    <td>
      The provider organization access token — unset, billing queries return empty and checkout actions fail on invocation. Workspaces need its <code>
        members:write
      </code>
      
       and <code>
        customer_sessions:write
      </code>
      
       scopes too, for team customers and the member's portal
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        BILLING_WEBHOOK_SECRET
      </code>
    </td>
    
    <td>
      Verifies webhooks on <code>
        /billing/events
      </code>
      
       — unset, events are rejected (503) and entitlements refresh only on demand
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        BILLING_ENVIRONMENT
      </code>
    </td>
    
    <td>
      <code>
        sandbox
      </code>
      
       (the default) or <code>
        production
      </code>
    </td>
  </tr>
</tbody>
</table>

All three are optional at deploy time and forwarded from `.env.local` by `npx nuxt-backend env push`; see the [configuration reference](https://nuxt-backend.dev/docs/reference/configuration/environment-variables#convex-deployment-variables).

## Next

<card-group>
<card icon="i-lucide-shopping-cart" title="Open checkout and the portal" to="https://nuxt-backend.dev/docs/billing/subscriptions/checkout">

Subscribe, top up, change or cancel a plan.

</card>

<card icon="i-lucide-ticket-percent" title="Offer discounts" to="https://nuxt-backend.dev/docs/billing/subscriptions/discounts">

Mint discounts on the server; customers type codes at checkout.

</card>

<card icon="i-lucide-receipt" title="Show orders, invoices and usage" to="https://nuxt-backend.dev/docs/billing/subscriptions/orders">

Past charges, invoice links, metered consumption and refunds.

</card>
</card-group>

<page-keys>

- Try it live in the playground: [/playground/platform/billing](https://nuxt-backend.dev/playground/platform/billing)
- API reference: [/docs/reference/api/use-billing](https://nuxt-backend.dev/docs/reference/api/use-billing)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/billing.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/billing.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
