When I started building my HR & attendance management SaaS, I thought the hardest part would be the business logic. I was wrong. The hardest part was making the right architectural decisions early — the ones that save you from rewriting everything two months later.
Here’s what I actually learned building a real SaaS product with Laravel 12, Livewire 4 (Volt), and Lemon Squeezy as a payment processor.
Clean architecture pays off when your app starts growing
1. Start With Your Subscription Model, Not Your Features
Most developers (myself included) start building features and bolt on payments later. Big mistake.
Your subscription model shapes everything: what users can access, how you handle plan limits, how you structure your database. If you design features first, you’ll spend weeks refactoring when billing comes in.
What I did instead:
I created a PlanVariant model early on that bridges my internal plan IDs to Lemon Squeezy variant IDs, and a UserSubscription model that tracks each user’s current plan state independently.
// PlanVariant — bridges internal plans to payment processor
Schema::create('plan_variants', function (Blueprint $table) {
$table->id();
$table->foreignId('plan_id')->constrained()->cascadeOnDelete();
$table->string('variant_id'); // Lemon Squeezy variant ID
$table->enum('billing_cycle', ['monthly', 'yearly']);
$table->timestamps();
});
This decoupling means if you ever switch payment processors, your core app logic doesn’t break.
2. Livewire Volt Is a Game Changer for Admin Panels
If you’re building a back-office or admin panel, Livewire 4 Volt with single-file components is incredibly productive. You write your PHP logic and your Blade template in one file — no more jumping between controllers, views, and form requests for simple CRUD.
One file, full reactivity — that’s the Volt promise
The pattern I settled on for create/update forms:
<?php
// ⚡plans/edit.blade.php
use function Livewire\Volt\{state, mount};
state(['plan' => null, 'name' => '', 'price' => 0]);
mount(function (Plan $plan) {
$this->plan = $plan->firstOrNew(['id' => $plan->id]);
$this->name = $this->plan->name ?? '';
$this->price = $this->plan->price ?? 0;
});
$save = function () {
$this->validate(['name' => 'required', 'price' => 'required|numeric']);
if ($this->plan->isDirty()) {
$this->plan->save();
$this->dispatch('alert', type: 'success', message: __('app.saved'));
}
};
?>
The firstOrNew + isDirty() combo is clean and avoids unnecessary database writes.
3. Choose Your Payment Processor Based on Your Situation, Not Just Features
I went with Lemon Squeezy for one specific reason: it acts as a Merchant of Record. This means Lemon Squeezy handles taxes, VAT, and compliance globally — not me. As an independent developer without a registered business entity, this was non-negotiable.
Stripe is more powerful, but it puts the tax compliance burden on you. For a solo developer shipping a product, that overhead is real.
The tradeoff: Lemon Squeezy takes a higher cut (5% + $0.50 per transaction). Worth it for the peace of mind.
Webhook setup in Laravel:
// routes/web.php
Route::post('/webhooks/lemon-squeezy', [LemonSqueezyWebhookController::class, 'handle'])
->name('webhooks.lemon-squeezy');
// In your webhook controller
public function handle(Request $request): Response
{
$payload = $request->all();
$eventName = $payload['meta']['event_name'];
match ($eventName) {
'subscription_created' => $this->handleSubscriptionCreated($payload),
'subscription_updated' => $this->handleSubscriptionUpdated($payload),
'subscription_cancelled' => $this->handleSubscriptionCancelled($payload),
default => null,
};
return response()->noContent();
}
Always verify the webhook signature. Always.
4. The Landing Page Is Not Optional
If you use a payment processor like Lemon Squeezy, they will ask for a live, public landing page before approving your store. Not a “coming soon” page — an actual page describing what your product does, who it’s for, and what it costs.
Your landing page is your first impression AND a business requirement
Build it early. It also forces you to clarify your own value proposition, which always improves the product.
5. Don’t Skip the Permission Layer
Spatie’s laravel-permission package is the standard for a reason. Set it up from day one — defining roles and permissions retroactively is painful.
My setup: each subscription plan maps to a set of permissions. When a user upgrades or downgrades, permissions are synced automatically via a job triggered by the Lemon Squeezy webhook.
// Sync permissions on subscription change
$user->syncRoles([$newPlan->role]);
Simple, auditable, and easy to extend.
Key Takeaways
| Decision | What I’d do again |
|---|---|
| Subscription model first | ✅ Absolutely |
| Livewire Volt for admin | ✅ Saves so much time |
| Lemon Squeezy as MoR | ✅ If you’re a solo dev |
| Spatie permissions from day 1 | ✅ Non-negotiable |
| Landing page early | ✅ Required anyway |
Building a SaaS is mostly about making decisions you won’t regret in 6 months. The tech stack matters less than the architecture decisions you make in week one.
If you’re building something similar or have questions about any of these choices, drop a comment — happy to go deeper on any of these points.