core-subscriptions

Subscription lifecycle management — checkout, tokens, discount codes, feature gates, and dunning.

core-subscriptions

Primitive Free

core-subscriptions manages the full subscription lifecycle on top of core-payments: checkout and customer portal sessions, plan changes, discount codes, usage-based token balances, feature gating, and dunning for failed payments.

What it does

  • Checkout and portal sessions through any registered payment provider.
  • Discount codes — percentage, fixed-price, and trial discounts with per-plan overrides, usage limits, and time-limited offers.
  • Token balances — race-safe usage-based credit accounting (addTokens, deductTokens).
  • Feature gates — gate functionality by the org's plan.
  • Dunning — handle past-due and unpaid subscriptions.
  • Admin billing analytics — MRR, churn, plan distribution, revenue over time.

Installation

bun run indigo add core-subscriptions

After installing, generate and apply the schema:

bun run db:generate
bun run db:migrate

ℹ️ Info

core-subscriptions depends on core-payments for payment provider and transaction access (wired via DI). Install core-payments first.

Configuration

Dependencies are wired through setSubscriptionsDeps() in config/deps/subscriptions-deps.ts. It supplies getPlans / getPlan / getPlanByProviderPriceId / getProviderPriceId, resolveOrgId, sendOrgNotification, enqueueTemplateEmail, and broadcastEvent.

  1. Wire dependencies

    Create config/deps/subscriptions-deps.ts (scaffolded on install) and import it as a side-effect in server.ts.

  2. Define plans

    Define your plans in config/plans.ts and call setPlanResolver() so the feature-gate can resolve plans.

  3. Sync and migrate

    Run bun run indigo:sync to register routers and schema, then bun run db:generate and bun run db:migrate.

Webhook routes remain in the project under app/api/webhooks/.

Schema

Table Notable columns
saas_subscriptions organizationId, providerId, providerCustomerId, providerSubscriptionId, planId, status, currentPeriodStart/End, cancelAtPeriodEnd, trialEnd, gracePeriodEndsAt
saas_discount_codes code (unique), discountType, discountValue, trialDays, trialPriceCents, planSpecificDiscounts, maxUses, maxUsesPerUser, validFrom/Until, timeLimitHours
saas_discount_usages userId, discountCodeId, planId, appliedAt, usedAt, removedAt, transactionId
saas_token_balances organizationId (unique), balance, lifetimeAdded, lifetimeUsed
saas_token_transactions organizationId, amount (signed), balanceAfter, reason, metadata — append-only ledger

API

billingRouter (mounted as billing):

Endpoint Access Purpose
billing.getPlans protected List available plans
billing.getProviders protected List enabled payment providers
billing.getSubscription protected Current org subscription (defaults to free)
billing.createCheckoutSession protected Start checkout for a plan/interval
billing.createPortalSession protected Open the provider customer portal
billing.applyDiscountCode protected Validate and apply a discount code
billing.removeDiscountCode protected Remove the user's active discount
billing.getActiveDiscount protected Get the user's currently applied discount
billing.renewSubscription protected Renew an expired/past-due subscription
billing.getTokenBalance protected Org token balance and lifetime totals
billing.getTokenTransactions protected Token ledger history
billing.getStats admin (billing) MRR, churn, plan distribution, revenue
billing.listSubscriptions admin (billing) Paginated subscription list with filters
billing.listChurned admin (billing) Canceled/past-due/unpaid subscriptions
billing.listDiscountCodes admin (billing) Discount codes with usage stats
billing.revenueOverTime admin (billing) Revenue time series
billing.addTokens admin (billing) Credit tokens to an org
billing.deductTokens admin (billing) Debit tokens from an org

discountCodesRouter (mounted as discountCodes) — admin (billing) CRUD:

Endpoint Access Purpose
discountCodes.list admin (billing) List discount codes
discountCodes.get admin (billing) Get a single discount code
discountCodes.create admin (billing) Create a discount code
discountCodes.update admin (billing) Update a discount code
discountCodes.delete admin (billing) Delete a discount code
discountCodes.getUsageStats admin (billing) Usage stats for a code

Integration

  • Token mutations addTokens() and deductTokens() use an atomic UPDATE WHERE balance >= amount and are race-safe — call them from feature handlers for usage-based billing.
  • Dunning checks (runDunningChecks) process past-due and unpaid subscriptions.
  • Nav items are registered under the billing group (Overview, Discount Codes).
  • The seed seedBilling creates demo users, orgs, subscriptions, and token balances.
Last updated: 5/27/2026Source: mdx file