GenerateSaaS

Credits

Add usage-based credits to your SaaS with per-plan grants, recurring re-grants, custom top-ups, and automatic refills scoped to users or organizations.

A usage meter owned by @repo/payments, configured in pricingConfig (packages/config/src/pricing.ts) and gated by pricingConfig.credits.enabled (default true). pricingConfig is a separate named export from @repo/config, not a key on the config object. With credits.enabled: false, grants and the credits UI are suppressed; balances simply stay at 0.

Storage

Balances belong to a billing entity - user or organization, per config.tenancy.billingScope. Better Auth persists them through billingAdditionalFields (packages/config/src/billing-fields.ts), all input: false so only the server mutates them via the Billing(entityId) accessor.

FieldDefaultPurpose
credits0Current balance.
creditsLastGrantedAt-Timestamp of last recurring grant.
autoTopUpEnabledfalsePer-entity auto top-up opt-in.
autoTopUpThreshold-Balance that triggers a top-up.
autoTopUpAmount-Credits bought per top-up.

The Billing(...) accessor (packages/payments/src/billing.ts) exposes addCredits, removeCredits, setCredits, and a hasCredits getter.

Per-plan grants

Each plan in pricingConfig.plans may carry these credit fields (PricePlan in packages/config/src/types/index.ts):

KeyTypeDefaultDescription
creditsnumber0Credits the plan includes per cycle.
creditIntervalnumber (days)0How often credits re-grant. Omit or 0 to disable recurring grants.
grantCreditsOnTrialbooleanunset = grantDuring a trialDays trial, credits are granted unless set to false (the runtime check is !== false).

The shipped plans grant free: 5, starter: 10, pro: 50, all on creditInterval: 30. Resolve them with getCreditsForPlan(planId) / getCreditIntervalForPlan(planId) (return 0 when absent).

grantCreditsOnTrial is opt-out: webhook handlers grant trial credits whenever plan.grantCreditsOnTrial !== false. Set it to false to withhold credits until the first paid invoice.

Recurring re-grants

The recurring-credits job (packages/api/src/functions/billing/recurring-credits.ts) runs daily on cron 0 0 * * *.

Collect plans where creditInterval > 0, then load active-plan entities for the billing scope.
For each, compare creditsLastGrantedAt + creditInterval against now; skip those not yet due.
Claim a per-day idempotency key, addCredits(...), update creditsLastGrantedAt, and write a CREDITS_ADDED audit entry.

See Background jobs for the scheduler and Audit logs for the entries.

Custom purchases and auto top-up

The shipped config is credits: { enabled: true } with no customPurchase block, so custom purchases and auto top-up are off. Add pricingConfig.credits.customPurchase to let users buy arbitrary credits beyond their plan; these are the fields you set when you add it:

KeyTypeDescription
enabledbooleanAllow custom credit purchases.
autoTopUpbooleanAllow users to opt into automatic refills.
minimumCreditsnumberFloor per custom purchase.
presetCreditsnumberSuggested amount in the purchase UI.
stripeProductId / polarProductIdstring?Metered line item per provider.

After every removeCredits call, checkAndTriggerAutoTopUp runs. With no customPurchase block it returns early and nothing fires. Once you add the block, a top-up fires only when all hold: customPurchase.enabled and customPurchase.autoTopUp are on, the active provider is "stripe", the entity has set autoTopUpEnabled/autoTopUpThreshold/autoTopUpAmount, and the balance is below autoTopUpThreshold.

Auto top-up is Stripe-only. Under Polar, users can still buy credits manually, but checkAndTriggerAutoTopUp returns early and no automatic refill runs.

Notifications

When credits land, @repo/notifications fires the credits_added trigger (notifyCreditsAdded), creating an in-app notification linking to billing settings - gated by config.notifications.enabled (default true).

notifyUser is user-scoped only - it is a no-op for organizations. Under config.tenancy.billingScope: "organization", org credits are granted but no credits_added notification fires.

Frequently asked questions

Who owns a credit balance - the user or the org?

Whichever config.tenancy.billingScope selects. The same Billing(entityId) accessor routes to the users or organizations table accordingly.

Are credits granted during a free trial?

Yes, by default - trial credits are granted unless the plan sets grantCreditsOnTrial: false.

Why don't org members see a credits notification?

The notification system is user-scoped; notifyUser no-ops for organizations, so org-scoped grants move the balance silently.

On this page