Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Multi Tenancy Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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.
  2. The kernel request listener (typically under EventListener/ or EventSubscriber/) — this is where the tenant is resolved from the request. Almost all behaviour and almost all bugs live here.
  3. Any Doctrine subscriber / connection-switching service — confirm whether it actually swaps connections or only injects parameters.
  4. 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);
}

Implementation Patterns

The bundle's core pattern is request-scoped tenant resolution with late config injection. Mirror it deliberately in Laravel:

  • Middleware + container binding. Resolve once per request, bind a TenantManager singleton (app()->instance() or a scoped binding), and read it everywhere via constructor injection. Avoid a facade lookup in every model.
  • Per-tenant connection, not per-tenant database name in a global config. Register a 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.
  • Tenant registry in config/cache, not a DB hit per request. The bundle reads static YAML. Do the same: cache a host → tenant_id map (Redis or Cache::rememberForever) and hydrate the tenant record only on a miss.
  • Tenant-aware listeners for cross-cutting concerns. The bundle wires this through its request listener; in Laravel use middleware groups, or a TenantScoped trait on models for global scopes so you never accidentally query across tenants.
  • Propagate tenant context into queued work. Any job dispatched inside a tenant request must serialize the tenant id and re-bind it (app()->instance('tenant', ...) + DB::purge) in the job's handle(). The bundle never solved this for Symfony CLI either — it is a universal gap.
  • Testing harness. Use $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.

Gotchas and Tips

  • Framework mismatch is the headline. Symfony 2 is EOL, PHP 5.x/7.x-era, 25 stars, 0 dependents, effectively unmaintained. Do not build new infrastructure on it. Port the concept.
  • Never trust 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.
  • Subdomain parsing is deceptively hard. 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.
  • Config caching kills dynamic config. In Symfony the container is compiled in prod; in Laravel 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.
  • Entity manager keeps stale connections. Changing the connection config mid-process does nothing until you DB::purge()/reconnect, and in long-running workers (Octane, Horizon) connections leak across requests. Reset tenant state in a terminating callback.
  • CLI, queues, and schedulers have no request. Every artisan command that touches tenant data needs an explicit tenant argument, or it will hit the default connection silently. This is the single most common production incident with this pattern.
  • Cache and session key collisions. Prefix cache keys with the tenant id, and scope session cookies to the tenant domain (.acme.example.com) or users will cross-authenticate between tenants.
  • Extension points to know: the tenant resolver and the kernel-event listener priority are the two seams you will override. If you need custom resolution (header, path prefix, API key), subclass or replace the listener rather than editing vendor code — the bundle is small enough that forking is realistic, and given its abandonment, likely.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource