Skip to main content
Skip to content

Resources

The SaaS boilerplate billing checklist: Stripe, webhooks, trials, renewals, and plan access

Last updated 4 June 2026 · Dan Tinsley, Halbon Labs

Billing is the part of a SaaS boilerplate where “demo-ready” and “production-ready” separate quickly.

A checkout button is not a billing system. A pricing table is not a subscription model. A success page is not proof that revenue, access, invoices, renewals, cancellations, and support cases will behave correctly after launch.

This checklist is for founders, agencies, and developers evaluating a Next.js SaaS starter before they trust it with money.

Start with the billing lifecycle, not the checkout page

Most billing bugs happen outside the happy path. The happy path is simple:

  1. User clicks a plan.
  2. User pays.
  3. App unlocks access.

Real billing includes the rest:

  • user starts a trial;
  • user upgrades mid-cycle;
  • user downgrades;
  • user changes card;
  • payment fails;
  • invoice is retried;
  • subscription renews;
  • subscription is cancelled;
  • cancellation takes effect later;
  • user requests a refund;
  • webhook arrives twice;
  • webhook arrives out of order;
  • support asks why a customer has access.

A SaaS boilerplate does not need to solve every advanced billing model. It does need to show that the author understands the lifecycle.

1. Products and prices should be configuration, not scattered code

A starter kit should make it obvious where plan IDs, price IDs, feature names, and access levels live.

Bad signs:

  • Stripe price IDs hard-coded in page components.
  • Feature access duplicated in multiple files.
  • Plan names used as permission logic.
  • No explanation of test vs live products.

Better signs:

  • A central billing configuration file.
  • Typed plan identifiers.
  • Separate display labels and internal entitlement keys.
  • Explicit test/live environment variable guidance.
  • Documentation for adding, removing, or renaming plans.

Your product will change. Your billing code should expect that.

2. Checkout should create intent, not final truth

Checkout is where the user starts payment. It should not be the only place your application decides someone has paid.

The success URL is useful for user experience, but it is not the source of billing truth. Users can close tabs. Redirects can be interrupted. URLs can be revisited. Payment methods can require delayed confirmation.

A safer pattern is:

  • create checkout session;
  • redirect user to Stripe;
  • receive verified webhook;
  • store billing state;
  • grant access from stored state;
  • show the user a success or pending state based on your database.

If a template grants access only from the success page, ask how it handles webhook delays, duplicate redirects, failed payments, and support reconciliation.

3. Webhook verification is mandatory

Stripe sends a signature header with webhook requests. Your endpoint should verify that signature using the raw request body and the correct endpoint secret.

This matters because webhooks update money-related state. An unauthenticated webhook endpoint is an invitation for abuse. A fragile webhook endpoint is a support problem waiting to happen.

A serious SaaS starter should include:

  • a dedicated webhook route;
  • signature verification;
  • raw request body handling;
  • endpoint secret documentation;
  • clear local testing steps;
  • safe errors that do not leak secrets;
  • logging for rejected events.

The webhook should fail closed. If verification fails, the handler should reject the event and leave billing state unchanged.

4. Webhook handlers should be idempotent

Payment platforms retry events. Networks fail. Servers crash. Developers replay events during testing.

That means a webhook handler must be safe to run more than once.

A good template records event IDs before applying side effects, or otherwise ensures each event can be processed once without duplicating results. This is especially important for:

  • account upgrades;
  • welcome emails;
  • invoice notifications;
  • audit log rows;
  • credit allocation;
  • usage limits;
  • team invitations triggered by purchase.

Ask where processed events are stored. If the answer is “we don’t need that,” treat it as a risk.

5. Subscription state should be explicit

A SaaS app should not guess access from scattered Stripe fields. It should store the state it needs to answer product questions.

At minimum, you probably need:

  • customer ID;
  • subscription ID;
  • current plan or price ID;
  • subscription status;
  • current period end;
  • cancellation status;
  • trial end;
  • updated timestamp;
  • source event ID.

The exact schema depends on the product. The principle does not: your app should have a local billing state that is updated by verified events.

