gecche/laravel-multidomain
Run one Laravel codebase across multiple domains/tenants. Automatically load per-domain .env, storage paths, and database connections so each customer can have isolated config, data, and files while sharing the same application code.
Installation:
composer require gecche/laravel-multidomain:13.*
Update bootstrap/app.php to replace Illuminate\Foundation\Application with Gecche\Multidomain\Foundation\Application.
Override QueueServiceProvider in config/app.php:
'providers' => [...]->replace([
\Illuminate\Queue\QueueServiceProvider::class => \Gecche\Multidomain\Queue\QueueServiceProvider::class,
])->merge([...])->toArray(),
Publish the config:
php artisan vendor:publish
Add a Domain:
php artisan domain:add site1.com
This creates:
.env.site1.com (custom env file)storage/site1_com/ (domain-specific storage)config/domains.php.Verify Setup:
php artisan domain:list
Check the current domain in code:
$currentDomain = app()->domain();
Multi-Tenant API Endpoint:
Create a route in routes/api.php:
Route::get('/tenant-data', function () {
$domain = app()->domain();
return response()->json([
'domain' => $domain,
'storage_path' => storage_path("{$domain}_com"),
'env_file' => ".env.{$domain}",
]);
});
Test with curl http://site1.com/api/tenant-data.
Domain-Specific Configuration:
app()->domain() to dynamically load domain-specific .env files.config() helper to read from the correct config file (e.g., config-site1_com.php):
$value = config('database.connections.mysql.host', null, $domain);
Storage Isolation:
$domainStorage = storage_path("{$domain}_com/uploads");
Storage::disk('local')->put() with domain-aware paths.Middleware for Tenant Context:
public function handle(Request $request, Closure $next) {
$domain = app()->domain();
config(['app.tenant' => $domain]);
return $next($request);
}
Artisan Commands with Domains:
php artisan migrate --domain=site1.com
php artisan queue:work --domain=site2.com --queue=site2-queue
Database Connections:
Define dynamic connections in config/database.php:
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
// Override per-domain
'database' => env('DB_DATABASE', "db_{$domain}"),
],
],
Queue Workers:
site1-queue, site2-queue)..env.site1.com:
QUEUE_DEFAULT=site1-queue
Horizon Integration:
Replace HorizonApplicationServiceProvider in app/Providers/HorizonServiceProvider.php:
use Gecche\Multidomain\Horizon\HorizonApplicationServiceProvider;
SPA Public Assets:
ln -s storage/site1_com/app/public public/storage-site1_com
filesystems.php:
'public' => [
'url' => env('APP_URL').'/storage'.env('APP_PUBLIC_STORAGE', ''),
],
$_SERVER['SERVER_NAME'] Missing:
$_SERVER['SERVER_NAME'] is empty (e.g., CLI or non-standard servers), override detection in bootstrap/app.php:
$domainParams = [
'domain_detection_function_web' => fn() => $_SERVER['HTTP_HOST'] ?? 'default',
];
Config Caching:
php artisan config:cache --domain=site1.com
php artisan config:clear --domain=site1.com
Queue Conflicts:
database driver. Use separate tables or queues.Storage Permissions:
storage/{domain}_com) are writable:
chmod -R 775 storage/site1_com
Check Current Domain:
dd(app()->domain(), app()->domainList());
Verify Env Loading:
vendor/gecche/laravel-multidomain/src/Foundation/Application.php:
Log::debug("Loaded env for domain: {$this->domain}");
Artisan Domain Issues:
--domain flag is ignored, ensure the QueueServiceProvider is properly overridden.Custom Domain Detection:
Extend Application to support subdomains or path-based routing:
'domain_detection_function_web' => function() {
return parse_url($_SERVER['REQUEST_URI'], PHP_URL_HOST);
},
Dynamic Config Loading:
Override config() in a service provider:
Config::addListener(function ($name) {
if (str_contains($name, 'tenant')) {
return config("tenant.{$app()->domain()}.{$name}");
}
});
Domain-Specific Routes: Use middleware to load routes dynamically:
Route::domain('{domain}', function () {
// Load domain-specific routes
})->where('domain', implode('|', array_keys(config('domains'))));
CLI Domain Defaults:
Set a default domain for CLI in config/domain.php:
'default_domain' => 'default.com',
How can I help you explore Laravel packages today?