Case Study

Invoice Tracker SaaS — Billing System Built with Laravel and Stripe

Invoice Tracker is a lightweight billing SaaS built with Laravel 12, Livewire 3 and Stripe Checkout. This case study focuses on the architectural decisions behind the payment workflow: how invoices, totals and payment states were modeled, and how Stripe webhooks were used to keep the system reliable and synchronized in real time.

Portfolio SaaS demo
Role: Fullstack Laravel Engineer Laravel 12 · Livewire 3 · TailwindCSS · Stripe · MySQL

Overview

Invoice Tracker was designed as a compact SaaS demo focused on a complete billing workflow. The system allows authenticated users to manage clients, create invoices with itemized lines, calculate totals and export invoices as PDF documents. The project was intentionally kept small in scope, but structured around patterns commonly found in real SaaS products.

The main objective was to build a portfolio-ready project that could demonstrate more than a CRUD: it needed to include a real payment flow. For that reason, Stripe Checkout and webhook-based confirmation were integrated so that invoice state could be updated reliably after a successful payment.

System Architecture

Domain Modeling

The billing domain is centered around a simple but clear relationship structure: clients, invoices and invoice items. Each invoice belongs to a client and contains one or more item lines used to calculate subtotal, tax and total. This provided a clean domain model for representing a typical invoicing workflow without unnecessary complexity.

Livewire-Based Interface

Livewire was used to build the main interface for clients and invoices, including index, create, edit and show screens. The goal was to keep the application server-driven while still providing a dynamic user experience. This approach kept the project simple to deploy and SEO-friendly, while avoiding the overhead of a separate SPA frontend.

Payment Integration Layer

Stripe Checkout was integrated as the hosted payment layer for one-time invoice payments. Instead of handling card data directly, the application creates a Checkout Session and redirects the user to Stripe. Metadata is attached to the session so the related invoice can be identified later when Stripe sends the confirmation event.

Webhook Confirmation Flow

Payment confirmation is handled through a dedicated webhook endpoint. When Stripe emits the checkout.session.completed event, the application verifies the request signature and updates the corresponding invoice. This ensures the system does not rely on browser redirects as the source of truth for payment status.

Key principle: keep the billing flow simple for the user, but reliable on the backend by separating payment initiation from payment confirmation.

Technical Decisions

  • Stripe Checkout instead of custom payment forms: using Stripe-hosted Checkout reduced complexity and avoided handling sensitive card data directly, while still providing a real billing flow suitable for SaaS products.
  • Webhook-driven payment confirmation: invoice status is not updated from the success page redirect. Instead, the system waits for Stripe’s signed webhook event, which provides a more reliable and production-oriented pattern.
  • Dedicated Stripe controllers: payment initiation and webhook handling were moved out of the routes file into dedicated controllers, keeping routing cleaner and preparing the codebase for future growth.
  • Clear invoice lifecycle states: the project uses explicit statuses such as draft, sent, paid and overdue, allowing the UI and business logic to stay synchronized and easier to reason about.

Payment Workflow

The payment workflow was designed to simulate a realistic SaaS billing experience: the user opens an invoice, reviews its details and initiates payment through a “Pay with Stripe” action. The application creates a Checkout Session on the server and redirects the user to Stripe’s hosted checkout page.

Once payment is completed, Stripe redirects the user back to the application, but the invoice is not considered paid yet. The final update only happens when the webhook endpoint receives and validates the Stripe event. At that point, the invoice status changes to paid, the payment timestamp is stored and the UI updates automatically by hiding the payment button.

This separation between user-facing success screens and backend confirmation mirrors how payment systems are typically implemented in production, where redirects are treated as UX signals, not as the source of truth.

Lessons Learned

Building this project reinforced the importance of reliability in payment workflows. The most relevant lesson was that success pages are not enough: the backend must always rely on signed webhook events to confirm financial actions. It also highlighted how even a small SaaS demo benefits from clear separation between UI, controllers and domain logic.

The project was also useful for practicing a more product-oriented mindset: not just making something work, but making it behave like a real billing system with consistent statuses, clear user feedback and predictable flows.

Future Improvements

Future iterations could include storing Stripe session identifiers and payment intent references, adding a dedicated payment history section and introducing more robust reporting inside the dashboard. It would also make sense to harden the billing layer further for production by refining logging, retry handling and deployment configuration for live Stripe environments.