Laravel Sep 05, 2026 · 12 min read

Building InvoiceHub: a Laravel project walkthrough

The story of shipping a real invoicing app — from a three-table schema to the dashboard, the REST API, and the background jobs that keep it fast. Screenshots included.

Building InvoiceHub: a Laravel project walkthrough cover

InvoiceHub started as a "quick weekend project" and turned into the most instructive app I've built so far. It's a Laravel app that lets freelancers create invoices, track payments, and email reminders — the kind of CRUD product that looks simple until you actually ship it. Here's how it went together, with the real screens along the way.

The schema at a glance

Everything hangs off three tables. A client has many invoices, and an invoice has many payments — which is how you know a "paid" invoice is really a set of payment records that happen to sum to the total. Three migrations, three relationships, and the whole data model fits on one screen (see the screenshots below):

2026_01_04_000000_create_invoices_table.php
Schema::create('invoices', function (Blueprint $table) {
    $table->id();
    $table->foreignId('client_id')->constrained()->cascadeOnDelete();
    $table->string('number')->unique();
    $table->enum('status', ['draft', 'sent', 'paid', 'overdue']);
    $table->decimal('total', 10, 2);
    $table->timestamp('due_at');
    $table->timestamps();
});

The dashboard

The landing page after login is the dashboard from the first screenshot — three stat cards fed by withCount() and a simple chart of money collected per month. No charting library; just an SVG and a handful of grouped queries. It loads fast because the heavy lifting stays in SQL, not PHP.

The REST API

The mobile companion app talks to a slim set of endpoints — all route-model-bound, all returning consistent JSON shapes so the frontend never has to guess. Form requests keep controllers thin, and APIResource keeps responses shaped — one source of truth for what a client sees over the wire.

Emails without blocking the request

Sending an invoice means rendering a PDF, attaching it, and firing an email — the kind of work that should never sit inside a controller. That's what queues are for:

InvoiceController.php
$invoice->status = 'sent';
$invoice->save();

Mail::to($invoice->client->email)
    ->queue(new InvoiceMail($invoice));

What I'd do differently next time

  • Add tests earlier. The schema and status transitions would have been easier to change with feature tests from day one.
  • Version the API from the start. A v1 prefix is cheap to add and painful to retrofit.
  • Use a real mail queue worker in dev. I lost a morning to queued emails that never arrived because no worker was running.

TL;DR

A small schema, a handful of withCount() queries, a slim REST API, and a queue for anything slow. The boring parts — consistent JSON and tested migrations — were exactly the parts that saved the project.

InvoiceHub taught me that a "simple" app is only simple because someone made deliberate choices about schema, endpoints, and background work. That's the whole walkthrough — four screens and the decisions behind each one.

Laravel Project MySQL Queues
Share:
FR

Fazale Rabbi

Laravel Full-Stack Developer writing about what I build and debug.

Let's work together →

Keep reading