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.
- Wire dependencies
Create
config/deps/subscriptions-deps.ts(scaffolded on install) and import it as a side-effect inserver.ts. - Define plans
Define your plans in
config/plans.tsand callsetPlanResolver()so the feature-gate can resolve plans. - Sync and migrate
Run
bun run indigo:syncto register routers and schema, thenbun run db:generateandbun 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()anddeductTokens()use an atomicUPDATE WHERE balance >= amountand 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
billinggroup (Overview,Discount Codes). - The seed
seedBillingcreates demo users, orgs, subscriptions, and token balances.