gecche/laravel-multidomain
Run one Laravel codebase across multiple domains/tenants. Automatically load a domain-specific .env, storage path, and database/config per customer, enabling isolated data and settings while sharing the same application and deployment.
Installation:
composer require gecche/laravel-multidomain:13.*
Replace bootstrap/app.php:
use Gecche\Multidomain\Foundation\Application;
Override QueueServiceProvider in config/app.php:
'providers' => [...]->replace([
\Illuminate\Queue\QueueServiceProvider::class => \Gecche\Multidomain\Queue\QueueServiceProvider::class,
])->merge([...])->toArray(),
Publish config:
php artisan vendor:publish
Add a Domain:
php artisan domain:add example.com
This creates:
.env.example.com (custom env file)storage/example_com/ (domain-specific storage)config/domains.phpVerify Setup:
php artisan domain:list
Check if example.com appears in the output.
Run a domain-specific Artisan command:
php artisan config:cache --domain=example.com
This generates config-example_com.php in the root directory.
Domain-Specific Configuration:
domain() helper in controllers/services to access the current domain:
$currentDomain = app()->domain();
$domains = app()->domainList();
Environment-Specific Logic:
.env.example.com for domain-specific settings (e.g., DB_DATABASE, APP_URL).env() as usual; the package auto-loads the correct .env file.Storage Isolation:
Storage facade are saved to storage/{domain}_com/.Storage::disk('public')->put('file.txt', 'content');
// Saves to: storage/example_com/app/public/file.txt
Domain-Specific Artisan Commands:
--domain to any Artisan command:
php artisan migrate --domain=example.com
Queue Management:
QUEUE_DEFAULT per domain in .env.example.com.php artisan queue:work --domain=example.com --queue=domain_queue
domain() helper in middleware to enforce domain-specific logic:
public function handle(Request $request, Closure $next) {
$domain = app()->domain();
if ($domain !== 'example.com') {
abort(403);
}
return $next($request);
}
boot():
public function boot() {
$domain = app()->domain();
$this->app->bind('domainService', function () use ($domain) {
return new DomainService($domain);
});
}
@inject('domain', 'Illuminate\Contracts\Foundation\Application')
Current Domain: {{ $domain->domain() }}
$_SERVER['SERVER_NAME'] Issues:
$_SERVER['SERVER_NAME'] for domain detection. If this is empty (e.g., CLI or non-standard environments), override it in bootstrap/app.php:
$domainParams = [
'domain_detection_function_web' => function() {
return $_SERVER['HTTP_HOST'] ?? 'default.com';
}
];
return Application::configure(..., domainParams: $domainParams)->create();
Storage Linking:
storage:link command does not support domain-specific symlinks. Manually create links:
ln -s storage/example_com/app/public public/storage-example_com
filesystems.php:
'public' => [
'url' => env('APP_URL').'/storage'.env('APP_PUBLIC_STORAGE', ''),
],
APP_PUBLIC_STORAGE=-example_com in .env.example.com.Queue Conflicts:
database queue driver, ensure each domain has a unique queue name (e.g., QUEUE_DEFAULT=example_com_queue). Avoid sharing queues across domains.Horizon Integration:
HorizonApplicationServiceProvider in app/Providers/HorizonServiceProvider.php:
use Gecche\Multidomain\Horizon\HorizonApplicationServiceProvider;
Environment File Permissions:
.env.example.com files are readable by the web server (e.g., chmod 644 .env.example.com).dd(app()->domain(), app()->domainList());
php artisan config:cache --domain=example.com and check for config-example_com.php.storage_path() to confirm paths:
dd(storage_path('example_com'));
--domain for domain-specific commands. Omitting it defaults to the primary domain.Custom Domain Detection:
Extend the domain_detection_function_web in bootstrap/app.php for non-standard setups (e.g., subdomains, headers):
'domain_detection_function_web' => function() {
return $_SERVER['HTTP_X_CUSTOM_DOMAIN'] ?? $_SERVER['SERVER_NAME'];
}
Dynamic Domain Routing: Use middleware to route based on the domain:
Route::domain('{domain}')->group(function () {
// Domain-specific routes
});
Combine with the package’s domain() helper for logic.
Domain-Specific Packages:
Load domain-specific packages in config/app.php:
'providers' => [
// ...
\Vendor\DomainSpecific\ExampleProvider::class => app()->domain() === 'example.com',
],
Event Listeners:
Listen for domain-specific events (e.g., domain.example.com):
Event::listen('domain.example.com', function () {
// Logic for example.com
});
Testing: Mock the domain in tests:
$this->app->instance('domain', 'test.com');
Or use the --domain flag in artisan tests:
$this->artisan('domain:list --domain=test.com');
How can I help you explore Laravel packages today?