Building a SaaS MVP with Laravel: From Zero to Paying Customers
A complete playbook for founders and developers who want to build a production-ready SaaS MVP with Laravel — covering architecture, auth, billing, onboarding, and the critical decisions that let you ship fast without accruing technical debt.
Why Laravel is the Right Foundation for a SaaS MVP
Startups live or die on velocity. The longer it takes to get a working product in front of paying customers, the higher the chance you run out of runway before finding product-market fit. Laravel is one of the fastest frameworks in the world for building production-grade web applications — not because it cuts corners, but because it solves the boring infrastructure problems (authentication, queues, mail, storage, payments) so you can focus on the features that differentiate your product.
I have helped four startups launch their SaaS MVPs on Laravel. The fastest went from zero to first paying customer in 19 days. The pattern that works is always the same: start with a solid, opinionated foundation, defer non-critical decisions, and resist the urge to over-engineer before you have validated demand.
This guide covers the full stack — architecture, authentication, billing, onboarding, and deployment — with the exact decisions I make at each step.
Project Architecture Decisions
Before writing a line of code, make three foundational decisions that are expensive to change later:
Decision 1: Multi-Tenancy Model
Almost every B2B SaaS needs multi-tenancy — the ability to serve multiple organisations from a single application instance. For an MVP, the right choice is almost always single database with a tenant_id column. It is the simplest approach, the cheapest to run, and the easiest to reason about. You can migrate to per-schema or per-database isolation later if compliance requirements demand it — but most early-stage SaaS products never need to.
// The pattern: every table that contains tenant data gets this column
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
// A global scope automatically filters every query
class TeamScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if ($teamId = app('current.team')) {
$builder->where('team_id', $teamId);
}
}
}
Decision 2: Billing Model
Decide before you write a line of code whether you are charging per-seat, per-workspace, usage-based, or a flat monthly fee. This determines your data model. For most B2B MVPs, per-workspace flat billing (one subscription per team, regardless of seat count) is the simplest to implement and the easiest for customers to understand.
Decision 3: Auth Unit
Is the primary entity a User or a Team/Organisation? In B2B SaaS it is almost always a Team — users belong to teams, and the team pays the bill. Start with this model even if you plan to allow solo users initially:
// users: id, name, email, password
// teams: id, name, owner_id, stripe_customer_id, trial_ends_at
// team_user: team_id, user_id, role (owner|admin|member)
Authentication and Onboarding with Laravel Breeze
Install Laravel Breeze for your auth scaffolding. It generates clean, minimal Blade views for login, registration, password reset, and email verification — all things you should not be building from scratch:
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
Extend the default registration to create a Team at the same time a User registers — this is the first critical UX moment in your SaaS:
// In RegisteredUserController::store()
DB::transaction(function () use ($request) {
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$team = Team::create([
'name' => $request->team_name ?? $user->name . "'s Workspace",
'owner_id' => $user->id,
'trial_ends_at' => now()->addDays(14),
]);
$team->users()->attach($user, ['role' => 'owner']);
session(['current_team_id' => $team->id]);
auth()->login($user);
});
Add a 14-day free trial by default. This is the industry standard for B2B SaaS and removes friction at the top of the funnel. Do not ask for a credit card at signup — conversion rates are significantly higher without it for most products.
Stripe Billing with Laravel Cashier
Install Cashier and point it at your Team model (not User — the team pays the bill):
composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
// app/Models/Team.php
use Laravel\Cashier\Billable;
class Team extends Model
{
use Billable;
}
Create your products and prices in the Stripe dashboard, then reference them by price ID in your config. For an MVP, start with two plans: Starter and Pro. You can add more tiers after you understand how customers segment themselves.
// routes/web.php — billing portal
Route::post('/billing/subscribe', [BillingController::class, 'subscribe'])->middleware('auth');
Route::get('/billing/portal', [BillingController::class, 'portal'])->middleware('auth');
// BillingController
public function subscribe(Request $request)
{
$team = $request->user()->currentTeam;
return $team->newSubscription('default', $request->price_id)
->trialDays($team->onTrial() ? 0 : 14)
->checkout([
'success_url' => route('dashboard') . '?subscribed=1',
'cancel_url' => route('billing'),
]);
}
public function portal(Request $request)
{
return $request->user()->currentTeam->redirectToBillingPortal(route('billing'));
}
Gate features behind subscription status with middleware:
// Middleware: EnsureTeamIsSubscribed
public function handle(Request $request, Closure $next)
{
$team = $request->user()->currentTeam;
if (! $team->subscribed() && ! $team->onTrial()) {
return redirect()->route('billing')->with('warning', 'Your trial has ended. Please subscribe to continue.');
}
return $next($request);
}
// In routes
Route::middleware(['auth', 'subscribed'])->group(function () {
// Protected app routes
});
Email with Laravel Mail and Mailgun
Transactional email is non-negotiable for SaaS. You need welcome emails, password resets, trial-ending warnings, invoice receipts, and team invitations. Use Mailgun for delivery — it is cheap, reliable, and has excellent deliverability out of the box:
MAIL_MAILER=mailgun
MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_SECRET=key-...
Send emails via queued Mailable classes so email delivery never blocks an HTTP response:
// Dispatch in RegisteredUserController after signup
WelcomeEmail::dispatch($user)->onQueue('emails');
// Send trial-ending warning via a scheduled command
// app/Console/Kernel.php
$schedule->command('teams:trial-ending-warning')->daily();
The three emails every SaaS must send on day one: welcome (immediate), trial ending at 3 days remaining, and subscription past due (send after Stripe webhook).
Feature Flags for Safe Rollouts
Use feature flags from day one. They let you deploy code to production before it is ready to show to all users, run A/B tests, and give beta access to specific teams. Laravel Pennant (official package) handles this with zero external dependencies:
composer require laravel/pennant
// Define a flag
Feature::define('ai-copilot', fn (User $user) => $user->team->onPlan('pro'));
// Check in code
if (Feature::active('ai-copilot')) {
return view('copilot.index');
}
// Check in Blade
@feature('ai-copilot')
@endfeature
Deployment: Laravel Forge + GitHub Actions
Use Laravel Forge to provision a DigitalOcean or AWS server in minutes. Connect your GitHub repository, add your environment variables, and enable zero-downtime deployments. Your deploy script should run migrations automatically on every push to main:
cd /home/forge/yourdomain.com
git pull origin main
composer install --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
( flock -w 10 9 || exit 1
echo 'Restarting FPM...'; sudo -S service php8.4-fpm reload ) 9>/tmp/fpmlock
For an MVP, a single $24/month DigitalOcean droplet (2 vCPU, 2GB RAM) can handle thousands of concurrent users. Do not over-provision infrastructure at launch — scale when you have the metrics to justify it.
The MVP Launch Checklist
- Registration creates a user + team atomically in a DB transaction
- 14-day trial with no credit card required
- Stripe Checkout for subscriptions with billing portal for self-serve management
- Stripe webhook handler for subscription created/updated/deleted and payment failed events
- Welcome email, trial-ending warning, and payment-failed email
- Feature gates that block access after trial/subscription expires
- Team invitation system (invite by email, accept via signed URL)
- Basic activity log (who did what, when)
- Error tracking (Sentry free tier)
- Uptime monitoring (Better Uptime or UptimeRobot free tier)
- SSL certificate (Forge handles this via Let's Encrypt)
- Backups (Forge automated daily backups to S3)
Frequently Asked Questions
Should I use a SaaS starter kit instead of building from scratch?
For most founders, yes. Packages like Laravel Spark (official, $99 one-time) handle multi-tenancy, Stripe billing, team management, and auth out of the box. If your MVP's core value proposition is the application logic — not the auth and billing plumbing — Spark lets you skip weeks of boilerplate. I recommend building the auth and billing from scratch at least once so you understand what these tools are doing, then use Spark on subsequent projects.
How do I handle data for multiple environments (local, staging, production)?
Never share a database between environments. Use separate Stripe accounts (test mode for local/staging, live mode for production), separate Mailgun domains, and separate environment variable files. Laravel's .env system handles this well — just ensure your staging environment uses Stripe test keys so developers can test billing flows without real charges.
When should I add a proper queue worker?
From day one. Add Laravel Horizon (or a simple Supervisor-managed queue worker) at launch. Every SaaS sends emails, processes webhooks, and eventually runs background jobs. Not having a queue worker means these tasks run synchronously in the HTTP request cycle, making your app feel slow and fragile. Horizon on a $4/month DigitalOcean droplet is all you need to start.
Senior Full Stack Developer — Laravel, Vue.js, Nuxt.js & AI. Available for freelance projects.
Hire Me for Your Project