Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Annual Billing for Your Next.js SaaS -- Add Stripe Annual Plans, Offer a Discount, and Reduce Churn

October 25, 2026
nextjsstripesaasbillingchurn

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.

What You Are Actually Building

You need four things:

  • A monthly price and an annual price in Stripe for each plan
  • A checkout flow that sends the right price ID based on billing interval
  • A webhook handler that stores the interval in your database
  • A one-click flow for existing monthly subscribers to upgrade to annual

You do not need a new billing system. Stripe manages the subscription interval. Your app just needs to pass the correct price ID.

Step 1 -- Create Annual Prices in Stripe

For every plan you have -- say, Pro at $29/month -- create a second price:

  • Monthly: $29/month (your existing price ID)
  • Annual: $278/year -- that is $29 x 12 x 0.80, rounded to a clean number

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.

Step 2 -- Pass the Right Price ID at Checkout

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.

Step 3 -- Store the Interval in Your Database

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.

Step 4 -- Let Monthly Subscribers Upgrade to Annual

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.

Step 5 -- Show the Savings Clearly on Your Pricing Page

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:

  • Default the toggle to Annual rather than Monthly. Opt-out friction is lower than opt-in friction.
  • Add a "Best value" badge to the annual option. It sounds obvious but it works.

Founders who switch the default to Annual typically see 25-40% of new subscribers choose the annual plan within the first week.

Why This Changes Your Business Math

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.

Get Started This Weekend

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.