gecche/laravel-multidomain
Run multiple HTTP domains from a single Laravel installation. Each domain gets its own env file, storage path, and database, letting different customers share code but keep separate configuration and data.
Architecture fit. gecche/laravel-multidomain implements hard multi-tenancy at the framework boot layer: one codebase, one deployment, but a distinct .env file, storage subtree, and (typically) database per HTTP host. Tenancy is resolved by intercepting the environment file selection inside a substituted Foundation\Application container, before configuration loads. That places it in a different category from full tenancy platforms (stancl/tenancy, hyn/multi-tenant) — it provides isolation primitives, not tenant lifecycle management. There is no provisioning API, no tenant-aware migration orchestration, no admin plane, and no tenancy-aware Eloquent layer. If the product requirement is "same application, different data per customer," the fit is good. If the requirement is "self-serve tenant onboarding, per-tenant feature segmentation, or row-level tenancy," it is the wrong tool.
Integration feasibility. Feasible, but this is not a drop-in Composer require. It demands two invasive edits: replacing the Laravel Application import in bootstrap/app.php, and replacing Illuminate\Queue\QueueServiceProvider in the provider list. Horizon requires a third substitution in HorizonServiceProvider. These are three touchpoints on framework internals that we must own in code review forever. The tenant registry lives in config/domains.php — a file, not a table — which makes tenant provisioning a deploy-time/config-management operation rather than a runtime one. That is a deliberate design choice and the single most consequential architectural constraint.
Technical risk. Ranked by impact:
.env is found, the standard .env is used. A misconfigured or newly pointed hostname will happily serve against the default database. This is a data-isolation hazard, not a bug, and it must be neutralized with an application-level allowlist assertion early in the request lifecycle.$_SERVER['SERVER_NAME']. The README explicitly flags environments where this is unset. Behind load balancers, CDNs, or container ingress, SERVER_NAME is frequently unreliable — which is why the package exposes domain_detection_function_web. We must own and test that closure per environment.config:cache is per-domain. php artisan config:cache --domain=site1.com writes config-site1_com.php. Miss one domain in the deploy pipeline and it runs on a stale or default cache — a class of failure that is invisible until a customer reports wrong data.Key questions. (a) What is the support model and response SLA from the maintainer — is there a funded fork or vendor backstop for security patches? (b) Is there a validated path for Laravel Octane / long-running workers, where per-request container boot semantics change? (c) Where do per-tenant secrets live in our target infrastructure — this design assumes env files on disk, which conflicts with 12-factor config injected as process environment. (d) Does the tenant registry need to be dynamic (e.g., customer-defined subdomains) within the next 18 months? If yes, this package alone will not carry us. (e) How many tenants do we expect at peak — the operational model costs one queue worker per domain.
Stack fit. Strong fit for a Laravel 10–13 monolith deployed as a single artifact per region, with a handful to a few dozen brand-level or customer-level domains, each needing isolated data and public storage. Poor fit for serverless/immutable-image deployments where writing .env.<domain> files at runtime is not possible, and poor fit for high-tenant-count (100+) self-serve SaaS.
Migration path. Two viable entry points:
config/domains.php is a static registry and will not map onto a database-backed tenant table without us building a synchronizer. Budget for it explicitly; do not assume a swap.Compatibility. The version matrix tracks Laravel majors 1:1 from 10.x onward, and older 5.x/6.x-era releases are functionally equivalent — good signal that the maintainer keeps parity with framework releases. Constraints to verify in a spike: Horizon (supported, with an extra substitution), storage:link (explicitly not supported per-domain — symlinks must be created manually and wired via APP_PUBLIC_STORAGE in each .env), Octane (undocumented), and any other package that also overrides bootstrap/app.php or the provider list.
Sequencing. Four gates, each a hard stop:
.env.config:cache --domain=…, generate/mount per-domain env files from our secret store, create per-domain storage symlinks, and run per-domain migrations. This is the bulk of the work and is ours, not the package's.Maintenance. Moderate-to-high and permanent. Every Laravel major upgrade becomes a package-compatibility gate plus a review of three framework-internal substitutions. The package is MIT with a single maintainer and no dependents to reveal failure patterns — we should assume we may need to fork and patch, and staff accordingly. The code surface is small, which makes forking tractable.
Support. Community-supported, no vendor. Internal ownership must be assigned to a named engineer with Laravel-internals fluency. Budget a standing allocation for upgrade work rather than treating this as a one-off integration.
Scaling. Two axes behave very differently. Per-tenant load scales horizontally without issue. Tenant count scales linearly in operational cost: one env file, one storage tree, one config cache, one set of migrations, and one queue worker process per domain. At 10–20 domains this is boring infrastructure. At 100+ it becomes a process-management and deploy-time problem that will push us toward a database-backed tenancy model. Set a threshold now — e.g., revisit at 25 domains — and write it into the decision record.
Failure modes. Ranked for on-call:
.env fallback — highest severity; mitigate with the allowlist assertion and a smoke test that hits each domain post-deploy.config-site*.php count matches domain:list in the pipeline.Ramp-up. A senior Laravel engineer can ship the two-domain pilot in about a week. Team-wide fluency — the deploy pipeline, the queue topology, the fallback guardrails — is more like three to four weeks of elapsed work, most of it in CI/CD and observability rather than in the package itself. Expect the package integration to be the small part of the effort; the operational scaffolding around it is the real cost.
Recommendation. Conditional go, pending a timeboxed spike that proves host detection behind our production ingress and demonstrates a fail-closed isolation guardrail. The package is well-matched, honest about its constraints, and small enough to fork — but it supplies isolation mechanics, not tenancy operations. Approve only with a named owner, a documented tenant-count threshold for revisiting, and deploy-pipeline work scoped as a first-class deliverable.
How can I help you explore Laravel packages today?