Not every recurring-revenue business fits Stripe Billing's assumptions cleanly. Here's how we handled tiered memberships, mid-cycle changes, and pauses for a subscription rental library on plain payment-gateway primitives.
By Team WebSync · · 4 min read

Off-the-shelf subscription billing tools handle the common case well: one plan, one price, upgrade or cancel any time. Real membership businesses rarely stay that simple for long - tiered packages, items on loan that need to be returned before a plan can downgrade, pauses for a family going on holiday, referral credits that offset a renewal. Once the rules stop being generic, you're often better off building billing logic on top of a payment gateway's raw primitives than fighting a subscriptions platform's opinions.
We built exactly that for a subscription toy-and-book rental library: catalogued products, tiered membership packages, and a referral engine, where the billing rules were specific enough to the business that no off-the-shelf platform matched them without workarounds of its own.
A subscription isn't just "paid" or "not paid" - it has a tier, a renewal date, a set of items currently on loan, and a history of pauses and referral credits. We modelled membership as its own state machine (ACTIVE, PAUSED, PAST_DUE, CANCELLED) independent of any single payment gateway's subscription object, so business logic like "can this member downgrade" checks our own rules against our own data, not a third party's schema.
Mid-cycle tier changes need a fair adjustment, and it's simpler to compute than it looks: take the unused portion of the current cycle (days remaining divided by cycle length, times the current tier's price) as a credit, and charge the difference against the new tier's prorated cost for the same remaining period. We calculate this server-side at the moment of the change and show the exact number before confirming - members trust proration a lot more when they can see the maths, not just a total.
function prorate(currentTierPrice, newTierPrice, daysRemaining, cycleDays) {
const unusedCredit = (daysRemaining / cycleDays) * currentTierPrice;
const newTierCost = (daysRemaining / cycleDays) * newTierPrice;
return Math.max(0, newTierCost - unusedCredit); // charge now, or 0 if downgrading
}Cancelling a membership and asking a member to resubscribe later loses their tenure, their referral history, and often the member entirely - the friction of signing up again is real. We built an explicit PAUSED state instead: billing stops, outstanding rentals are handled by their own return-by rules independent of billing, and resuming reactivates the same membership record with the same history intact, on the next cycle boundary rather than an arbitrary resume date.
A referral programme that just discounts the next invoice breaks the moment credits need to roll over, split across two referred members, or get reversed after a refund. We track referral credit as its own ledger - each grant and each redemption a separate row - so a renewal simply asks "what's this member's current credit balance" instead of trying to remember which coupon applies.
A subscriptions platform is optimised for the subscriptions its designers imagined. The moment your billing rules are specific to your business, that's a sign to own the state machine yourself.
Model membership as its own state machine - active, paused, past-due, cancelled - independent of the payment gateway, and compute proration server-side as unused-cycle credit against the new tier's cost. Give pauses a real hold state that preserves tenure and history, and reconcile charges against expected renewals nightly.
Book a free consult - we'll scope it and give you a fixed price.