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

Laravel Auto Tenancy Laravel Package

worldesports/laravel-auto-tenancy

Automatic multi-tenancy for Laravel. Automatically scopes models, queries, and database connections to the current tenant with minimal configuration, so you can add tenant isolation to existing apps without rewriting your code. A lightweight package for building SaaS and multi-tenant apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish:

composer require worldesports/laravel-auto-tenancy
php artisan vendor:publish --provider="WorldEsports\AutoTenancy\AutoTenancyServiceProvider"

Then read config/auto-tenancy.php before touching anything else — it defines the central connection name, the tenant connection template, the tenant model, and the column/attribute used to resolve a tenant from the authenticated user. Everything else in the package is downstream of those four values. The second place to look is the package's middleware directory (src/Http/Middleware or similar); that is where the actual DB::purge / config(['database.connections.…']) / DB::reconnect handoff happens, and it is short enough to read top-to-bottom in a few minutes.

Mental model: one central database holds users, tenants, and anything global; each tenant owns a physically separate database. Before authentication there is no tenant, so the app runs against the central connection. After authentication, a middleware resolves the tenant from the user and swaps the default connection for the rest of the request.

First use case to validate end-to-end:

  1. Configure the central connection plus a tenant connection template (host/username/password shared, database name supplied per tenant).
  2. Run the package migrations against the central DB only.
  3. Add the tenant trait to your User and/or Tenant model as the config instructs.
  4. Create one tenant record and provision its database.
  5. Run migrations against the tenant connection.
  6. Log in as that tenant's user and assert:
Route::get('/whoami', fn () => [
    DB::connection()->getName(),
    DB::connection()->getDatabaseName(),
    auth()->user()->tenant_id,
]);

If those three agree, the package is working. If they don't, the middleware is in the wrong place in the stack — see Gotchas.

Before you invest: the license is NOASSERTION. That is a legal blocker for most companies until clarified with the maintainer. With 33 stars and zero dependents there is effectively no community to fall back on, so budget a half-day to read the entire src/ tree — it is almost certainly a few hundred lines, and you need to know it cold because you will be maintaining it.

Implementation Patterns

Middleware placement is the whole design. The package is "post-authentication," which means the switch must happen after the session/auth middleware has resolved a user but before any controller, policy, or model touches a tenant table. Register it into the web group explicitly after StartSession and Authenticate, or attach it per-route-group:

Route::middleware(['auth', 'auto-tenancy'])->group(function () {
    // tenant-scoped routes
});

Keep a parallel, tenant-free route group for login, registration, billing, and the tenant-selection screen. Those routes must never see a switched connection.

Resolution strategies. The default is "tenant from auth()->user()," which covers the SaaS case. For admin impersonation and support tooling, add a second resolver that reads a signed session key set by a deliberate "enter tenant" action, and make sure it is gated by a policy — otherwise a crafted session key is a full cross-tenant read.

Provisioning as an explicit pipeline. Do not create the database inside the registration controller. Write a queued job (ProvisionTenant) that: creates the DB, switches the connection manually, runs Artisan::call('migrate', ['--path' => 'database/migrations/tenant', '--force' => true]), seeds defaults, and marks the tenant active. Idempotency matters here — a retried queue job must not fail halfway and leave a half-migrated database. Record a provisioned_at timestamp and check it before re-running.

Queue jobs lose the connection. A serialized job carries no database connection. Capture the tenant identifier on the job and re-establish it in handle():

public function __construct(public int $tenantId) {}

public function handle(): void
{
    Tenancy::forTenant($this->tenantId, fn () => $this->doWork());
}

If the package doesn't expose a helper like that, write a small TenantAware trait with a run() wrapper around the same purge/config/reconnect calls the middleware uses — reusing the package's own switch logic rather than duplicating it keeps behaviour consistent.

Console and scheduler are yours to handle. php artisan migrate will only hit whatever connection is default at boot, i.e. central. Build a tenants:artisan style command that iterates active tenants and calls the requested command per tenant with --force. Same for scheduled jobs — a scheduled closure runs once, centrally; wrap the tenant loop inside it.

Testing. Two databases per test is painful. The pragmatic pattern is sqlite :memory: for central and a per-test file-backed or in-memory tenant connection, with RefreshDatabase on the central connection plus an explicit migrate of the tenant migration path in setUp(). Avoid DatabaseTransactions across both connections — it will not roll back the tenant side and you will get bleed-through between tests that looks like a package bug.

Keep central and tenant models apart. Maintain an explicit list of central-only models and be disciplined: never write a relationship from a tenant model to a central model, and never join across the two. Cross-boundary work belongs in application code (fetch IDs centrally, pass them into the tenant context).

Gotchas and Tips

  • Connection object caching. Laravel caches resolved connection instances. A switch that only mutates config('database.connections.tenant.database') without DB::purge('tenant') will silently keep using the previous database. Verify the package purges; if you write your own switch anywhere, purge first.
  • Long-running processes leak tenants. Under Octane, Horizon, or any queue worker, the connection and config mutations persist between jobs/requests. Every entry point needs a deterministic set-and-reset, and every exit path (including exceptions) needs a reset. Add a finally block.
  • config:cache freezes the connection template. Do not compute the tenant database name in config/database.php at boot — it will be baked in. Set it at runtime only.
  • Debugging kit. When things look wrong, log DB::connection()->getName() and getDatabaseName() at three points: middleware entry, controller entry, and after any manual DB::reconnect. Nine times out of ten the answer is "the middleware never ran" or "we switched the default but a model pinned $connection."
  • Stray $connection on models. Any model with a hardcoded protected $connection ignores the switch entirely. Grep for it.
  • Silent fallback to central. If the resolver returns null (unauthenticated, or a user with no tenant) and the middleware doesn't fail closed, requests proceed against the central database using the default connection. That is a data-leak shape, not a bug. Assert non-null and abort.
  • Transactions do not span databases. A central write plus a tenant write in one DB::transaction() is not atomic. Use an outbox or compensating action.
  • Deploy order. Run central migrations first, then the tenant loop, and gate the deploy on the tenant loop succeeding. A schema drift between tenants is the failure mode this package makes easy to create and hard to notice — keep a tenants:migrate --dry-run check in CI that compares migration state across all tenants.
  • Tenant database credentials. If you use a shared DB user across tenants, a single misconfigured name exposes everything. Prefer per-tenant credentials, or at minimum a per-tenant user with grants limited to its own schema.
  • Extension points to prefer (verify names in source): the tenant resolver binding, the connection-name config value, and any events fired around the switch. Hook observability into the switch event rather than editing the middleware — you will need to re-apply edits otherwise.
  • Maintenance reality check. NOASSERTION license, 33 stars, no dependents, no visible security policy. Fork it into your own namespace, pin the commit, and treat it as vendored code rather than a dependency you can upgrade casually. The upside: the surface area is small enough that owning it is cheap — which is exactly why reading all of src/ on day one is the right call.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle