---
title: "@playstack/billing"
description: "Provider-neutral subscription projection, hosted billing actions, and entitlement synchronization."
tags: ["package","identity","billing","subscriptions","stripe","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/billing` keeps a rebuildable local projection of provider-owned customers and subscriptions. Provider adapters own checkout, customer portals, and normalized webhook translation; entitlements remain the application-facing authorization boundary.

{/* package-install:start */}

## Install

After confirming [preview access](/docs/packages#access-policy), install the package at your application's shared Playstack version:

```sh
npm install --save-exact @playstack/billing@0.1.0-beta.1
```

Check the peer requirements below before choosing a runtime or provider.

{/* package-install:end */}

## Define plans and compose

```ts
import { createBilling, definePlans } from '@playstack/billing'
import { stripeBilling } from '@playstack/billing/stripe'

const billing = createBilling({
  provider: stripeBilling(stripe, { instance: 'subscriptions' }),
  persistence,
  entitlements,
  entitlementConsistency: 'transactional',
  events,
  clock,
  plans: definePlans({
    pro: {
      prices: {
        monthly: {
          provider: 'stripe',
          instance: 'subscriptions',
          id: 'price_pro_monthly',
        },
      },
      entitlements: [{ entitlement: 'packages.pro', value: true }],
      trial: {
        durationMs: 14 * 24 * 60 * 60_000,
        graceMs: 7 * 24 * 60 * 60_000,
        graceEntitlements: [{ entitlement: 'workspace.read', value: true }],
      },
    },
  }),
})
```

Create checkout and portal sessions only from authenticated routes. Pass only verified, normalized provider events from `@playstack/webhooks` to `handleWebhook()`.

## Configuration

| Property                 | Required | Purpose                                                                                      |
| ------------------------ | -------- | -------------------------------------------------------------------------------------------- |
| `provider`               | Yes      | One provider instance and its native-client escape hatch.                                    |
| `plans`                  | Yes      | Code-defined prices, grants, legacy plans, and optional local trials.                        |
| `persistence`            | Yes      | Customers, current subscriptions, immutable revisions, and trial eligibility.                |
| `entitlements`           | Yes      | Replaces independently scoped subscription and trial grant sources.                          |
| `entitlementConsistency` | Yes      | Selects `transactional` for a shared store or `eventual` for durable cross-store projection. |
| `events`, `clock`        | Yes      | Provider-neutral lifecycle events and deterministic time.                                    |
| `pastDueGraceMs`         | No       | Access grace applied to past-due subscriptions; defaults to seven days.                      |

Every provider reference includes `{ provider, instance }`. Compose separate
billing services for multiple Stripe accounts or other providers. Customer,
price, subscription, checkout, webhook, and entitlement-source identities stay
isolated by that instance.

## Subscription state and repair

```ts
const current = await billing.getSubscription(accountId)
const subscriptions = await billing.listSubscriptions(accountId)
const revisions = await billing.listSubscriptionHistory(accountId)

await billing.reconcileSubscription(accountId, current.id)
```

Each accepted projection stores the provider event ID and occurrence time.
Exact retries and older events are no-ops, while every accepted state is added
to immutable history. Scheduled provider transitions appear as `pendingPlan`,
`pendingProviderPriceId`, and `pendingChangeAt`; the current plan remains
authoritative until a later provider event applies the transition.

Reconciliation fetches the provider’s current subscription and sends it through
the same ordering, history, event, and entitlement path. The application cannot
write subscription state directly.

## Adopt an existing provider projection

```ts
const customer = await billing.previewCustomerAdoption({
  accountId,
  providerCustomerId,
})
const subscription = await billing.previewSubscription(accountId, providerSubscriptionId)

await billing.adoptCustomer({ accountId, providerCustomerId })
await billing.importSubscription(accountId, providerSubscriptionId)
```

Preview operations are report-only and include field-level drift. Import retrieves provider-owned state and runs it through the ordinary ordering, history, events, and entitlement path; callers cannot inject a status or plan. Running previews beside an existing projection is the supported shadow phase, and retaining the old tables until reports remain clean provides the rollback boundary.

## No-card trials

```ts
const trial = await billing.startTrial({ accountId, plan: 'pro' })
const currentTrial = await billing.getTrial(accountId)
await billing.cancelTrial(accountId)
```

The trial’s full grants expire at `endsAt`; explicitly smaller grace grants
start then and expire at `graceEndsAt`. Time-bounded entitlements move the
account from active to grace to expired without a timer. Trial eligibility is
immutable per account, and an active paid subscription cancels its trial
sources.

## Lifecycle events

Billing emits provider-neutral events for subscription updates and terminal
cancellation, cancellation scheduling and resumption, scheduled and applied
plan changes, invoice payment, payment failure, provider trial ending, and
local trial start/cancellation. Notification, audit, and reconciliation code
therefore does not need Stripe payload knowledge.

## Boundary

The provider remains the subscription source of truth, while product code checks stable entitlements rather than plan or price names. The application owns raw-body webhook routing and verification, redirects, migrations, reconciliation scheduling, and product-specific downgrade behavior. `billing.provider.client` exposes the exact native billing client.

## Durable checkout and usage

Checkout now requires an explicit, globally unique `idempotencyKey`. Persistence atomically reserves immutable request identity before provider I/O and records completed or unknown outcomes. `getCheckoutAttempt()` supports reconciliation. Retry ambiguous outcomes with the same key; use a new key only for an intentional new checkout, not to bypass an unknown result.

`createBillingUsage()` records idempotent usage and claims bounded aggregates for provider reporting. Adopters supply the persistence/reporting seams and product allowance, overage and quota policies. A usage total is not an atomic quota reservation.

Subscription projection holds a customer lock across ordering checks, history and transactional entitlement projection. Stripe period normalization supports subscription-level and item-level period fields; the earliest item boundary is the conservative aggregate end. Refund/dispute policy and resource-license grants are separate from subscription billing. Test shadow/import/reconciliation and rollback against real application data before cutover.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/billing@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/billing` | `./dist/index.d.ts` |
| `@playstack/billing/errors` | `./dist/errors.d.ts` |
| `@playstack/billing/prisma` | `./dist/prisma.d.ts` |
| `@playstack/billing/testing` | `./dist/testing.d.ts` |
| `@playstack/billing/usage/prisma` | `./dist/usage-prisma.d.ts` |
| `@playstack/billing/usage/stripe` | `./dist/usage-stripe.d.ts` |
| `@playstack/billing/usage/testing` | `./dist/usage-testing.d.ts` |
| `@playstack/billing/stripe` | `./dist/stripe.d.ts` |
| `@playstack/billing/webhooks` | `./dist/webhooks.d.ts` |
| `@playstack/billing/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
