A four-stop line for one tenant session: tenancy is initialized for tenant acme and the default connection becomes tenant; acme counts 3 notes and globex 1; a queued job run later by one worker still counts 3 and 1; then the track breaks at the first Cache::put, which throws BadMethodCallException, 'This cache store does not support tagging'. The fix: a store with tags, such as Redis.

Tenancy for Laravel review: what held, what broke, and our verdict against building it yourself

We installed stancl/tenancy on a clean Laravel 12 app, gave it two tenants and tried to make their data cross. Most of it held. One default broke on the first cache write, and a hand-rolled scope leaked on the first raw query.

What is our Tenancy for Laravel review verdict after running it?#

We ran Tenancy for Laravel v3.10.1 on a fresh Laravel 12 app: tenant databases and queued jobs stayed isolated, and the cache bootstrapper threw on the default cache store. Tenancy for Laravel is the right default for database-per-tenant Laravel SaaS once the cache store supports tags and the queue points at the central connection.

Tenancy for Laravel is the package published as stancl/tenancy. It makes a Laravel app serve many customers, called tenants, from one install. Our test on 29 September 2026 used PHP 8.2.31, SQLite and two tenants named acme and globex. Then we tried to make one tenant's data show up inside the other, in a request, in a queued job and in the cache.

So the verdict is not a star count. It is three checks on your own app. First, does your cache store support tags? Second, which queue driver do you run? Third, how much of your code talks to the database without Eloquent? When the answers are Redis, any driver with the central connection set, and "a lot", adopt the package. But if they point the other way, read the last three sections before you install anything.

Three checks in order: a cache store that supports tags, the database queue on the central connection, and how much code talks to the database without EloquentThe three checks that decide the verdict: a tag-capable cache store, the queue on the central connection, and how much code bypasses Eloquent. Atyantik test of stancl/tenancy v3.10.1 on Laravel 12.69.3, 29 September 2026.

What does the package save compared with writing tenancy yourself?#

One composer require and one artisan command wrote five new files into our app, led by a 200-line config, and made the database, cache, filesystem and queue tenant-aware together. The package trades five generated files you own and read for five tenant-aware subsystems that a hand-rolled scope would have to rebuild one by one.

Since those lines are yours after the install, you review them like any other code. In our test on 29 September 2026, php artisan tenancy:install created five files and edited composer.json. Also, every later fix goes into that config file.

Show data table
Lines the install added to a fresh Laravel 12 app, per file. Source: Atyantik test, 29 September 2026.
Item Value
config/tenancy.php 200
TenancyServiceProvider.php 148
create_tenants_table migration 37
create_domains_table migration 37
routes/tenant.php 29

The 200-line config is where every later fix goes.

Install footprint Lines the install added to a fresh Laravel 12 app, per file. Source: Atyantik test, 29 September 2026. Atyantik test, 29 September 2026

What those lines buy is the bootstrapper list. The bootstrappers page names five: database, cache, filesystem, queue and Redis. Each one makes a Laravel service tenant-aware. In the page's own words, that happens "in such a way that you don't have to change a line of your code". In practice, a hand-rolled route starts with the database alone. Then the cache, file paths and job payloads each become a separate project, and each one is a place to forget a tenant.

What does Tenancy for Laravel change when a tenant is identified?#

A middleware finds the tenant from the domain, fires TenancyInitialized, and each bootstrapper swaps one Laravel service to the tenant's copy in order. Tenancy is a chain of bootstrappers run on one event, so each subsystem can fail on its own while the others work.

The tenant identification page lists five ways to find the tenant. They are domain, subdomain, domain or subdomain, path, and request data such as a query string or a header. For example, the quickstart uses the domain, through the InitializeTenancyByDomain middleware on the tenant routes.

Once the tenant is known, the bootstrappers run. First, the database bootstrapper "switches the default database connection to tenant", so any model on the default connection now reads the tenant's database. Second, the bootstrappers page says the cache bootstrapper "adds tags with the current tenant's ids to each cache call". Also, the queue bootstrapper writes the tenant's ID into each job payload and starts tenancy again when a worker picks the job up.

A request to a tenant domain passes the identification middleware, fires TenancyInitialized, and five bootstrappers each switch one service: database, cache, filesystem, queue and RedisFrom request to tenant context: identification middleware, TenancyInitialized, then the database, cache, filesystem, queue and Redis bootstrappers. Tenancy for Laravel v3 docs, tenant identification and tenancy bootstrappers pages.

This chain explains every symptom later in the review. Because each bootstrapper only swaps one service, a broken cache does not stop the database from working. As a result, a half-working install can look healthy until the first cache call.

