@playstack/billing
Provider-neutral subscription projection, hosted billing actions, and entitlement synchronization.
Free. MIT licensed. Check preview availability before installing. See package access.
@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.
Install
After confirming preview access, install the package at your application's shared Playstack version:
npm install --save-exact @playstack/billing@0.1.0-beta.1Check the peer requirements below before choosing a runtime or provider.
Define plans and compose
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
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
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
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.
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. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.