Articles

Why I use Stripe for SaaS billing

Billing is one of those features that looks small until you ship it.

The checkout button is easy. The real work is everything after it: webhooks, subscription state, failed payments, cancellation, invoices, the billing portal, and making sure the app knows who should have access.

I use Stripe for this because it gives me the parts I want to own and hosts the parts I do not want to build.

What Stripe handles

Stripe Checkout hosts the payment page. That matters. I do not want card fields in my app, and I do not want to maintain the compliance and browser edge cases around card entry.

For a subscription SaaS, Stripe gives me:

  • hosted Checkout
  • recurring billing
  • invoices and receipts
  • the customer billing portal
  • subscription lifecycle events
  • payment method updates
  • failed payment handling
  • webhook delivery

The official Stripe docs are clear about the important bit: your app should receive Stripe events through webhooks, and subscription flows need webhook handling for payment failures and status changes.

That matches how I want a SaaS app to work. The frontend can start checkout, but the backend should trust Stripe webhooks before granting paid access.

Why not just store "paid" after checkout?

Because checkout is not the whole subscription.

A user can:

  • complete checkout
  • fail a renewal payment later
  • cancel at the end of the billing period
  • update their payment method
  • switch plans
  • get a discount
  • ask for a refund

If the app only knows "this user clicked checkout once", the billing state will rot.

The database needs durable Stripe identifiers and a small amount of local state. In Stacknaut, the user row tracks the Stripe customer ID, active price ID, cancellation state, billing period, credits, and discount metadata.

The app does not try to become Stripe. It keeps the state it needs to make product decisions.

How Stacknaut uses Stripe

Stacknaut includes the billing flow I use in production:

  • frontend starts a checkout session
  • backend creates the Stripe Checkout Session
  • Stripe sends webhook events back to the backend
  • backend verifies webhook signatures
  • database subscription state is updated from Stripe events
  • customers use Stripe's hosted billing portal for invoices, card changes, and cancellation

The key file is:

backend/src/controllers/stripeController.ts

Routes stay thin. The controller owns the billing behavior. That makes it easier for a coding agent to inspect the flow and change it without spreading Stripe logic across the app.

The agent benefit

Billing is exactly the kind of code where I want boring patterns.

When an agent adds annual pricing, a second tier, or a checkout success message, it can follow an existing shape:

  • validate input with Zod
  • call the backend
  • create or reuse a Stripe customer
  • pass metadata into Checkout
  • update access from verified webhook events

No guessing. No half-built payment state in the frontend.

This is why I prefer Stripe in a starter kit. Not because Stripe is the only reasonable payment processor. Paddle and Lemon Squeezy have good reasons to exist, especially if merchant-of-record handling matters to you.

I use Stripe because I want control over the app, the billing model, and the code. Stacknaut packages that setup so the first version already has the hard parts wired in.

See the implementation details in Stacknaut's Stripe billing docs.

Production SaaS starting point

Use the foundation your checklist assumes.

Stacknaut packages the recurring SaaS work into repos your coding agent can inspect, modify, and deploy.

What you get in Stacknaut

  • Auth, billing, shared types, API structure, jobs, logging, and prerendered pages
  • Agent instructions and skills that keep future changes consistent
  • Kamal + Hetzner deployment so launch work has a clear finish line

2e75a5fa