Open-source project
gecche/laravel-multidomain avatar
gecche/laravel-multidomain

laravel-multidomain: one Laravel install, one .env and one database per domain

A Laravel extension for using a laravel application on a multi domain setting

1,121 stars134 forksPHPMIT

At a glance

What is it?
laravel-multidomain lets a single Laravel application serve several HTTP domains, each with its own environment file, storage path and database, by overriding the domain detection at bootstrap. It is a per-domain configuration tool rather than a full multi-tenancy system, and it requires editing the application container to work.
Who is it for?
Adopt laravel-multidomain if you run the same Laravel codebase for several customers or brands that each need their own database, environment settings and uploads, and you are happy to run domain:add for each site and provision the generated environment files on each server.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 148 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem: same code, different database and storage per customer

laravel-multidomain is a Laravel extension for using a Laravel application in a multi domain setting, and the README describes the situation it addresses in one paragraph. There are many cases in which different customers use the same application in terms of code but not in terms of database, storage and configuration. The package gives a simple way to get a specific env file, a specific storage path and a specific database for each such customer.

That framing is precise, and it is worth staying inside it. The unit of separation here is the domain, and what varies per domain is exactly three things: the environment file, the storage path, and the database connection. The code is shared. The deployment is shared. The application process is shared.

The package is PHP, licensed MIT, and the repository topics name the territory it occupies: laravel, multi-domains, multi-env, multi-tenancy and tenancy. Note that multi-tenancy is listed alongside multi-env, and the difference matters. Multi-tenancy as a term in the Laravel ecosystem usually means one database with a tenant_id column, or one database per tenant with a shared schema, managed by a tenancy package that resolves the tenant at request time. laravel-multidomain is closer to the multi-env approach: separate Laravel environments, each with its own .env, selected before the framework boots.

The audience is therefore an operator with several brands or customer sites running the same code, who has decided that separate environments is the right granularity. That is a defensible choice when the sites differ in configuration rather than in data model, and when a per-site database is acceptable.

The repository is not archived and the last push was on 2026-05-28. There are no GitHub releases, and the README's compatibility table is the authoritative version guide.

Domain detection has to be overridden before the framework boots

The technical core of the package is stated plainly in the README. It needs to override the detection of the HTTP domain in a minimal set of Laravel core functions at the very start of the bootstrap process in order to get the specific environment file. And that is why it needs more configuration steps than most Laravel packages.

The reason is ordering. A normal Laravel application boots, loads .env, resolves the configuration, and only then knows which database it is talking to. By that point the domain has to have been detected from the request, and the environment file has to have been chosen before the config is built. So the package substitutes the application class itself. At the very top of bootstrap/app.php, the import changes:

php
//use Illuminate\Foundation\Application
use Gecche\Multidomain\Foundation\Application

The package's Application subclass is what reads the incoming host, finds the matching entry in the domains configuration, and loads the right .env file so that the rest of the framework boots with that environment's values. The README notes that the result is that your application is aware of the HTTP domain it is running in, both for HTTP and CLI requests, including queue support.

That CLI and queue part is the part that is easy to miss and expensive to get wrong. A queued job does not arrive with an HTTP request, so a package that only looked at the request would have no domain to resolve and would run the worker against the wrong environment. Handling it is what the extended QueueServiceProvider is for, and the installation replaces it in config/app.php:

php
    'providers' => \Illuminate\Support\ServiceProvider::defaultProviders()->merge([
        // Package Service Providers...
    ])->replace([
      \Illuminate\Queue\QueueServiceProvider::class => \Gecche\Multidomain\Queue\QueueServiceProvider::class,
    ])->merge([
        // Added Service Providers (Do not remove this line)...
    ])->toArray(),

The README also notes that if you have already changed the providers entry in config/app.php for other reasons, the only important line is the one which replaces the QueueServiceProvider. That is a helpful warning, because this is the kind of file that many applications have customised, and a blind overwrite would be a bad way to lose that work.

The artisan commands that create and destroy a domain

Day-to-day use is three artisan commands, and the domain:add one is the entry point for everything else.

The command takes the HTTP domain as its argument:

bash
php artisan domain:add site1.com

and the same for a second domain:

bash
php artisan domain:add site2.com

