packages/payments knows the plan catalog and talks to the payment provider. It does not know about studios, trainees or limits enforcement. That is modules/billing in core-api. The end to end flow is on Billing and plan limits.
src/index.ts exports:
config) is not exported. Consumers go through the helpers.
Types (types.ts)
Money in this package is in major currency units (
300 means 300 ILS). Domain prices for trainee plans are stored in agorot (priceAgorot). Do not mix the two.
Plans (lib/plans.ts)
Provider ids (lib/provider-price-ids.ts)
Despite the name, a “price id” here is a Polar product id.
Studio plan resolution (lib/studio-plan.ts)
planId can be null on a billed purchase whose product is not in the catalog. The caller decides what that means. core-api reads it as the custom plan with no limits.
Purchases helper (lib/helper.ts)
The file exports createPurchasesHelper(purchases), returning { activePlan, hasSubscription, hasPurchase }, and the ResolvedPurchase type. It is not re-exported from the package index. It predates the studio plan resolver and treats “no purchase” as plan id free when requireActiveSubscription is false. Use resolveStudioPlanPurchase for anything about a studio’s plan.
Customer link (lib/customer.ts)
Subscription change events (lib/subscription-events.ts)
The listener set lives on
globalThis under Symbol.for('perform.payments.subscriptionChangeListeners'), so two copies of the module loaded through different paths still share it.
Provider (provider/)
provider/index.ts re-exports ./polar.
provider/stripe/index.ts implements the same function types against Stripe. It is in the tree and not exported. Switching provider means changing the one export line, supplying the Stripe env and reviewing every place that assumes Polar’s product based ids and seat model.
Tests
lib/catalog.test.ts checks the catalog and the lookup helpers. provider/polar/polar.test.ts covers the provider.
Changing the catalog
- Edit
src/config.ts. Keep the current price first and the legacy price second for each paid plan. - Create the Polar product and supply its id through the matching env variable.
- Never remove a legacy price while a studio still holds a purchase on it. The purchase would stop resolving to a plan and the studio would read as
custom, with no limits. - Rebuild with
pnpm --filter @repo/payments build. Both@repo/apiand core-api read the package fromdist. - If the web app keeps its own copy of the catalog for its pricing screens, update it there too.