6. Access checks should be centralised

Plan access should not be implemented as random conditionals across the interface.

Avoid this pattern:

ts
if (user.plan === 'pro') {
  // show feature
}

Prefer a central access function:

ts
canUseFeature(user, 'advanced_exports')

This makes it easier to support trials, grandfathered plans, usage limits, coupons, beta flags, admin overrides, and future pricing changes.

A boilerplate should make it easy to answer: who can do what, and why?

7. Trials need a real end state

Trials create edge cases immediately.

What happens when the trial ends? Does access stop instantly? Is there a grace period? Are users warned before the end date? Is payment required before the trial starts? Can a trial be restarted? What happens if a user cancels during the trial?

A starter kit does not need to impose your business policy, but it should expose the right pieces:

  • trial status;
  • trial end date;
  • reminder hooks;
  • conversion state;
  • cancellation state;
  • permission behaviour after expiry.

If trials are mentioned in marketing but not represented in code, you may be buying a promise rather than a system.

8. The billing portal should not be an afterthought

Most SaaS teams eventually need a way for customers to manage payment methods, invoices, and cancellations.

A good starter should document how the customer portal is opened and who is allowed to open it. It should also handle users without a Stripe customer ID gracefully.

Useful questions:

  • Does the app create or reuse customers consistently?
  • Can only the account owner or billing admin open the portal?
  • Is the return URL configured per environment?
  • Does the app refresh billing state after portal changes?
  • Are cancellation and downgrade events handled by webhook?

Do not assume “portal included” means the permissions and lifecycle are correct.

9. Failed payments should have a product state

Payment failure is not just a Stripe event. It is a product experience.

You need to decide:

  • does the user lose access immediately?
  • is there a grace period?
  • what banner appears?
  • can they update payment details?
  • does the admin see the account as delinquent?
  • are support logs clear?

A template can help by modelling failed-payment states and providing a place for the UI to respond.

Without that, your first failed invoice becomes a custom debugging session.

10. Billing logs reduce support pain

Support questions sound like this:

  • “I paid but still cannot access Premium.”
  • “Why was I charged?”
  • “I cancelled but still have access.”
  • “My invoice failed but my card is fine.”
  • “Can you check whether the webhook arrived?”

A billing event log gives you a source of truth. It does not need to be fancy. Even a small internal table with event ID, event type, customer ID, status, and error message can save hours.

If the starter includes an admin area, billing logs belong there. If it does not, they should at least be easy to inspect in the database.

11. Local billing tests should be documented

A billing integration is not complete until a buyer can test it.

The docs should explain:

  • how to create test products and prices;
  • how to set environment variables;
  • how to run the Stripe CLI;
  • how to forward events locally;
  • which events to trigger;
  • how to inspect the database after each event;
  • how to reset test state.

The less experience your buyer has with Stripe, the more valuable this documentation becomes.

12. Billing should connect to the rest of the app

Billing state is not isolated. It affects:

  • route access;
  • feature access;
  • team limits;
  • usage limits;
  • admin screens;
  • email notifications;
  • audit logs;
  • account deletion;
  • support workflows.

A production-ready boilerplate treats billing as a core system. A demo-ready one treats it as a checkout integration.

Quick audit checklist

Before you buy or adopt a SaaS starter, check:

  • Products and prices are centralised.
  • Checkout does not directly grant final access.
  • Webhooks are signature-verified.
  • Raw request body handling is documented.
  • Events are idempotent or deduplicated.
  • Subscription state is stored locally.
  • Trial behaviour is represented.
  • Customer portal flow is permission-aware.
  • Failed payments have a product state.
  • Plan access is centralised.
  • Billing logs exist.
  • Local Stripe CLI testing is documented.
  • Test and live environments are separated.

Template Empire angle

Template Empire’s full-stack templates treat billing as part of the application foundation, not a detached checkout snippet. The goal is to make the money path traceable: checkout, verified webhook, database state, access decision, and support evidence.

That is the difference between a starter that helps you launch and one that helps you stay launched.

Further reading