tahoelimited/multi-tenancy-bundle
A Laravel multi-tenancy bundle from Tahoe Limited that helps developers build tenant-aware applications with isolated database connections, configurable tenant resolution, and secure separation of tenant data, ideal for SaaS platforms and multi-tenant APIs.
Read this first: it is a Symfony 2 bundle, not a Laravel package. There is no service provider, no config/multi-tenancy.php, and no Composer extra.laravel block. If you are assessing this to drop into a Laravel app, you cannot. Treat it as a reference implementation of host/subdomain tenant resolution and port the pattern. If you are actually maintaining a Symfony 2.x app, it installs the classic way:
// app/AppKernel.php
$bundles = [ new VMe\MultiTenancyBundle\VMeMultiTenancyBundle(), ];
# app/config/config.yml
vme_multi_tenancy:
tenants:
acme:
host: acme.example.com
# per-tenant parameters / doctrine connection
Where to look first, in order:
DependencyInjection/Configuration.php — the config tree is the entire public API. Every knob the bundle supports is defined here, regardless of what the README says.EventListener/ or EventSubscriber/) — this is where the tenant is resolved from the request. Almost all behaviour and almost all bugs live here.Resources/config/services.xml — reveals the real service graph and listener priorities, which the docs usually omit.First Laravel-oriented use case: resolve Host → tenant → tenant-scoped DB connection, in a middleware. That is the minimum viable equivalent and the thing you will actually write:
// app/Http/Middleware/ResolveTenant.php
public function handle($request, Closure $next)
{
$host = $request->getHost();
$tenant = Tenant::where('domain', $host)->firstOrFail();
app()->instance('tenant', $tenant);
config(['database.connections.tenant' => $tenant->dbConfig()]);
DB::purge('tenant');
return $next($request);
}
The bundle's core pattern is request-scoped tenant resolution with late config injection. Mirror it deliberately in Laravel:
TenantManager singleton (app()->instance() or a scoped binding), and read it everywhere via constructor injection. Avoid a facade lookup in every model.tenant connection, call DB::purge('tenant') after mutating it, and set it as default (DB::setDefaultConnection('tenant')). The purge is what the Symfony bundle effectively does when it rebuilds the entity manager.host → tenant_id map (Redis or Cache::rememberForever) and hydrate the tenant record only on a miss.TenantScoped trait on models for global scopes so you never accidentally query across tenants.app()->instance('tenant', ...) + DB::purge) in the job's handle(). The bundle never solved this for Symfony CLI either — it is a universal gap.$this->withServerVariables(['HTTP_HOST' => 'acme.example.com'])->get(...) or $this->withHeader('Host', ...) to simulate tenants. Add a helper trait; you will need it constantly.For an actual Symfony 2 port, the equivalent is a kernel.request listener with priority above routing, injecting per-tenant parameters into the container before the router/Doctrine boot.
Host blindly. $request->getHost() honours X-Forwarded-Host behind a proxy, and a raw Host header is client-controlled. Whitelist against your cached tenant map and explicitly configure TrustProxies. Host-header injection here becomes cross-tenant data leakage, not just cache poisoning.co.uk, com.au, wildcard subdomains, and www all break naive explode('.', $host) logic. If the bundle's resolver uses a simple regex, verify it against your real domains before trusting it.php artisan config:cache freezes config(). Anything you derive at runtime must go through config(['...' => $value]) after boot (it will not be picked up if read before the middleware runs) or through a bound object, not a cached config file.DB::purge()/reconnect, and in long-running workers (Octane, Horizon) connections leak across requests. Reset tenant state in a terminating callback..acme.example.com) or users will cross-authenticate between tenants.How can I help you explore Laravel packages today?