Each one does four things, and the README describes them precisely. It creates a new environment file,.env.site1.com or.env.site2.com, in which you put the specific configuration for that site, such as the database configuration and the cache configuration and other settings you would normally find in an environment file. It adds an entry in the domains key in the config/domains.php file. It creates two new folders, storage/site1_com/ and storage/site2_com/, which have the same folder structure as the main storage. And it expects the storage substructure customisations to be matched by values in the config/domain.php file.

The third artefact is the one with a naming wrinkle worth noting. The storage folder is named with an underscore, site1_com, not a dot, even though the environment file uses a dot,.env.site1.com. That inconsistency is in the documentation rather than in your head, and it matters if you write a deployment script that moves or cleans up per-site directories.

Removal is the mirror image:

bash
php artisan domain:remove site2.com

which deletes the environment file for that domain and removes the entry from the domains key in config/domains.php. Adding the force option also deletes the domain storage folder, which is the destructive part and the one to be careful with, since it takes the uploads with it.

The third command, domain:update_env, is not fully described in the README, so its exact behaviour is not something to guess at. What the surrounding text does establish is that there are three commands, and that the first two are about adding and removing domains.

One design consequence follows from the whole approach. Because the domain list lives in config/domains.php and the per-domain settings live in.env.site1.com files, a deployment has to carry those files with it. They hold database credentials, so they cannot be in a public repository, and they have to be provisioned on the server in step with the code. The command set is convenient, but it does not remove the operational work of running several environments.

Version numbering that changed shape twice

The compatibility table in the README is long and unusual, and it is worth reading as history rather than as a lookup.

For Laravel 5.x the package had its own numbering. Laravel 5.5 mapped to multidomain 1.1.x, 5.6 to 1.2.x, 5.7 to 1.3.x and 5.8 to 1.4.x, and 6.x to 2.x, 7.x to 3.x, 8.x to 4.x and 9.x to 5.x. The reasoning is given in the notes. Releases have been separated in order to run integration tests with the corresponding version of the Laravel framework, which is a sensible approach and explains why there are a dozen branches of the same code with functionally equivalent behaviour.

From Laravel 10.x up, the package versions follow the same numbering as the framework, and the README records that change with a dated update from 2023-02-20. So 10.x pairs with multidomain 10.x, 11.x with 11.x, 12.x with 12.x and 13.x with 13.x.

The badges at the top of the README list Laravel 5.x through 13.x, which is the project's stated support range, but the notes underneath qualify it. v1.0 requires Laravel 5.1 to 5.4 and is no longer maintained and not tested against Laravel 5.4, though the usage of the package is the same as for 1.1. The 1.1.x line has a specific caveat: from v1.1.0 to v1.1.5 those releases are fully compatible with Laravel 5.5, 5.6, 5.7, 5.8 or 6.0, and from v1.1.6 the 1.1.x releases are only compatible with Laravel 5.5 in order to run tests correctly. A dated update from 2021-02-13 notes that some last features for the 1.1+ releases are still ongoing.

The practical advice is narrow. If you are on a modern Laravel, which is most likely, you use the version with the matching number and the compatibility question is settled. If you are on an old Laravel, you need to read the notes for that specific line rather than assuming the badge tells the whole story.

There are no GitHub releases for this repository, so the composer.json constraint is how you pin the version, which is why the installation instructions give the 13.* constraint in the example.

The limits: shared code, shared process, shared deployment

The boundary of this package is worth stating without softening, because the word multi-tenancy in the topics invites a stronger expectation than the implementation delivers.

First, there is no isolation between domains beyond the three things that vary. The code, the application code, is one codebase. A bug or a vulnerability in the shared code affects every domain. There is no per-tenant authorisation layer, no role model, no policy engine, because the package does not know what a tenant is. It knows what a domain is.

Second, the separation is by environment file, and the environment file is read at boot. So the database selection happens when the process starts, not when a request arrives. In a typical PHP-FPM deployment each request is a fresh process, so this works. Under a long-lived worker, such as a persistent application server or a queue worker, the environment is fixed for the life of the process, which is exactly why the package has to override the QueueServiceProvider to make the worker resolve the right domain. The README mentions Horizon compatibility with a second import swap in app/Providers/HorizonServiceProvider.php, which is the same ordering problem showing up in a third place.

Third, the storage separation is by directory convention. The storage/site1_com/ folders have the same structure as the main storage, and the customisations have to be matched by values in the config/domain.php file, which means the per-domain configuration of the storage layer is manual and duplicated between the config file and the directory layout. Anything that writes to storage outside the framework's storage helper needs to know which domain it is in, and the package's job is to make that available, not to enforce it.

