Why multi‑tenant SaaS matters in 2024
Enterprises are shifting from single‑tenant monoliths to shared‑infrastructure models because a single codebase can serve hundreds of customers while cutting operational costs by up to 40%. For developers, a multi‑tenant architecture means faster feature rollout, unified analytics, and the ability to price per seat or usage. The challenge is delivering isolation, secure billing, and low‑latency performance without reinventing the wheel.
Choosing Tenancy for Laravel
The open‑source package Tenancy for Laravel (formerly hyn/multi‑tenant) provides a ready‑made tenant identification middleware, automatic database switching, and a clean artisan command set. Install it via Composer and publish the configuration files in three minutes:
composer require tenancy/tenancy After installation run:
php artisan vendor:publish --provider="Tenancy\Providers\TenancyProvider" The generated config/tenancy.php lets you define the tenant model, domain resolver, and whether you prefer separate databases or shared tables with a tenant_id column. For most SaaS products, separate databases simplify GDPR compliance and make data‑migration scripts safer.
Configuring database separation
Define a tenant model that extends Tenancy\Database\Models\Tenant. The model holds connection details and any subscription metadata.
<?php
namespace App\Models;
use Tenancy\Database\Models\Tenant as BaseTenant;
class Tenant extends BaseTenant
{
protected $fillable = [
'name',
'domain',
'database',
'stripe_customer_id',
];
}
Next, configure the TenantDatabaseResolver to create a new PostgreSQL database per tenant. The resolver runs during each request, reads the tenant’s database attribute, and swaps the default connection:
<?php
use Tenancy\Database\ConnectionResolver;
ConnectionResolver::setResolver(function (Tenant $tenant) {
config(["database.connections.tenant" => [
"driver" => "pgsql",
"host" => env('DB_HOST'),
"port" => env('DB_PORT'),
"database" => $tenant->database,
"username" => env('DB_USERNAME'),
"password" => env('DB_PASSWORD'),
"charset" => "utf8",
"prefix" => "",
]]);
return "tenant";
});
With the resolver in place, any Eloquent query automatically runs against the tenant’s isolated schema, preserving data integrity across the SaaS platform.
Integrating Stripe billing
Stripe remains the de‑facto payment processor for SaaS because of its robust subscription APIs and webhooks. First, add the Stripe PHP SDK:
composer require stripe/stripe-php Store the Stripe customer ID on the tenant record and listen for checkout.session.completed events to activate a plan. The webhook route below validates the signature, locates the tenant, and updates its plan attribute.
<?php
use Illuminate\Http\Request;
use Stripe\Webhook;
use App\Models\Tenant;
Route::post('/stripe/webhook', function (Request $request) {
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
$event = Webhook::constructEvent($payload, $sigHeader, config('services.stripe.webhook_secret'));
if ($event->type === 'checkout.session.completed') {
$session = $event->data->object;
$tenant = Tenant::where('stripe_customer_id', $session->customer)->first();
if ($tenant) {
$tenant->plan = $session->display_items[0]->plan->id;
$tenant->save();
}
}
return response()->json(['status' => 'success']);
});
Remember to register the endpoint in your Stripe dashboard and use a secret that expires after 90 days for maximum security. For usage‑based billing, you can push meter events from any tenant context using the same Stripe client.
Deploying to Fly.io edge network
Fly.io places your containers in 12+ global regions, reducing latency for tenants spread across Europe, North America, and APAC. The platform supports Docker, so a typical Laravel Dockerfile works unchanged. After committing your code, run the following commands on a machine with flyctl installed:
flyctl launch \
--name my-saas \
--region iad \
--dockerfile ./Dockerfile \
--no-deploy
flyctl secrets set APP_KEY=$(php artisan key:generate --show)
flyctl secrets set DB_HOST=... DB_DATABASE=... DB_USERNAME=... DB_PASSWORD=...
flyctl deploy
The --region iad flag selects the Virginia edge, but Fly.io automatically replicates the image to all regions you enable in the fly.toml. Combined with Laravel’s cache driver set to redis on Fly’s managed Redis, response times under 50 ms are achievable for most CRUD endpoints.
Testing and monitoring
Use Laravel’s built‑in php artisan test suite to verify tenant isolation. A simple test that creates two tenants, runs a query, and asserts that records never cross boundaries gives confidence before each deployment:
<?php
test('tenant isolation') {
$tenantA = Tenant::factory()->create(['database' => 'tenant_a']);
$tenantB = Tenant::factory()->create(['database' => 'tenant_b']);
tenancy()->initialize($tenantA);
User::create(['email' => 'a@example.com']);
tenancy()->initialize($tenantB);
expect(User::count())->toBe(0);
}
For production observability, integrate Fly.io metrics with Grafana or use Laravel Telescope behind a secure sub‑domain. Track Stripe webhook success rates, database connection latency, and edge request distribution to spot bottlenecks before customers notice them.
Conclusion
Building a Laravel multi‑tenant SaaS platform becomes a repeatable process when you combine Tenancy for Laravel, Stripe billing integration, and Fly.io edge deployment. The stack delivers data isolation, scalable subscription management, and sub‑second latency across continents—all within a codebase you can maintain with standard Laravel tooling. Start with the Composer packages, configure tenant databases, hook Stripe into your tenant model, and push the Docker image to Fly.io. The result is a production‑ready, future‑proof SaaS architecture ready for growth.
Sources
Official Tenancy for Laravel documentation, Stripe API reference, Fly.io deployment guides.
Author: Mahmut Sarıkaya — sarikayadev.com
.jpeg&w=320&q=50)