Did tenant isolation hold for the database and for queued jobs?#

Tenant acme saw its 3 rows and globex saw its 1, inside the request and again inside a queued job run later by one worker. Database-per-tenant isolation and queued-job tenant context both held on Laravel 12 defaults with no extra configuration.

Our test on 29 September 2026 gave each tenant its own SQLite file and one notes table, created by a tenant migration. Also, the central database had no notes table at all, so a query that escaped the tenant would have failed loudly. Inside tenant acme, the default connection was named tenant, which is the switch the database bootstrapper promises.

Show data table
Rows each tenant saw, in the request and in a queued job. Source: Atyantik test, 29 September 2026.
Item Value
acme, inside the request 3
globex, inside the request 1
acme, queued job 3
globex, queued job 1

Each tenant saw only its own rows, in the request and in a queued job.

Tenant isolation Rows each tenant saw, in the request and in a queued job. Source: Atyantik test, 29 September 2026. Atyantik test, 29 September 2026

The queue result matters more than the request result. First, we dispatched one job from inside each tenant. Then we ran a single worker later with queue:work --stop-when-empty. Both jobs waited in the central jobs table, and each ran against its own tenant's rows. For example, the acme job counted 3 notes and the globex job counted 1. That matches the queues page, which says jobs "will be stored centrally" and that tenancy starts for the right tenant before the job runs.

Which Laravel 12 defaults break Tenancy for Laravel, and how do you fix them?#

Laravel 12 ships the database cache store, which cannot tag, so the first Cache::put inside a tenant threw a BadMethodCallException in our test. The cache bootstrapper needs a store that supports tags, such as Redis, and the database queue should name the central connection explicitly.

The error text was "This cache store does not support tagging". The bootstrappers page says as much in one line: "you must use a cache store that supports tagging, e.g. Redis." Still, a fresh Laravel 12 app does not ship Redis as its cache, so the quickstart path fails on the first cache write. Then we tried four stores inside tenant acme. The database and file stores failed with the same error, while the array and Redis stores worked.

Cache writes inside a tenant by cache store. Source: Atyantik test of stancl/tenancy v3.10.1 on Laravel 12.69.3, 29 September 2026.

Cache storeCache write inside a tenantWhat to do
database (Laravel 12 default)failed: store does not support taggingmove to Redis, or remove the cache bootstrapper
filefailed: store does not support taggingmove to Redis
arrayworkedfine for tests only, since it forgets on each request
redisworkedthe store to run in production

Source: Atyantik test of stancl/tenancy v3.10.1 on Laravel 12.69.3, 29 September 2026.

The queue fix is a line the docs ask for, not a failure we saw. The queues page covers the database bootstrapper with the database queue driver. In that setup, it says, you must stop jobs landing in the tenant's database. Its fix is to "force the database queue driver to use the central connection" by adding 'connection' => 'central' to the database queue config. In practice, the jobs in our run reached the central table without it. However, that line costs nothing and removes the risk, so we set it.

Try it
Cache store
Queue driver
Isolation model
Works untouched
Needs a line first
Needs a line first

Change these lines before you install

2 of 3 need a line
  • Database bootstrapper: It switches the default database connection to the tenant; each tenant kept its own rows in our test.
  • Cache bootstrapper: The database store failed with "This cache store does not support tagging" in our test. Move the cache to Redis, or remove the cache bootstrapper from config/tenancy.php.
  • Queue bootstrapper: Add 'connection' => 'central' to the database queue config in config/queue.php.

Rules from the Tenancy for Laravel v3 bootstrappers and queues pages; store results from the Atyantik test of stancl/tenancy v3.10.1 on Laravel 12.69.3, 29 September 2026.

Worked example: Laravel 12 defaults, a database per tenant
Database bootstrapperWorks untouched. It switches the default database connection to the tenant; each tenant kept its own rows in our test.
Cache bootstrapperNeeds a line first. The database store failed with "This cache store does not support tagging" in our test. Move the cache to Redis, or remove the cache bootstrapper from config/tenancy.php.
Queue bootstrapperNeeds a line first. Add 'connection' => 'central' to the database queue config in config/queue.php.
Pick your cache store, queue driver and isolation model to see which bootstrappers need a line first. The rules come from the package docs and the store results from our test; modelled, not measured.

How does a hand-rolled tenant_id global scope compare?#

Our closure scope on tenant_id returned acme's 2 rows through Eloquent, and all 3 rows once the same query went through DB::table. A hand-rolled global scope protects Eloquent queries only, so every raw query, report and export is a tenant leak you must find yourself.

