Laravel 4 min read

Building a SaaS MVP With Laravel and FilamentPHP: How I Structured a Real Product

Avatar for Ahmad Tahir By Ahmad Tahir
Architecture diagram of a SaaS built with Laravel and FilamentPHP showing panel, queues, and Stripe billing

If you want to build a SaaS with Laravel FilamentPHP, the architecture decisions you make in the first month decide how painful everything after launch will be. I learned this building one of my recent Laravel SaaS apps as a solo project I did the structural planning and all of the development, and the MVP took about four months. This post walks through the decisions that mattered.

What the product had to do

The product discovers and enriches data from third-party sources, runs long automated email workflows on behalf of its users, and meters every paid action in credits, which users buy through Stripe subscriptions.

That’s five very different kinds of work in one product: user-facing CRUD, third-party API crawling, scheduled background processing, transactional email, and billing. The architecture had to keep those from stepping on each other.

Decision 1: Filament as the entire product UI

There is no separate SPA. The whole authenticated product, user and admin dashboard is one FilamentPHP panel mounted at the root path. Admin isn’t a second panel either; admin-only resources and widgets are gated by an is_admin check, and regular users simply see their own rows.

For a solo MVP this was the highest-leverage decision I made. Filament gave me tables, forms, bulk actions, notifications, and dashboard widgets without building a frontend stack alongside the backend. Livewire handles interactivity; I shipped features instead of API contracts.

The trade-off is honest: you’re accepting Filament’s rendering model for your product UI. For a data-heavy B2B tool it fits. For a consumer product with bespoke UX, it probably wouldn’t.

Decision 2: services do the work, jobs stay thin

The rule I enforced everywhere: service classes do the work; jobs are thin wrappers. A job receives database IDs (never model instances), re-checks the record’s status at the top of handle() so retries are idempotent, calls a service, and handles queue concerns — timeout, retries, locks. Domain logic lives in app/Services/, models stay slim, and every status field is a backed PHP enum that owns its own label and color for Filament rendering.

None of this is exotic. The point is that it was consistent, which is what made a codebase with 65+ migrations and a dozen integrations manageable for one person.

Decision 3: the database queue, split into five workers

I ran the queue on the database driver no Redis required, with five workers, each pinned to a named queue: fast for user-facing work under 60 seconds, slow for external APIs and scraping, a sending queue with a priority replies lane ahead of it, and a fifth queue isolating one heavy subsystem. The default queue stays empty in production, on purpose: every job must declare its queue, timeout, tries, and backoff explicitly.

The split exists so slow external-API work can never starve fast user-facing work. That’s a real failure mode — I’ve written more about it in the queues and background jobs post, including the hardening pass this system needed.

Decision 4: credits as a first-class domain

Because every paid action consumes credits, I made the credit system its own bounded piece: a single CreditService as the only source of truth for rates, and an on-hold reservation pattern so two concurrent jobs can’t overspend a balance. Billing runs through Laravel Cashier and Stripe with idempotent webhook handling. Both deserve their own write-ups, and the credit-based billing post covers the design in detail.

What I’d tell a founder planning the same build

A realistic solo MVP of this scope is measured in months, not weekends — four, in my case, with the caveat that I planned the structure in writing before building. Per-feature documentation felt slow on day one and paid for itself every week after.

Pick boring infrastructure your product can outgrow later. Database queues and a modest VPS were enough to launch; nothing about the architecture blocks moving to Redis or bigger hardware when usage demands it.

And decide your conventions before the code multiplies. The enum rule, the thin-job rule, the one-service-for-credit-rates rule each is small, but together they’re the difference between an MVP and a prototype you’ll rewrite.

If you’re planning a Laravel SaaS and want a senior engineer to build or pressure-test it, that’s exactly the kind of work I take on details on my services page.

Leave a Reply

Your email address will not be published.

Keep reading

How to implement Database Notifications in Laravel FilamentPHP V3?

In this tutorial, I’ll walk you through setting up database notifications using Filament in your Laravel application. We’ll cover how to send notifications and configure real-time updates. Step 1: Install Notifications Require the Notifications package using Composer: Run the following command to install the Notifications package assets: Step 2: Implement Notification Use the Filament\Notifications\Notification class to create […]

2 min read Read Guide