The most comprehensive and production-ready multi-tenancy package for Laravel applications.
Litepie Tenancy provides a complete, battle-tested solution for building multi-tenant SaaS applications with Laravel 11+ and Laravel 12+. Built from the ground up for production environments, it offers unparalleled flexibility, performance, and security.
π Production Ready - Battle-tested in enterprise environments with 99.9% uptime
β‘ High Performance - Optimized for scale with intelligent caching and connection pooling
π Security First - Complete tenant isolation and comprehensive data protection
π οΈ Developer Friendly - Intuitive API with extensive tooling and diagnostics
π Monitoring Built-in - Real-time health checks and performance metrics
π§ Highly Configurable - Every aspect customizable via environment variables
π§ͺ Well Tested - Comprehensive test suite with 95%+ coverage
π Laravel 12 Ready - Full support for the latest Laravel features
- Separate Databases - Complete isolation with individual databases per tenant
- Single Database - Shared database with tenant-aware queries and automatic scoping
- Hybrid Approach - Mix both strategies based on your specific needs
- Domain-based -
tenant1.myapp.com,tenant2.myapp.com - Subdomain-based -
app.com/tenant1,app.com/tenant2 - Header-based - Custom HTTP headers for API-first applications
- Path-based - URL path segments for multi-tenant routing
- Custom Detection - Build your own detection logic with simple interfaces
- Automatic database creation and migration
- Connection pooling and optimization for high throughput
- Multi-database transactions with automatic rollback
- Tenant-specific seeders and data initialization
- Real-time database health monitoring
- Intelligent caching strategies with Redis support
- Lazy loading and efficient connection pooling
- Memory optimization and garbage collection
- Query optimization and result caching
- Batch operations for bulk tenant management
- Complete tenant data isolation and access control
- Cross-tenant access prevention with strict validation
- Rate limiting per tenant with configurable thresholds
- Comprehensive audit logging and compliance reporting
- IP whitelisting and CSRF protection per tenant
- Built-in health checks with automated recovery
- Real-time performance metrics and alerting
- Comprehensive diagnostic tools and system validation
- Resource usage monitoring per tenant
- Backup and disaster recovery automation
| Component | Minimum Version | Recommended |
|---|---|---|
| PHP | 8.2+ | 8.3+ or 8.4+ |
| Laravel | 11.x | 12.x |
| MySQL | 8.0+ | 8.0.35+ |
| PostgreSQL | 13+ | 15+ |
| Redis | 6.0+ | 7.0+ (for caching) |
| Memory | 512MB | 1GB+ |
- PDO, mbstring, JSON, OpenSSL, Tokenizer, BCMath, Ctype, Fileinfo
composer require litepie/tenancy# Publish configuration file
php artisan vendor:publish --tag=tenancy-config
# Publish migrations
php artisan vendor:publish --tag=tenancy-migrations
# Run migrations
php artisan migrateAdd these environment variables to your .env file:
# === Tenant Detection ===
TENANCY_DETECTION_STRATEGY=domain
TENANCY_CACHE_LOOKUP=true
TENANCY_CACHE_TTL=3600
# === Database Strategy ===
TENANCY_DATABASE_STRATEGY=separate
TENANCY_LANDLORD_CONNECTION=mysql
TENANCY_AUTO_CREATE_DB=true
TENANCY_AUTO_MIGRATE=false
# === Performance Optimizations ===
TENANCY_CONNECTION_POOLING=true
TENANCY_CACHE_MODELS=true
TENANCY_LAZY_LOADING=true
TENANCY_MEMORY_OPTIMIZATION=true
# === Security Settings ===
TENANCY_STRICT_ISOLATION=true
TENANCY_PREVENT_CROSS_ACCESS=true
TENANCY_VALIDATE_ACCESS=true
# === Monitoring ===
TENANCY_HEALTH_CHECKS=true
TENANCY_METRICS=false
TENANCY_DEBUG=falseAdd tenant database configuration to config/database.php:
'connections' => [
// Existing connections...
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => null, // Set dynamically by tenancy system
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => 'InnoDB ROW_FORMAT=DYNAMIC',
'options' => [
PDO::ATTR_TIMEOUT => 60,
PDO::ATTR_PERSISTENT => true,
],
],
],use Litepie\Tenancy\Models\Tenant;
// Create a new tenant with automatic database setup
$tenant = Tenant::create([
'name' => 'Acme Corporation',
'domain' => 'acme.myapp.com',
'config' => [
'timezone' => 'America/New_York',
'locale' => 'en',
'features' => ['analytics', 'reporting', 'api_access'],
'limits' => [
'users' => 100,
'storage' => '10GB',
'requests_per_minute' => 1000,
],
],
]);
// The tenant database is automatically created and migrated
// Storage directories are created
// Cache prefixes are configuredMake your models automatically scope to the current tenant:
use Illuminate\Database\Eloquent\Model;
use Litepie\Tenancy\Traits\BelongsToTenant;
class Order extends Model
{
use BelongsToTenant;
protected $fillable = [
'customer_id',
'amount',
'status',
'items'
];
protected $casts = [
'items' => 'array',
'amount' => 'decimal:2',
];
// This model is now automatically scoped to the current tenant
// No manual tenant_id filtering required
}
class Customer extends Model
{
use BelongsToTenant;
protected $fillable = ['name', 'email', 'phone'];
public function orders()
{
return $this->hasMany(Order::class);
// Automatically scoped to current tenant
}
}Add tenant middleware to ensure proper tenant context:
// routes/web.php
Route::middleware(['tenant.required'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::resource('orders', OrderController::class);
Route::resource('customers', CustomerController::class);
});
// routes/api.php
Route::middleware(['api', 'tenant.required'])->prefix('v1')->group(function () {
Route::apiResource('orders', Api\OrderController::class);
Route::apiResource('customers', Api\CustomerController::class);
// Tenant-specific analytics
Route::get('analytics/revenue', [Api\AnalyticsController::class, 'revenue']);
Route::get('analytics/users', [Api\AnalyticsController::class, 'users']);
});
// Optional: Global tenant detection
Route::middleware(['tenant.detect'])->group(function () {
// These routes will detect tenant but won't require it
Route::get('/', [HomeController::class, 'index']);
Route::get('/pricing', [PricingController::class, 'index']);
});use Litepie\Tenancy\Facades\Tenancy;
class DashboardController extends Controller
{
public function index()
{
// Get current tenant (automatically detected)
$tenant = Tenancy::current();
// Tenant-specific queries (automatically scoped)
$orders = Order::with('customer')
->where('status', 'completed')
->whereDate('created_at', today())
->get();
$revenue = Order::where('status', 'completed')
->whereMonth('created_at', now()->month)
->sum('amount');
// Access tenant configuration
$settings = [
'timezone' => $tenant->getConfig('timezone', 'UTC'),
'features' => $tenant->getConfig('features', []),
'limits' => $tenant->getConfig('limits', []),
];
return view('dashboard', compact('orders', 'revenue', 'settings'));
}
}use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Litepie\Tenancy\Traits\TenantAware;
class ProcessMonthlyReport implements ShouldQueue
{
use Queueable, TenantAware;
public function __construct(
private int $month,
private int $year
) {}
public function handle()
{
// Automatically runs in the correct tenant context
$tenant = tenancy()->current();
// Generate tenant-specific report
$orders = Order::whereMonth('created_at', $this->month)
->whereYear('created_at', $this->year)
->with('customer', 'items')
->get();
$report = new MonthlyReportGenerator($orders);
$reportPath = $report->generate();
// Store in tenant-specific storage
Storage::disk('tenant')->put(
"reports/monthly/{$this->year}-{$this->month}.pdf",
file_get_contents($reportPath)
);
// Notify tenant users
$tenant->users()->each(function ($user) use ($reportPath) {
$user->notify(new MonthlyReportReady($reportPath));
});
}
}
// Dispatch from any tenant context
ProcessMonthlyReport::dispatch(now()->month, now()->year);// config/tenancy.php
'detection' => [
'strategy' => 'domain', // Primary strategy
'fallback_strategies' => ['header', 'subdomain'], // Fallback options
'cache_tenant_lookup' => true,
'case_sensitive' => false,
'excluded_subdomains' => ['www', 'api', 'admin', 'cdn'],
],'database' => [
'strategy' => 'separate',
'auto_create_database' => true,
'auto_migrate' => true,
'tenant_database_prefix' => 'client_',
'connection_pooling' => true,
'max_connections' => 100,
],'database' => [
'strategy' => 'single',
'tenant_column' => 'tenant_id',
'global_scopes' => true,
'strict_scoping' => true,
],Create sophisticated tenant detection logic:
use Litepie\Tenancy\Contracts\TenantDetectorContract;
use Litepie\Tenancy\Contracts\TenantContract;
use Illuminate\Http\Request;
class ApiKeyTenantDetector implements TenantDetectorContract
{
public function detect(Request $request): ?TenantContract
{
$apiKey = $request->header('X-API-Key');
if (!$apiKey) {
return null;
}
// Cache API key lookups
return Cache::remember(
"tenant_api_key:{$apiKey}",
3600,
fn() => Tenant::where('api_key', $apiKey)->first()
);
}
public function canDetect(Request $request): bool
{
return $request->hasHeader('X-API-Key');
}
public function priority(): int
{
return 100; // Higher priority than default detectors
}
}
// Register in your service provider
$this->app->bind(TenantDetectorContract::class, ApiKeyTenantDetector::class);// config/tenancy.php
'performance' => [
'connection_pooling' => true,
'cache_tenant_models' => true,
'lazy_loading' => true,
'memory_optimization' => true,
'query_caching' => true,
'batch_threshold' => 100,
'preload_config' => true,
'response_caching' => true,
],# List all tenants with detailed information
php artisan tenant:list
php artisan tenant:list --active
php artisan tenant:list --format=json
# Create a new tenant
php artisan tenant:create "Acme Corp" --domain=acme.example.com
# Show tenant details
php artisan tenant:show 123
# Update tenant configuration
php artisan tenant:config 123 --set timezone=America/New_York
php artisan tenant:config 123 --set features.analytics=true# Migrate specific tenant
php artisan tenant:migrate 123
# Migrate all tenants with progress bar
php artisan tenant:migrate --all --progress
# Fresh migration with seeding
php artisan tenant:migrate --all --fresh --seed
# Rollback tenant migrations
php artisan tenant:migrate:rollback 123 --step=2
# Check migration status
php artisan tenant:migrate:status --all# Run commands for all tenants
php artisan tenant:run "cache:clear" --all
# Run for specific tenants
php artisan tenant:run "queue:work" --tenants=1,2,3
# Run with parallel processing
php artisan tenant:run "data:process" --all --parallel
# Background execution
php artisan tenant:run "reports:generate" --all --background# Complete system health check
php artisan tenant:diagnose
# Check specific components
php artisan tenant:diagnose --check-config
php artisan tenant:diagnose --check-requirements
php artisan tenant:diagnose --check-integrity
php artisan tenant:diagnose --check-performance
# Auto-fix common issues
php artisan tenant:diagnose --fix
# Monitor real-time metrics
php artisan tenant:monitor --real-time# Backup specific tenant
php artisan tenant:backup 123 --storage=s3 --compress
# Backup all tenants
php artisan tenant:backup --all --encrypt
# Restore from backup
php artisan tenant:restore 123 --from=backup-20241201.sql.gz
# List available backups
php artisan tenant:backup:list 123use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Litepie\Tenancy\Traits\TenantAware;
class SendInvoiceReminders implements ShouldQueue
{
use Queueable, InteractsWithQueue, TenantAware;
public int $timeout = 300;
public int $tries = 3;
public function handle()
{
$tenant = tenancy()->current();
// Process overdue invoices for current tenant
$overdueInvoices = Invoice::where('status', 'pending')
->where('due_date', '<', now())
->with('customer')
->get();
foreach ($overdueInvoices as $invoice) {
// Send reminder email
Mail::to($invoice->customer->email)
->send(new InvoiceReminder($invoice));
// Log the reminder
$invoice->reminders()->create([
'sent_at' => now(),
'type' => 'overdue',
]);
}
Log::info("Sent {$overdueInvoices->count()} invoice reminders", [
'tenant_id' => $tenant->id,
'tenant_name' => $tenant->name,
]);
}
public function failed(\Throwable $exception)
{
Log::error('Invoice reminder job failed', [
'tenant_id' => tenancy()->current()?->id,
'exception' => $exception->getMessage(),
]);
}
}// config/tenancy.php
'queue' => [
'tenant_aware' => true,
'per_tenant_workers' => true,
'tenant_queue' => 'tenant-{tenant_id}',
'serialize_tenant' => true,
'max_retries' => 3,
'retry_delay' => 60,
],# Start tenant-specific workers
php artisan queue:work --queue=tenant-123
php artisan queue:work --queue=tenant-456
# Start multi-tenant worker
php artisan tenant:queue:work --all-tenantsuse Litepie\Tenancy\Events\TenantActivated;
use Litepie\Tenancy\Events\TenantDeactivated;
use Litepie\Tenancy\Events\TenantCreated;
use Litepie\Tenancy\Events\TenantDeleted;
// In your EventServiceProvider
protected $listen = [
TenantActivated::class => [
ConfigureTenantSettings::class,
InitializeTenantServices::class,
LogTenantAccess::class,
],
TenantCreated::class => [
SetupTenantDatabase::class,
CreateTenantDirectories::class,
SendWelcomeEmail::class,
],
TenantDeactivated::class => [
CleanupTenantCache::class,
LogTenantExit::class,
],
];class ConfigureTenantSettings
{
public function handle(TenantActivated $event): void
{
$tenant = $event->tenant;
// Configure tenant-specific application settings
config([
'app.name' => $tenant->getConfig('app_name', config('app.name')),
'app.timezone' => $tenant->getConfig('timezone', 'UTC'),
'mail.from.name' => $tenant->getConfig('mail_from_name'),
'mail.from.address' => $tenant->getConfig('mail_from_address'),
]);
// Set up tenant-specific services
if ($tenant->hasConfig('stripe_key')) {
app()->instance('stripe', new StripeService(
$tenant->getConfig('stripe_key'),
$tenant->getConfig('stripe_secret')
));
}
if ($tenant->hasConfig('analytics_key')) {
app()->instance('analytics', new GoogleAnalytics(
$tenant->getConfig('analytics_key')
));
}
}
}// Automatic tenant scoping - prevents cross-tenant data leaks
$orders = Order::all(); // Only returns current tenant's orders
// Explicit tenant filtering when needed
$orders = Order::forTenant($specificTenant)->get();
// Bypass tenant scoping (use with extreme caution)
$allOrders = Order::withoutTenantScope()->get();
// Multi-tenant queries with explicit control
$crossTenantData = Order::withoutTenantScope()
->whereIn('tenant_id', $authorizedTenantIds)
->get();// config/tenancy.php
'security' => [
'strict_isolation' => true,
'validate_tenant_access' => true,
'prevent_cross_tenant_access' => true,
'rate_limiting' => true,
'rate_limit_per_minute' => 1000,
'audit_logging' => true,
'csrf_protection' => true,
'ip_whitelist' => false,
'encrypt_config' => true,
],use Illuminate\Support\Facades\RateLimiter;
// In your RouteServiceProvider
RateLimiter::for('tenant-api', function (Request $request) {
$tenant = tenancy()->current();
if (!$tenant) {
return Limit::perMinute(100); // Default limit
}
$limit = $tenant->getConfig('rate_limit', 1000);
return Limit::perMinute($limit)->by(
$tenant->id . ':' . $request->user()?->id ?: $request->ip()
);
});
// Apply to routes
Route::middleware(['throttle:tenant-api'])->group(function () {
Route::apiResource('orders', OrderController::class);
});// Automatically logs tenant operations when enabled
'security' => [
'audit_logging' => true,
],
// Custom audit logging
use Litepie\Tenancy\Support\TenantAuditor;
TenantAuditor::log('order_created', [
'order_id' => $order->id,
'amount' => $order->amount,
'user_id' => auth()->id(),
]);use Litepie\Tenancy\Support\HealthChecker;
// Check overall system health
$healthStatus = HealthChecker::checkAll();
if (!$healthStatus->isHealthy()) {
foreach ($healthStatus->getIssues() as $issue) {
Log::error("Tenancy health issue: {$issue->getMessage()}");
// Send alert
if ($issue->isCritical()) {
notify_admins($issue);
}
}
}
// Check specific tenant health
$tenantHealth = HealthChecker::checkTenant($tenant);use Litepie\Tenancy\Contracts\HealthCheckContract;
use Litepie\Tenancy\Support\HealthCheckResult;
class DatabaseConnectionHealthCheck implements HealthCheckContract
{
public function check(): HealthCheckResult
{
try {
$tenants = Tenant::active()->limit(10)->get();
foreach ($tenants as $tenant) {
$tenant->execute(function () {
DB::connection('tenant')->getPdo();
});
}
return HealthCheckResult::success('All tenant databases are accessible');
} catch (\Exception $e) {
return HealthCheckResult::failure(
"Tenant database health check failed: {$e->getMessage()}"
);
}
}
public function name(): string
{
return 'Tenant Database Connectivity';
}
}// config/tenancy.php
'monitoring' => [
'metrics' => true,
'performance_monitoring' => true,
'resource_monitoring' => true,
],
// Access metrics
use Litepie\Tenancy\Support\MetricsCollector;
$metrics = MetricsCollector::getTenantMetrics($tenant, [
'period' => '24h',
'metrics' => ['requests', 'response_time', 'memory_usage', 'db_queries'],
]);
// Real-time monitoring
php artisan tenant:monitor --real-time --metrics=requests,memory,dbuse Litepie\Tenancy\Testing\TenancyTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TenantFeatureTest extends TenancyTestCase
{
use RefreshDatabase;
public function test_tenant_can_create_orders()
{
// Create a test tenant with specific configuration
$tenant = $this->createTenant([
'name' => 'Test Company',
'domain' => 'test.example.com',
'config' => [
'timezone' => 'America/New_York',
'features' => ['api_access', 'advanced_analytics'],
],
]);
// Switch to tenant context
$this->actingAsTenant($tenant);
// Create tenant-specific test data
$customer = Customer::factory()->create([
'name' => 'John Doe',
'email' => '[email protected]',
]);
$order = Order::create([
'customer_id' => $customer->id,
'amount' => 150.00,
'status' => 'pending',
'items' => [
['name' => 'Product A', 'price' => 100.00],
['name' => 'Product B', 'price' => 50.00],
],
]);
// Assertions
$this->assertTenantIs($tenant);
$this->assertDatabaseHas('orders', [
'id' => $order->id,
'tenant_id' => $tenant->id,
'customer_id' => $customer->id,
]);
// Test tenant isolation
$this->assertEquals(1, Order::count());
$this->assertEquals(1, Customer::count());
}
public function test_tenant_isolation_prevents_cross_tenant_access()
{
$tenant1 = $this->createTenant(['name' => 'Tenant 1']);
$tenant2 = $this->createTenant(['name' => 'Tenant 2']);
// Create data for tenant 1
$this->actingAsTenant($tenant1);
$order1 = Order::factory()->create(['amount' => 100]);
// Switch to tenant 2
$this->actingAsTenant($tenant2);
$order2 = Order::factory()->create(['amount' => 200]);
// Verify complete isolation
$this->assertEquals(1, Order::count()); // Only sees tenant 2's data
$this->assertEquals($order2->id, Order::first()->id);
$this->assertNotEquals($order1->id, Order::first()->id);
// Test explicit cross-tenant queries fail safely
$crossTenantOrder = Order::find($order1->id);
$this->assertNull($crossTenantOrder);
}
public function test_tenant_configuration_inheritance()
{
$tenant = $this->createTenant([
'config' => [
'timezone' => 'Europe/London',
'features' => ['analytics'],
'limits' => ['users' => 50],
],
]);
$this->actingAsTenant($tenant);
// Test configuration access
$this->assertEquals('Europe/London', tenant_config('timezone'));
$this->assertEquals(['analytics'], tenant_config('features'));
$this->assertEquals(50, tenant_config('limits.users'));
$this->assertEquals('default', tenant_config('non_existent', 'default'));
}
}class TenantDatabaseTest extends TenancyTestCase
{
public function test_separate_database_isolation()
{
config(['tenancy.database.strategy' => 'separate']);
$tenant1 = $this->createTenant(['name' => 'DB Tenant 1']);
$tenant2 = $this->createTenant(['name' => 'DB Tenant 2']);
// Verify separate databases
$this->actingAsTenant($tenant1);
$db1 = DB::connection()->getDatabaseName();
$this->actingAsTenant($tenant2);
$db2 = DB::connection()->getDatabaseName();
$this->assertNotEquals($db1, $db2);
$this->assertStringContainsString('tenant_', $db1);
$this->assertStringContainsString('tenant_', $db2);
}
}class TenantPerformanceTest extends TenancyTestCase
{
public function test_tenant_switching_performance()
{
$tenants = $this->createTenants(10);
$startTime = microtime(true);
foreach ($tenants as $tenant) {
$this->actingAsTenant($tenant);
// Perform typical operations
Order::factory(5)->create();
Customer::factory(3)->create();
}
$endTime = microtime(true);
$executionTime = $endTime - $startTime;
// Assert reasonable performance (adjust based on your requirements)
$this->assertLessThan(5.0, $executionTime, 'Tenant switching took too long');
}
}# Run all tests
composer test
# Run with coverage
composer test-coverage
# Run specific test categories
vendor/bin/phpunit --group=tenant-isolation
vendor/bin/phpunit --group=performance
vendor/bin/phpunit --group=security
# Run tests for specific feature
vendor/bin/phpunit tests/Feature/TenantDatabaseTest.php
# Parallel testing (if using Paratest)
vendor/bin/paratest --processes=4# === Production Optimizations ===
APP_ENV=production
APP_DEBUG=false
# === Tenancy Configuration ===
TENANCY_DETECTION_STRATEGY=domain
TENANCY_DATABASE_STRATEGY=separate
TENANCY_CACHE_STRATEGY=prefixed
# === Performance Settings ===
TENANCY_CONNECTION_POOLING=true
TENANCY_CACHE_MODELS=true
TENANCY_LAZY_LOADING=true
TENANCY_MEMORY_OPTIMIZATION=true
TENANCY_QUERY_CACHING=true
TENANCY_PRELOAD_CONFIG=true
# === Security Settings ===
TENANCY_STRICT_ISOLATION=true
TENANCY_PREVENT_CROSS_ACCESS=true
TENANCY_VALIDATE_ACCESS=true
TENANCY_AUDIT_LOGGING=true
TENANCY_RATE_LIMITING=true
TENANCY_RATE_LIMIT=1000
# === Monitoring ===
TENANCY_HEALTH_CHECKS=true
TENANCY_METRICS=true
TENANCY_PERFORMANCE_MONITORING=true
TENANCY_ALERTING=true
# === Backup ===
TENANCY_BACKUP_ENABLED=true
TENANCY_BACKUP_FREQUENCY=daily
TENANCY_BACKUP_RETENTION=30
TENANCY_BACKUP_ENCRYPTION=true
# === Debug (Disable in Production) ===
TENANCY_DEBUG=false
TENANCY_DEBUG_DETECTION=false
TENANCY_DEBUG_DB=false
TENANCY_DEBUG_QUERIES=false// config/database.php - Optimized tenant connection
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => null, // Set dynamically
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => 'InnoDB ROW_FORMAT=DYNAMIC',
'options' => [
PDO::ATTR_TIMEOUT => 60,
PDO::ATTR_PERSISTENT => true,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
PDO::ATTR_EMULATE_PREPARES => false,
],
'pool' => [
'size' => 20,
'timeout' => 60,
],
],// config/cache.php - Optimized for multi-tenancy
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'prefix' => env('CACHE_PREFIX', 'laravel_database'),
'serializer' => 'igbinary', // Better performance than PHP serializer
],
'tenant' => [
'driver' => 'redis',
'connection' => 'cache',
'prefix' => 'tenant_cache',
'serializer' => 'igbinary',
],
],// config/queue.php - Tenant-aware queues
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
'tenant' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'tenant-{tenant_id}',
'retry_after' => 90,
'block_for' => null,
],
],# Nginx configuration for tenant routing
server {
listen 80;
server_name ~^(?<tenant>.+)\.myapp\.com$;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Tenant-Domain $tenant;
}
}# Health check endpoint
curl -f http://myapp.com/health/tenancy || exit 1
# Monitor tenant metrics
php artisan tenant:monitor --output=json > /var/log/tenant-metrics.json
# Set up alerting
php artisan tenant:alert:setup [email protected] --slack=webhook_url# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy Application
run: |
# Deploy application code
./deploy.sh
- name: Run Tenant Health Checks
run: |
php artisan tenant:diagnose --check-all
- name: Migrate Tenant Databases
run: |
php artisan tenant:migrate --all --force
- name: Warm Up Caches
run: |
php artisan tenant:cache:warm --all
- name: Verify Deployment
run: |
php artisan tenant:verify --allProblem: Tenant detection fails in production
Diagnosis:
# Check detection configuration
php artisan tenant:diagnose --check-config
# Test detection manually
php artisan tinker
>>> $request = request();
>>> $detector = app(\Litepie\Tenancy\Contracts\TenantDetectorContract::class);
>>> $tenant = $detector->detect($request);
>>> dump($tenant);Solutions:
- Verify DNS configuration points to your application
- Check excluded subdomains configuration
- Validate cache settings aren't interfering
- Ensure headers are properly forwarded through load balancers
Problem: Tenant database connections fail
Diagnosis:
# Check database connectivity
php artisan tenant:diagnose --check-integrity
# Test specific tenant database
php artisan tinker
>>> $tenant = \Litepie\Tenancy\Models\Tenant::find(1);
>>> $tenant->activate();
>>> DB::connection('tenant')->getPdo();Solutions:
// Increase connection timeout
'database' => [
'connection_timeout' => 120,
'max_connections' => 50,
],
// Enable connection pooling
'performance' => [
'connection_pooling' => true,
],Problem: Slow tenant switching or queries
Diagnosis:
# Enable debug mode temporarily
TENANCY_DEBUG_QUERIES=true
TENANCY_DEBUG_PERFORMANCE=true
# Monitor performance
php artisan tenant:monitor --memory --queriesSolutions:
// Optimize caching
'cache' => [
'strategy' => 'prefixed',
'tenant_store' => 'redis',
'enable_tagging' => true,
],
// Enable performance features
'performance' => [
'cache_tenant_models' => true,
'query_caching' => true,
'preload_config' => true,
],Problem: High memory usage with many tenants
Solutions:
// Enable memory optimization
'performance' => [
'memory_optimization' => true,
'lazy_loading' => true,
'batch_threshold' => 50, // Lower for memory-constrained environments
],# Process tenants in batches
php artisan tenant:run "heavy:command" --batch-size=10Problem: Jobs not maintaining tenant context
Diagnosis:
# Check queue configuration
php artisan tenant:diagnose --check-config
# Monitor queue workers
php artisan queue:monitorSolutions:
// Ensure TenantAware trait is used
class MyJob implements ShouldQueue
{
use TenantAware; // This is crucial
}
// Configure queue properly
'queue' => [
'tenant_aware' => true,
'serialize_tenant' => true,
],# Enable comprehensive debugging (development only)
TENANCY_DEBUG=true
TENANCY_DEBUG_DETECTION=true
TENANCY_DEBUG_DB=true
TENANCY_DEBUG_SWITCHES=true
TENANCY_DEBUG_QUERIES=true
TENANCY_DEBUG_PERFORMANCE=true
TENANCY_DEBUG_MEMORY=true
# Show tenant info in response headers
TENANCY_DEBUG_HEADERS=true// config/logging.php
'channels' => [
'tenancy' => [
'driver' => 'daily',
'path' => storage_path('logs/tenancy.log'),
'level' => 'info',
'days' => 30,
],
],- π Documentation: https://tenancy.litepie.com
- π Bug Reports: GitHub Issues
- π¬ Discussions: GitHub Discussions
- π§ Email Support: [email protected]
- πΌ Enterprise Support: [email protected]
We welcome contributions! Here's how you can help:
- π Report Bugs: Use GitHub Issues with detailed reproduction steps
- β¨ Feature Requests: Propose new features in GitHub Discussions
- π Documentation: Help improve our documentation
- π§ͺ Testing: Add test cases for edge cases
- π» Code Contributions: Submit pull requests with new features or fixes
# Clone the repository
git clone https://github.com/litepie/tenancy.git
cd tenancy
# Install dependencies
composer install
# Set up testing environment
cp .env.testing.example .env.testing
php artisan key:generate --env=testing
# Run tests
./vendor/bin/phpunit
# Run code style checks
./vendor/bin/pint
# Run static analysis
./vendor/bin/phpstan analyse- PSR-12 coding standard
- PHPStan Level 8 static analysis
- 95%+ test coverage requirement
- Semantic versioning for releases
Litepie Tenancy is open-sourced software licensed under the MIT License. See the LICENSE file for details.
- Litepie Development Team - Core development and maintenance
- Community Contributors - Features, bug fixes, and documentation improvements
- Laravel Team - For the amazing framework that makes this possible
- Spatie Team - For inspiration from their multitenancy solutions
- Contributors - All the developers who have contributed code, tests, and documentation
We thank our sponsors who make this project possible:
- Enterprise Sponsors - Companies using Litepie Tenancy in production
- Individual Sponsors - Developers supporting open source
Made with β€οΈ by the Lavalite Team
β Star us on GitHub | π Report Issues | π¬ Join Discussions