Building a Multi-Tenant SaaS Application with Laravel 13
What You'll Build
In this tutorial, you'll learn how to create a multi-tenant SaaS application using Laravel 13. The end product will be a web application that supports multiple tenants, each with their own isolated data. Users will be able to sign up, log in, and manage their own space independently. Here's a quick look at what you'll achieve:
- Tenant Isolation: Each tenant will have its own database.
- User Management: Users can register and log in under their respective tenants.
- Admin Panel: A basic admin panel to manage tenants and users.
Why This Matters
Multi-tenancy is a crucial design pattern for SaaS applications. It allows you to serve multiple customers (tenants) with a single instance of your application, reducing operational costs and simplifying updates. This approach is particularly beneficial for:
- Startups: Quickly scale your application to serve multiple clients without duplicating codebases.
- Enterprises: Manage different departments or subsidiaries with isolated data but shared infrastructure.
- Developers: Learn a valuable architectural pattern that enhances your ability to build scalable applications.
Architecture Overview
The architecture of a multi-tenant SaaS application involves components that ensure data isolation and efficient resource usage. Here’s a simple diagram to illustrate:
+-----------------+ +-----------------+
| Tenant A DB | | Tenant B DB |
+-----------------+ +-----------------+
| |
+-----------+------------+
|
+-----------------+
| Laravel App |
+-----------------+
|
+---------------+
| User Access |
+---------------+
- Database Per Tenant: Each tenant has its own database for data isolation.
- Single Laravel Application: A single codebase handles requests for all tenants.
- User Access Layer: Manages authentication and authorization across tenants.
Step-by-Step Implementation
Let's dive into building our multi-tenant SaaS application with Laravel 13.
Step 1: Set Up Laravel 13 Project
First, we need to set up a new Laravel project. Run the following command to create a new Laravel application:
composer create-project --prefer-dist laravel/laravel multi-tenant-saas
Navigate into your project directory:
cd multi-tenant-saas
Ensure everything is working by starting the local development server:
php artisan serve
Visit http://localhost:8000 in your browser to see the default Laravel welcome page.
Step 2: Configure Environment Variables
Next, we need to set up our environment variables for database connections. Open the .env file and configure the database settings. Since we are building a multi-tenant application, we'll use a default database for the initial setup:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=default_db
DB_USERNAME=root
DB_PASSWORD=your_password
This configuration connects Laravel to a MySQL database named default_db. Ensure that this database exists on your MySQL server.
Step 3: Create Authentication Scaffolding
Laravel provides a convenient way to generate authentication scaffolding. We'll use Laravel Breeze for a simple and clean setup.
First, install Laravel Breeze:
composer require laravel/breeze --dev
Then, run the Breeze installation command:
php artisan breeze:install
This command sets up authentication routes, controllers, and views. Next, run the migrations to create the necessary tables:
php artisan migrate
Finally, compile your assets:
npm install && npm run dev
At this point, you should be able to register and log in to the application. Visit http://localhost:8000/register to test the authentication functionality.
With these steps, we've laid the foundation for our multi-tenant application. In the next part of this tutorial, we'll focus on setting up tenant-specific databases and implementing tenant isolation.
Step 4: Implement Tenant Management
Now that we have the basic authentication setup, let's move on to tenant management. We'll create a simple system where each tenant has its own database. This involves setting up a model and migration for tenants.
First, create a Tenant model and migration:
php artisan make:model Tenant -m
Open the migration file created in database/migrations and define the schema for the tenants table:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTenantsTable extends Migration
{
public function up()
{
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('domain')->unique();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('tenants');
}
}
Run the migration to create the tenants table:
php artisan migrate
Step 5: Set Up Tenant Database Connections
For each tenant, we need to create a separate database. We will achieve this by dynamically switching the database connection based on the tenant's domain.
First, create a middleware to determine the current tenant:
php artisan make:middleware TenantMiddleware
In app/Http/Middleware/TenantMiddleware.php, implement the logic to resolve the tenant by its domain:
namespace App\Http\Middleware;
use Closure;
use App\Models\Tenant;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
class TenantMiddleware
{
public function handle($request, Closure $next)
{
$tenant = Tenant::where('domain', $request->getHost())->first();
if (!$tenant) {
abort(404, 'Tenant not found');
}
// Set the database connection for the tenant
Config::set('database.connections.tenant.database', $tenant->database);
DB::purge('tenant');
return $next($request);
}
}
In config/database.php, add a new connection for tenants:
'connections' => [
// Other connections...
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => '',
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
],
Register the middleware in app/Http/Kernel.php:
protected $middlewareGroups = [
'web' => [
// Other middleware...
\App\Http\Middleware\TenantMiddleware::class,
],
];
Step 6: Create Tenant-Specific Migrations
Finally, create tenant-specific migrations that will be run for each tenant database. You can use a command to automate this process. First, create a command:
php artisan make:command MigrateTenants
In app/Console/Commands/MigrateTenants.php, implement the command:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Tenant;
use Illuminate\Support\Facades\Artisan;
class MigrateTenants extends Command
{
protected $signature = 'tenants:migrate';
protected $description = 'Run migrations for all tenants';
public function handle()
{
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
$this->info("Migrating tenant: {$tenant->name}");
config(['database.connections.tenant.database' => $tenant->database]);
Artisan::call('migrate', [
'--database' => 'tenant',
'--path' => '/database/migrations/tenant',
'--force' => true,
]);
}
return 0;
}
}
Run this command whenever you add new migrations specific to tenant databases:
php artisan tenants:migrate
Common Mistakes
- Database Connection Issues: Forgetting to set the database name dynamically can lead to queries being run on the wrong database.
- Middleware Order: Ensure that the tenant middleware is correctly ordered in the middleware stack to avoid routing issues.
- Migration Path: Always specify the correct migration path for tenant databases to avoid running unwanted migrations.
How I Would Use This
This setup is ideal for SaaS products where data isolation is crucial. It's suitable for applications that need to scale across multiple clients with different data requirements. However, it can be overkill for small projects or those with minimal data isolation needs due to the complexity of managing multiple databases.
In production, consider the cost implications of managing multiple databases and ensure that your hosting solution supports this architecture.
Lessons Learned
- Complexity: Multi-tenancy introduces complexity in database management and deployment processes.
- Scalability: While this setup scales well for multiple tenants, it requires careful planning around database resources and query optimization.
- Security: Ensuring data isolation is critical; improper configuration can lead to data leaks across tenants.
Next Steps
- Implement advanced features like tenant-specific configurations or subdomains.
- Explore Laravel packages like
tenancy/tenancyfor more robust multi-tenancy solutions. - Dive into performance optimization for multi-tenant applications.