Fourth, the operational cost. Every domain needs its own .env file with database credentials, its own database, and its own storage directory, and those have to be deployed in step with the code and kept in step with each other. domain:add makes the local side easy; it does not make the remote side easy.

So the honest use is this: one code base, several sites, each with its own data and configuration, deployed together. That is a legitimate pattern, and it is much simpler than a tenancy package. It is not, and should not be sold as, data isolation between mutually distrusting tenants.

laravel-multidomain against a real tenancy package

The alternative worth comparing is a Laravel tenancy package, and the difference in approach is one of granularity and isolation rather than features.

A tenancy package resolves a tenant during the request, from the hostname or the subdomain, and then scopes the database queries accordingly. The common designs are a shared database with a tenant_id column on every table, or a database per tenant with the same schema, with a central registry of which tenant lives where. The shared-schema design means one database server and one connection pool, with a filter applied to every query, and the connection-per-tenant design means the tenant's data is in its own database but the queries are not automatically scoped. In both cases the tenancy package is responsible for the resolution and for the scoping, and the developer has to write code that does not leak across tenants.

laravel-multidomain takes a different route. It does not resolve a tenant per request. It resolves an environment at boot, which means a separate database connection, separate cache configuration, separate mail settings, separate everything that lives in .env, for each domain. The isolation is at the configuration level and therefore at the database level too, since each environment points at its own database. There is no tenant_id, no query scoping, and no risk of a forgotten where clause leaking one customer's rows into another's page, because the connection is already the right one.

The trade is real in both directions. The multi-env approach gives you stronger separation, since a database mistake is contained by the environment, and it costs you an application process per domain or a resolution mechanism like the queue provider swap, plus duplicated configuration and a deployment that manages N environments. The per-request tenancy approach gives you one process and one deployment for all tenants, and it costs you a scoping discipline that every query has to respect.

So the deciding question is whether your sites differ in configuration or in data model. Sites that run the same application with different credentials, different branding and different uploads are a good fit for the multi-env approach, because the differences are exactly the three things this package varies. A SaaS product where one tenant can see another tenant's rows is a fit for a per-request tenancy design, and choosing the wrong one of these is a data leak waiting to happen.

Editorial conclusion

Adopt laravel-multidomain if you run the same Laravel codebase for several customers or brands that each need their own database, environment settings and uploads, and you are happy to run domain:add for each site and provision the generated environment files on each server. Do not adopt it as a multi-tenant isolation boundary, because tenants share the code, the deployment and the application process, and the database separation is per environment file rather than per request. Verify first that your Laravel version has a matching package major, since the 10.x and later lines use the same numbering as the framework while the 5.x era used a separate scheme, and that your queue driver, and Horizon if you use it, are configured as the installation steps require.

Frequently asked questions

How do I install laravel-multidomain?

Add gecche/laravel-multidomain to composer.json, then replace the Application import in bootstrap/app.php with Gecche\Multidomain\Foundation\Application, replace the QueueServiceProvider in config/app.php, and run `php artisan vendor:publish`. The package uses Laravel's package discovery.

Which Laravel versions does laravel-multidomain support?

The compatibility table pairs Laravel 13.x, 12.x, 11.x and 10.x with matching package majors, while the 5.x era used separate numbering, with 5.5 to 1.1.x and 6.x to 2.x. The README records that from Laravel 10.x up the package versions follow the same numbering as the framework.

What does adding a domain with domain:add create?

It creates an environment file such as .env.site1.com, adds an entry to the domains key in config/domains.php, and creates a storage folder such as storage/site1_com/ with the same structure as the main storage. The domain:remove command deletes the environment file and the config entry, and the force option also deletes the storage folder.

Does laravel-multidomain work with Laravel Horizon and queued jobs?

The package is compatible with Horizon through community contributions, and it supports queue processing because it overrides the QueueServiceProvider. Using Horizon additionally requires replacing the Horizon import in app/Providers/HorizonServiceProvider.php with Gecche\Multidomain\Horizon\HorizonApplicationServiceProvider.

Official sources

  1. gecche/laravel-multidomain on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gecche-laravel-multidomain.svg)](https://hysenlabs.com/projects/gecche-laravel-multidomain)

Was this useful?