A simple, drop-in Laravel package for database-per-tenant multitenancy using SQLite. Perfect for SaaS applications where each tenant gets their own isolated SQLite database with automatic URL-based tenant identification and seamless database switching.
You can install the package via composer:
composer require 16bit/easy-multitenancyPublish the config file:
php artisan vendor:publish --tag="easy-multitenancy-config"This is the contents of the published config file:
return [
'database' => [
'path' => env('TENANT_DB_PATH', database_path('tenants')),
'connection' => env('TENANT_DB_CONNECTION', 'tenant'),
'extension' => '.sqlite',
],
'cache' => [
'prefix_enabled' => env('TENANT_CACHE_PREFIX', true),
],
'session' => [
'prefix_enabled' => env('TENANT_SESSION_PREFIX', true),
],
'storage' => [
'prefix_enabled' => env('TENANT_STORAGE_PREFIX', true),
'path' => env('TENANT_STORAGE_PATH', 'tenants'),
],
'queue' => [
'tenant_aware' => env('TENANT_QUEUE_AWARE', true),
],
'seeders' => [
// Seeders to run when creating a new tenant
'on_create' => [
// \Database\Seeders\DatabaseSeeder::class
],
],
'routes' => [
'parameter' => 'tenant',
'middleware' => ['web'],
'auto_prefix' => env('TENANT_AUTO_PREFIX_ROUTES', true),
'excluded_routes' => [
'home',
],
'excluded_patterns' => [
'up',
'horizon*',
'telescope*',
'api/*',
'_debugbar/*',
'*.js',
'*.css',
'*.map',
],
],
];- Database-per-tenant architecture using SQLite
- Automatic route prefixing with tenant identification
- Seamless database switching based on URL
- Tenant-isolated storage, cache, and sessions
- Queue job tenant awareness with the
TenantAwaretrait - Artisan commands for tenant management
- Events for tenant lifecycle hooks
- Custom URL generator for tenant-aware routing
# Interactive creation with prompts
php artisan tenant:create
# Create with specific name
php artisan tenant:create acme
# Create without user
php artisan tenant:create acme --no-userphp artisan tenant:list# Migrate specific tenant
php artisan tenant:migrate acme
# Migrate with fresh (drop all tables)
php artisan tenant:migrate acme --fresh
# Migrate and seed
php artisan tenant:migrate acme --seed
# Migrate all tenants
php artisan tenant:migrate-all# Seed specific tenant
php artisan tenant:seed acme
# Seed with specific seeder class
php artisan tenant:seed acme --class=DatabaseSeeder
# Seed all tenants
php artisan tenant:seed-allThe package automatically identifies tenants from the URL and switches the database context. All routes are automatically prefixed with {tenant} parameter.
use Bit16\EasyMultitenancy\Facades\Tenant;
// Get current tenant
$currentTenant = Tenant::current(); // Returns tenant identifier (e.g., 'acme')
// Get current tenant ID (alias for current())
$tenantId = Tenant::id();
// Get current database path
$database = Tenant::database();
// Check if tenant exists
if (Tenant::exists('acme')) {
// Tenant exists
}
// Get all tenants
$tenants = Tenant::all();
// Manually switch tenant (rarely needed)
Tenant::identify('acme');
// Forget current tenant context
Tenant::forget();Add the TenantAware trait to your jobs to ensure they run in the correct tenant context:
use Bit16\EasyMultitenancy\Traits\TenantAware;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, TenantAware;
public function handle()
{
// Job automatically runs in the correct tenant context
}
}The package dispatches several events you can listen to:
use Bit16\EasyMultitenancy\Events\TenantIdentified;
use Bit16\EasyMultitenancy\Events\TenantNotFound;
use Bit16\EasyMultitenancy\Events\DatabaseSwitched;
// Listen to tenant identified event
Event::listen(TenantIdentified::class, function ($event) {
// $event->tenant
// $event->database
});
// Listen to database switched event
Event::listen(DatabaseSwitched::class, function ($event) {
// $event->tenant
// $event->database
// $event->connection
});
// Listen to tenant not found event
Event::listen(TenantNotFound::class, function ($event) {
// $event->tenant
});By default, all routes are automatically prefixed with the tenant parameter. You can exclude specific routes:
// In config/easy-multitenancy.php
'routes' => [
'parameter' => 'tenant',
'middleware' => ['web'],
'auto_prefix' => env('TENANT_AUTO_PREFIX_ROUTES', true),
'excluded_routes' => [
'home',
],
'excluded_patterns' => [
'up',
'horizon*',
'telescope*',
'api/*',
'_debugbar/*',
'*.js',
'*.css',
'*.map',
],
],The package includes a custom URL generator that automatically includes the tenant parameter:
// Generate URL to a route
url('/dashboard'); // Automatically becomes /{tenant}/dashboard
// Named routes
route('dashboard'); // Automatically includes tenant parameter
// Generate URL for a specific tenant
route('dashboard', ['tenant' => 'acme']);composer testPlease see CHANGELOG for more information on what has changed recently.
If you discover a security vulnerability, please email Mattia Trapani at mt@16bit.it.
The MIT License (MIT). Please see License File for more information.