This is the route most guides suggest: one shared table, a tenant_id column, and a global scope that adds the filter. The Laravel 12 global scopes docs describe scopes as constraints added to "all queries for a given model". That is the limit, because a scope belongs to the model, and a DB::table call never goes through the model.

Show data table
Rows returned for one tenant under a hand-rolled scope, where 2 of 3 rows belong to that tenant. Source: Atyantik test, 29 September 2026.
Item Value
Eloquent with the global scope 2
DB::table query builder 3
Eloquent withoutGlobalScopes() 3

The scope held for Eloquent and leaked all 3 rows through DB::table.

Hand-rolled scope Rows returned for one tenant under a hand-rolled scope, where 2 of 3 rows belong to that tenant. Source: Atyantik test, 29 September 2026. Atyantik test, 29 September 2026

The package's own single-database page warns of the same gap for its BelongsToTenant trait. It says DB facade calls "of course won't be scoped to the current tenant". So the leak is not a flaw in one scope. Instead, it is a property of every model-level filter. Then there is withoutGlobalScopes(), which any teammate can call to remove the filter on purpose. In our test it returned all 3 rows.

How does stancl/tenancy compare with spatie/laravel-multitenancy?#

Spatie's package deliberately ships only the bare essentials, while stancl/tenancy bootstraps the database, cache, filesystem and queue for you automatically. Choose spatie/laravel-multitenancy when you want to write each tenant switch yourself, and stancl/tenancy when you want the switches written for you.

Spatie's introduction states the philosophy directly: the package "should only provide the bare essentials to enable multitenancy". It finds the current tenant and lets you define what happens when a tenant becomes current. Also, it makes queued jobs tenant-aware and runs artisan commands for each tenant.

Meanwhile, the comparison page that the stancl/tenancy maintainer wrote describes that package as having "the absolute largest amount of out-of-the-box features". Because the author maintains one of the two packages, read it as one side's view. Still, the design split is real. With Spatie, every tenant switch is code you write and test. With stancl/tenancy, the switches arrive written. Your job becomes checking that each one fits your stack, and in ours the cache store did not.

What each package does for you, in each maintainer's own words. Source: Spatie laravel-multitenancy v4 introduction; Tenancy for Laravel v3 bootstrappers and package comparison pages.

spatie/laravel-multitenancystancl/tenancy
Stated aimonly the bare essentials to enable multitenancythe absolute largest amount of out-of-the-box features, in its maintainer's view
Tenant switchescode you write and testbootstrappers switch the database, cache, filesystem and queue for you
Queued jobsmade tenant-awarethe tenant's ID written into each job payload
Your jobwrite each switchcheck that each switch fits your stack

Single database or a database per tenant: which fits your app?#

The package's own docs put it plainly: single-database tenancy has lower devops complexity but larger code complexity, because you scope every model yourself. Pick database per tenant when raw queries or third-party packages touch tenant data, and a single database when a few models and managed hosting dominate.

The single-database page gives the full trade: "lower devops complexity, but larger code complexity than multi-database tenancy". In particular, it adds that you "won't be able to integrate some third-party packages". Our scope test shows the code cost in one number: every raw query returned all 3 rows.

A database per tenant moves the cost to operations. For example, each tenant needs migrations, backups and a database created when it signs up. The package handles creation through its job pipeline, and the multi-database page runs migrations with tenants:migrate. In short, ask where your risk lives. If it lives in code you cannot fully audit, split the databases. If it lives in running many databases, share one and test every query path.

Where your risk lives decides the model: code you cannot fully audit points to a database per tenant, running many databases points to one shared databaseWhere the risk lives decides the model: code you cannot audit points to a database per tenant, running many databases points to one shared database. Tenancy for Laravel v3 single-database tenancy page; Atyantik test, 29 September 2026.

Which pages do you build a tenant-aware Laravel app from?#

Build from five Tenancy for Laravel v3 pages in this order: quickstart, multi-database tenancy, tenant identification, bootstrappers, then queues. The v3 docs in build order are the complete implementation path, and the v4 docs describe the next major line before you pin a version.

  1. Quickstart: the install, the Tenant model and the service provider line.
  2. Multi-database tenancy: how tenant databases are created and migrated.
  3. Tenant identification: the middleware for domain, subdomain, path or request data.
  4. Tenancy bootstrappers: which services switch, and the tag-capable cache rule.
  5. Queues: the central connection line for the database driver.

