Monthly billing is the default because it is easy to sell. Annual billing is what keeps your business alive.
The average monthly SaaS subscriber churns within 2-3 months. An annual subscriber commits upfront, pays once, and is statistically still your customer a year later. One annual conversion is worth roughly 4-5 monthly subscribers -- and it requires zero extra engineering work beyond what you already have.
With the Next.js SaaS Boilerplate and Stripe, adding annual billing is a one-afternoon change.
You need four things:
You do not need a new billing system. Stripe manages the subscription interval. Your app just needs to pass the correct price ID.
For every plan you have -- say, Pro at $29/month -- create a second price:
In the Stripe Dashboard: open your product, add a new price, set the billing period to Yearly, and copy the new price ID. Then add both IDs to your environment:
NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY=price_xxx
NEXT_PUBLIC_STRIPE_PRICE_PRO_ANNUAL=price_yyy
The NEXT_PUBLIC_ prefix makes both available to your pricing toggle in the browser.
Your checkout API route already accepts a priceId from the client. Nothing changes in the route itself -- you just pass the annual price ID when the user selects annual:
// POST /api/stripe/checkout
const { priceId } = body; // sent by the pricing toggle
const session = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId,
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?upgraded=true`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
});
The billing interval -- monthly or annual -- is embedded in the price itself. Stripe handles the renewal cadence without any extra logic on your end.
When the checkout.session.completed webhook fires, retrieve the subscription to get the interval and store it:
const subscription = await stripe.subscriptions.retrieve(session.subscription);
const interval = subscription.items.data[0].price.recurring.interval; // 'month' or 'year'
await userRepo.updateByStripeCustomerId(customerId, {
stripePriceId: subscription.items.data[0].price.id,
stripeSubscriptionInterval: interval,
});
Add a stripe_subscription_interval text column to your users table. This lets you show "Annual plan" vs "Monthly plan" in the account settings page without an extra Stripe API call on every page load.
This is the support request you will receive most often after adding annual billing. Wire up a one-click upgrade endpoint:
// POST /api/stripe/upgrade-to-annual
const user = await getUserFromRequest(req);
const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId);
await stripe.subscriptions.update(subscription.id, {
items: [{ id: subscription.items.data[0].id, price: annualPriceId }],
proration_behavior: 'create_prorations',
});
Stripe prorates the remaining monthly days as a credit toward the annual plan. Your customer pays the annual price minus what they already paid this billing cycle -- no double-charge, no manual refund. The same proration logic covered in the plan switching guide applies here.
The annual toggle UI is covered in the pricing page guide. The one thing most founders get wrong is how they display the discount.
"Save 20%" converts worse than "Save $72/year." A percentage feels like marketing copy. A dollar amount feels real and concrete. Show both -- the monthly equivalent price ($23/mo billed annually) and the annual savings in dollars.
Two patterns that improve annual conversion:
Founders who switch the default to Annual typically see 25-40% of new subscribers choose the annual plan within the first week.
Monthly churn of 5% sounds manageable. Compounded over 12 months, that is 46% of your customers gone every year.
An annual subscriber who already paid is not going anywhere for 12 months. One billing interval change eliminates a full year of churn risk for every annual subscriber you acquire. It also front-loads cash: an annual payment on a $29/month plan means $278 in your account today instead of $29 dribbling in month by month.
Most SaaS founders treat annual billing as a late-stage optimization. It is actually one of the first things worth adding once your core product is working.
The Next.js SaaS Boilerplate ships with Stripe subscriptions, webhook handling, a user settings page, and all the plumbing you need. Adding annual billing means two new price IDs in Stripe, two environment variables, and a single toggle in your pricing page. Everything else is already there -- clone the repo and start with the getting started guide.