This post contains affiliate links.
We earn commissions when you shop through the links below.
If you’ve been wondering how to build a SaaS with Laravel and Stripe, you’re in the right place. This guide walks you through everything from scaffolding the project to wiring up subscription plans, handling webhooks, and protecting routes based on billing status. I’ve built several SaaS products using this exact stack, and it remains my go-to combination for shipping fast without sacrificing reliability.
Why Laravel and Stripe?
Laravel’s ecosystem is mature enough to handle billing out of the box via Laravel Cashier, which wraps the Stripe API in a clean, expressive interface. You don’t need to hand-roll API calls or manage subscription state manually — Cashier does the heavy lifting. Stripe, on the other hand, is simply the best payment processor available for SaaS products: excellent documentation, robust webhook infrastructure, and a dashboard that doesn’t make you want to throw your laptop.
Project Setup
Start with a fresh Laravel installation and pull in the required packages:
composer create-project laravel/laravel my-saas
cd my-saas
composer require laravel/cashierNext, publish the Cashier migrations and run them:
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrateAdd the Billable trait to your User model:
<?php
namespace App\Models;
use Laravel\Cashier\Billable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Billable;
}Then configure your Stripe keys in .env:
STRIPE_KEY=pk_live_xxxxxxxxxxxx
STRIPE_SECRET=sk_live_xxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxCreating Stripe Products and Prices
Before writing any more code, head into your Stripe dashboard and create your products and pricing plans. I usually create at least two tiers — a Starter and a Pro plan — with monthly and annual billing options. Copy the price IDs (they look like price_1Oxxxxxxxx) because you’ll reference them in your code.
Store these in your config or as environment variables:
STRIPE_PRICE_STARTER_MONTHLY=price_1OstartMonthly
STRIPE_PRICE_PRO_MONTHLY=price_1OproMonthlyBuilding the Checkout Flow
One of the best parts of knowing how to build a SaaS with Laravel and Stripe is that Cashier handles the entire hosted checkout session. Here’s a simple controller action that redirects a user to Stripe Checkout:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class BillingController extends Controller
{
public function checkout(Request $request)
{
$priceId = $request->input('price_id');
return $request->user()
->newSubscription('default', $priceId)
->checkout([
'success_url' => route('dashboard') . '?subscribed=1',
'cancel_url' => route('pricing'),
]);
}
public function portal(Request $request)
{
return $request->user()->redirectToBillingPortal(
route('dashboard')
);
}
}The portal method gives users a Stripe-hosted page where they can upgrade, downgrade, or cancel their subscription without you building any of that UI yourself. This alone saves days of work.
Protecting Routes with Middleware
You need to gate features behind an active subscription. Cashier ships with a subscribed middleware you can register and use on route groups:
// In bootstrap/app.php (Laravel 11+)
$middleware->alias([
'subscribed' => \Laravel\Cashier\Http\Middleware\CheckSubscription::class,
]);Then apply it to your protected routes:
Route::middleware(['auth', 'subscribed'])->group(function () {
Route::get('/app/dashboard', [AppController::class, 'index'])->name('dashboard');
Route::get('/app/reports', [ReportController::class, 'index']);
});For more granular control — like limiting features by plan — I recommend creating a PlanFeature service class that checks the user’s current subscription plan against a feature map:
<?php
namespace App\Services;
class PlanFeature
{
protected array $features = [
'starter' => [
'max_projects' => 3,
'api_access' => false,
],
'pro' => [
'max_projects' => PHP_INT_MAX,
'api_access' => true,
],
];
public function can(string $feature): bool
{
$plan = auth()->user()->subscription('default')?->stripe_price;
$tier = str_contains($plan ?? '', 'pro') ? 'pro' : 'starter';
return (bool) ($this->features[$tier][$feature] ?? false);
}
}Handling Stripe Webhooks
Webhooks are non-negotiable when you want to build a SaaS with Laravel and Stripe that behaves correctly in production. Stripe fires events for subscription renewals, failed payments, cancellations, and more. Cashier registers a webhook route automatically — you just need to configure your Stripe dashboard to point to it.
Register the webhook endpoint in Stripe pointing to:
https://your-app.com/stripe/webhookMake sure this route is excluded from CSRF protection in your middleware config. Cashier handles the signature verification using your STRIPE_WEBHOOK_SECRET.
For custom logic on webhook events, extend Cashier’s webhook controller:
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierWebhookController;
class WebhookController extends CashierWebhookController
{
public function handleInvoicePaymentFailed(array $payload): void
{
$userId = $payload['data']['object']['metadata']['user_id'] ?? null;
if ($userId) {
// Send a dunning email, flag the account, etc.
\App\Jobs\SendPaymentFailedNotification::dispatch($userId);
}
}
}Then point the webhook route to your custom controller in routes/web.php:
Route::post('/stripe/webhook', App\Http\Controllers\WebhookController::class);Deploying Your SaaS
Once you’re ready to ship, you need reliable infrastructure. I deploy most of my Laravel SaaS projects on DigitalOcean using their managed databases and App Platform or a plain Droplet with Laravel Forge. The combination gives you predictable costs and enough control to tune performance as you scale.
A few deployment checklist items specific to Stripe integration:
- Switch to live Stripe keys and create a live webhook endpoint in the Stripe dashboard.
- Run
php artisan cashier:webhookto automatically register your webhook with Stripe via CLI. - Set up queue workers for processing webhook jobs asynchronously.
- Enable Stripe Radar for fraud protection on your checkout sessions.
Common Pitfalls
After shipping multiple products using this approach, here are the mistakes I see most often:
- Not testing webhooks locally. Use the Stripe CLI (
stripe listen --forward-to localhost/stripe/webhook) to simulate events during development. - Assuming subscription status from the database alone. Always let Cashier sync state from Stripe via webhooks rather than relying solely on a local boolean.
- Skipping the billing portal. Building custom upgrade/downgrade flows is a trap. Stripe’s hosted portal is polished and handles proration automatically.
- Missing trial period setup. If you want trials, add
->trialDays(14)to your subscription builder — don’t bolt it on later.
Wrapping Up
Knowing how to build a SaaS with Laravel and Stripe gives you a legitimate shortcut to a production-ready billing system. Cashier abstracts away the Stripe API complexity, and the hosted checkout and billing portal mean you can focus on building your core product instead of reinventing payment UIs. Start with the basics covered here, ship early, and layer in more advanced features like usage-based billing or metered subscriptions as your user base grows.
The stack is proven, the tooling is excellent, and there’s no shortage of resources when you get stuck. Now go build something.