Then read version 4 and its changelog before you pin a version. The changelog lists higher requirements, "PHP 8.4+, Laravel 12+", along with namespace changes and a new config you must republish. Also, it adds PostgreSQL row-level security, which the v4 page still calls "an experimental feature". Because our PHP 8.2 app could not meet v4's floor, our test ran v3.10.1, the release of 5 August 2026 on the releases page.

What is the smallest setup that passed our test?#

Five steps gave us a working database-per-tenant app: require the package, run tenancy:install, extend the Tenant model, register the provider, and fix two config lines. The minimum is five steps in PHP and config, and two of them are the cache and queue fixes the defaults need.

php
<?php
// Step 1, in the shell: composer require stancl/tenancy
// Step 2, in the shell: php artisan tenancy:install, then php artisan migrate

// Step 3, app/Models/Tenant.php
namespace App\Models;

use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;

class Tenant extends BaseTenant implements TenantWithDatabase
{
    use HasDatabase, HasDomains;
}

// Then in config/tenancy.php:
// 'tenant_model' => \App\Models\Tenant::class,

// Step 4, bootstrap/providers.php
return [
    App\Providers\AppServiceProvider::class,
    App\Providers\TenancyServiceProvider::class,
];

// Step 5a, config/queue.php, in connections.database, name the central connection.
// 'connection' => 'central',
// Step 5b. Pick a cache store that supports tags, such as redis.

Steps 1 to 4 come from the quickstart. Step 5 comes from the queues page and the cache rule on the bootstrappers page. After that, create a tenant and a domain in tinker, the way the quickstart shows, and move your tenant migrations into database/migrations/tenant.

When not to use Tenancy for Laravel, and what fits instead?#

Skip the package when you serve one customer per deployment, or when a shared table with a few models and no raw queries is the whole tenancy need. For one tenant per deployment use separate deployments, and for minimal manual tenancy use spatie/laravel-multitenancy or a tested global scope.

First, if each customer already gets its own server or container, you have tenancy by deployment. So a package adds a layer with nothing to separate. Second, if the need is only to scope records such as todo tasks to the current user, the package's own introduction says there is "no need to use a multi-tenancy package". Instead, a relation such as auth()->user()->tasks() covers it.

Third, if you want tenancy but want to write each switch yourself, use spatie/laravel-multitenancy. Its stated aim is "the bare essentials", which suits a team that prefers less generated code. Also, a plain global scope works for a few models. It needs a test that runs every raw query path under two tenants. Our 3-row result is what that test should catch.

When the package is the wrong tool, and what fits instead. Source: Tenancy for Laravel v3 introduction; Spatie laravel-multitenancy v4 introduction.

Your situationWhat fits instead
Each customer gets its own server or containerSeparate deployments, with no package
Scoping records such as todo tasks to the current userA relation such as auth()->user()->tasks()
Tenancy with each switch written yourselfspatie/laravel-multitenancy
A few models and no raw queriesA global scope, with a test of every raw query path under two tenants

Where should you go next with Laravel multi tenancy?#

For the rest of the SaaS stack read Laravel for SaaS, and for isolation as a product risk read our Laravel SaaS development challenges post. The next reads are the SaaS package map, the SaaS challenges, and queue reliability, with SaaS development and Laravel hiring as routes for teams.

The Laravel for SaaS guide maps billing, plan access, queues and API tokens to their packages. Also, the queues at scale guide covers the worker side of tenant-aware jobs. For the admin screens of the same app, see which Filament version and PHP branch a Laravel admin panel should run on.

For a team that wants a multi-tenant product built, there is SaaS development. For a team adding tenancy to an app it already runs, there is hiring Laravel developers. Still, the five v3 pages above and the two config lines are enough to build it yourself.

Questions about Tenancy for Laravel

Is Tenancy for Laravel still worth using in 2026?
Yes, for database-per-tenant Laravel apps, as long as the cache store can tag and the queue uses the central connection. Version 3.10.1 shipped on 5 August 2026 and passed our isolation test on Laravel 12. However, version 4 needs PHP 8.4 or later.
Why does Tenancy for Laravel throw 'This cache store does not support tagging'?
The cache bootstrapper adds a tenant tag to every cache call, and Laravel 12's default database store cannot hold tags. The file store fails the same way. Therefore, move the cache to Redis, or take the cache bootstrapper out of config/tenancy.php.
Is a tenant_id global scope enough for Laravel multi tenancy?
A global scope is enough only when every query goes through Eloquent models. In our test, the same query through DB::table returned all 3 rows instead of the tenant's 2. So any raw query, report or export needs its own tenant filter and its own test.

Keep reading