Skip to content

Full v4 changelog

Version 4 brings many new features, with the most important ones being:

  • PostgreSQL RLS
  • Jetstream/Fortify support
  • Early identification and a general overhaul of routing logic
  • Resource syncing refactor
  • New tenancy bootstrappers
  • Pending tenants
  • Parallel migrations and other commands

If you’re upgrading from v3, the main changes that will affect you are:

  • Higher requirements: PHP 8.4+, Laravel 12+, and MySQL 8+ if you use MySQL
  • Namespace changes (mostly database-related things being moved to a dedicated namespace). These are easily resolved with any modern IDE
  • New config structure. You will have to delete and republish it (and then re-apply your changes, if any). It’s recommended to also do this with the TenancyServiceProvider
  • Several static properties being removed and replaced with config keys
  • Changes to routing logic (if you’re using universal routes)
  • The TenantConfig feature is deprecated in favor of TenantConfigBootstrapper

Diff: d0de09a...master

  • Tenant schema dump (tenants:dump) command 7d98ebb #807 a1a976c #985 cb7567a #1015 ea19117 #1019 f851288
  • Add Microsoft SQL Server support cc6d4fe #715
  • Add ScoutPrefixBootstrapper f83504a #936
  • Set default value for tenant URL parameters when a tenant is identified by path f2c6408 #925
  • Make tenants:run handle stdin prompts from subcommands 409190f #923 0e223e0
  • Add optional DeleteTenantStorage listener ab5fa7a #938 aa6bfb4, both it and CreateTenantStorage now accept any TenantEvent #1410
    • Later deprecated in favor of the Jobs\DeleteTenantStorage job #1446, since a job pipeline is a better fit for this logic than a listener, and CreateTenantStorage became redundant once FilesystemTenancyBootstrapper started creating the directories
  • Add BatchTenancyBootstrapper b78320b #874 (later deprecated eff4155 and removed d8af9b4)
  • Add Storage::url() support 7bacc50 #909 a7ad828 222bbe4 8cd15db
  • Add tenant-specific maintenance mode 121370e #761, e1b8658 (fixes bug #1404), add events 3f60c4a #979
  • Add current() and currentOrFail() tenant methods 42dab29 #970
  • Add option for dropping tenant databases on migrate:fresh 080b271 #971 9520cbc #1007
  • Add cookie option on InitializeTenancyByRequestData 05f1b2d #980 15d12e2
  • Add pending tenants 198f34f #869 7d3298c #1025 6b74589 85bdbd5
    • Refactored in 6b0066c (BC break: pullPendingFromPool() now defaults to $firstOrCreate = false, pullPending() is a direct alias for it with $firstOrCreate = true, both accept $attributes, and createPending() sets pending_since even if an exception is thrown)
    • Add getPendingAttributes() for providing default values for custom non-nullable columns when createPending() is called without them (e.g. a dummy slug that gets overwritten when the tenant is pulled) 6523f24
    • PendingScope cleanup d274d8c
    • Fix chaining withoutPending() with where() (the two pending conditions weren’t grouped) #1457
    • Pending tenants are now created with pending_since already set (minor BC break, they used to be marked as pending only after the creation pipeline finished, so e.g. DatabaseCreated listeners saw them as non-pending; CreatingPendingTenant/PendingTenantCreated are now fired from the model’s creating/created hooks), MigrateDatabase/SeedDatabase pass --with-pending (opt out using their $includePending property) and --with-pending now accepts a value which overrides the tenancy.pending.include_in_queries config #1458
    • Fix a remaining race condition when two processes pull at the same time (pending_since is now cleared using a conditional update whose affected row count decides who claimed the tenant, retrying with the next candidate otherwise) — minor BC break: clearing pending_since no longer fires model events, and PullingPendingTenant now fires before the claim (outside the transaction), so it may fire more than once and no longer guarantees the tenant will be pulled; use PendingTenantPulled for that #1463
  • Add DatabaseSessionBootstrapper (adds support for database session driver) 5849089
  • Add session state when impersonating tenant f42f08c #1029, later changed in #1437
  • Add a dedicated feature for tenant-specific mail credentials 0f892f1 #989 ea5a746
  • Add skip-failing option to tenants:migrate command 342c67f #945
  • Allow defining the tenant connection template using array syntax 087733d #1040
  • Add RootUrlBootstrapper (changes APP_URL in CLI context, used e.g. when sending emails in queued jobs that should have links to the tenant’s domain), also UrlGeneratorBootstrapper 617e9a7 #1044 fbdb13f #1068 719b1be #1079 2c0f615 5d3b3d3 cecf07a
    • Fix URL::temporarySignedRoute() not respecting the bypass parameter #1438
    • Override toRoute() too (needed for Livewire v4, which uses it instead of the route() helper) and remove the now-redundant temporarySignedRoute() override #1439
    • Make TenancyUrlGenerator inherit the original UrlGenerator’s scheme so https app URLs no longer become http in tenant context #1390
  • Add BroadcastingConfigBootstrapper (makes multi-tenant broadcasting/websockets possible using tenant-specific keys) d7a4982 #1027, Reverb: 590360b, rewritten in #1448
  • Add BroadcastChannelPrefixBootstrapper (makes broadcasting/websockets tenant-aware by prefixing channel names) c34952f ca400b5 00a00a2, Reverb: 252e337, helper change: a37dc9b
  • Add cache prefix mode for separating tenant caches (introduces tenancy support for any cache driver, no longer only Redis) bd9bbe8 #1014
  • Support defining domains for tenants using a domain string column instead of a HasMany relation e25e7b7 8b0bdd4
  • Add InitializeTenancyByOriginHeader 9e4f33e 56dd411 91295f0
  • Add permission-controlled MSSQL database manager cf3d06c
  • Add the option to set headers and middleware in TenantAssetController a39da04 cc2d555 fdd401f
  • Add more tenant key generators dc43066 55b1c53 25360f6 8d87ee9 b967d16
  • Make tenant column used by path identification parameters configurable, general resolver refactor 0c11f29
  • Add tenant:tinker command 6f4b9da 489fbb9
  • Add Tenancy::$findWith ac5948d
  • PostgreSQL RLS 7317d26 05b602e 588d1fc 2057e1e, support table constraints (+ refactor for clarity and performance using dynamic programming) #1354, fix dropRLSPolicies() #1413, tenants:rls fixes (--force is now a plain flag, $forceRls = false un-forces tables when their policy is recreated, RLS is disabled on tables that stopped being scoped and have no policies, policy lookups are limited to the current schema) #1494
  • Parallel migrations and other commands 1b0e7d0 39bcbda f017b65 075789e 303b52d c6ba62b
  • SQLite: support named in-memory DBs 48b916e 99b79a5 aba7a50, stricter in-memory database detection #1459
  • SQLite: add DisallowSqliteAttach feature a88a42f, it requires a 64-bit system (we don’t build 32-bit extensions) and silently does nothing on unsupported platforms instead of throwing 13a2209 a0a9b85, on PHP 8.5+ it uses the native PDO::setAuthorizer() instead of loading a compiled extension fadf100
  • Impersonation tokens: support custom models 95dd906
  • Support database cache store tenancy ecc3374
    • Switch the stores’ connections using setConnection() instead of purging them #1408
    • Make globalCache always use the central connection for database cache stores #1462
  • Add --force option to tenants:migrate-fresh #1391
  • Use --database in tenants:migrate as the template connection #1386
  • Add tenants:purge-impersonation-tokens command (deletes impersonation tokens older than UserImpersonation::$ttl) #1387 (resolves #1348)
  • Support Laravel’s scoped disks in FilesystemTenancyBootstrapper #1402 (resolves #1401)
  • Add Tenancy::reinitialize() #1449
  • Add a --skip-tenants option to all tenant commands (opposite of --tenants) #1436
  • Add DatabaseTenancyBootstrapper::$harden (opt-in, throws a RuntimeException if a tenant’s connection ends up pointing at the central database or another tenant’s database for whatever reason) #1459
  • Add LogChannelBootstrapper #1381
  • Early identification (old version: ff46bcf, new version: 1d0ca27), related: 4953c69 e4df597 df9324b c312156 80b1183 4e51cdb #1362 #1371, remove a redundant universal route check from PreventAccessFromUnwantedDomains #1427
  • Add route cloning (1d0ca27, see above):
    • Change the URI prefix order and move cloning to app->booted() b789f5c
    • Trim trailing / from route prefixes 0f7cd2e
    • Refactor — add cloneRoutesWithMiddleware() and support cloning routes by name #1353
    • Add addTenantParameter(bool) and domain(string|null) b2f2669 #1374
    • Add the tenant parameter before any existing route prefix (configurable using tenantParameterBeforePrefix()) #1393
    • Add addTenantMiddleware() for specifying the identification middleware used on cloned routes (previously it had to be “inherited” from the central route, which meant marking that route as universal even when it wasn’t meant to be usable in tenant context), remove all route context middleware flags (central, tenant, universal) from cloned routes, add cloneRoutes() and a static make() #1416
  • Respect user-defined $onFail in the Universal Routes feature 233a122 #679
  • Change how tenants:run is used with arguments a45aa8e #912 #686
  • Stronger typing, minor changes to the Tenant interface 55d0a9a 8af354c 87212e5 d463e2d a94227a f98a901 942d79c
  • Improved namespacing, all database-related logic was moved to namespaces under Stancl\Tenancy\Database 40bf28c
  • Refactor TenantDatabaseManagers d2e1ce0 0fc1054
  • Fix/improve Ignition solutions 55d0a9a 3542b3f
  • Reverse order of tenancy bootstrappers when reverting to central context 62d19c5 #932
  • Make impersonation tokens require stateful guards 3bf2c39 #935
  • Remove configurable static property from BelongsToTenant trait (deprecated in PHP 8.1), replace it with a config key 24146b2 e5bc8dd
  • Refactor more static properties to config keys ccaba05
  • Improve CLI command outputs using Laravel 9+ components e4f5b92 #968 5d688e6 68de360 #1030
  • Change TenantDatabaseManager-related interfaces, add public database(): Connection method fe0a322
  • Make tenants:migrate default to configured schema path a1a976c #985
  • Use a dedicated DB connection for creating/deleting tenant databases bf504f4 #946 (fixes bug #515)
  • Improve resource syncing logic 77c5ae1 #915 (fixes bug #658) 22d1b20 #993 ea3e445 #992, 758fbc8 #997 (adds polymorphic table for mapping resources to tenants) 6784685 77226cf, add the SyncedResourceDeleted event (now also fired for tenant resources, not just SyncMasters, and on deleted rather than deleting) and the DeleteResourceMapping listener which deletes pivot records when a synced resource is deleted, add an optional DeleteAllTenantMappings listener for cleaning up pivot records of deleted tenants (intended for pivot tables without cascading foreign keys), support morph maps in pivots, rename PivotWithRelation to PivotWithCentralResource (minor BC break, the interface now only returns the central resource class), move global ID generation to an overridable generateGlobalIdentifierKey() method #1411, fix attach listener event #1469
  • Centralize config used by BelongsToTenant and HasDomains, replace all tenant_id literals dd0f03f 2a39b05 (fix #998), 82fa6cb
  • Convert publishable migrations to anonymous classes fb2369d #1001
  • Manual mode improvements (use correct event type, add new listeners) 73c5655 #1013
  • Add remember bool column to ImpersonationToken 228c267 #1101
  • Minor migrate:rollback fixes b503dbf
  • Fix PathTenantResolver when resolving tenant from cache 8db27a3, related aa1437f
  • Require MySQL 8 fd6070f
  • Disable asset_helper_tenancy / asset_helper_override by default, make the controller more configurable a39da04, related (rename + misc changes): 657e165
  • Simplify Tenant contract, move run() logic to Tenancy, add generics af3b693
  • Simplify TenantWithDatabase contract, move tenantConfig() logic e8c3c75 29d1469
  • Drop Laravel 10 support ce8e74e
  • Add $ignoreExisting static property to CreateDatabase bf1ba69
  • Filesystem logic refactor a41ad69
  • Cache prefixing logic rewrite, session scoping improvements eecf6f2, also ScopeSessions improvement dc90e60, CacheTenancyBootstrapper now always throws when tenancy.cache.scope_sessions is enabled but the session driver isn’t cache-driven (redis, memcached, dynamodb, apc) and the config default is derived from SESSION_DRIVER so it’s only enabled with supported drivers 74634df 6049ade aba7a50 #1412
  • Support non-interactive tenancy:install 8b41ea3, star the repository using the gh CLI when it’s available 3846fe8
  • SQLite: use .sqlite suffix if no suffix is specified 836a258
  • SQLite: use WAL mode for newly created DBs by default (configurable) 9ee1d63 (later reverted 13a2209 — the $WAL static property is gone, Laravel’s own journal_mode config is respected instead)
  • Consume PendingDispatch return values in Tenancy::run() (see #1277) 3c183e4 b9cc63f ffad2db
  • Queue logic refactor 8f958d5 #1289: QueueTenancyBootstrapper now always initializes tenancy for the job being processed and reverts to the previous context afterwards, so the $forceRefresh static property is gone — changes to a tenant’s state are always picked up by the next job. The previous behavior, staying in the tenant’s context after the job, is available in the new PersistentQueueTenancyBootstrapper; it’s needed when a JobProcessed listener has to run in the tenant context (e.g. Telescope’s JobWatcher, which unserializes the job and would fail on models bound to the tenant connection), but requires more careful use, which is why it isn’t the default anymore
  • runForMultiple() now doesn’t execute when passed an empty array 30ee4e9
  • URL generation, request data identification improvements (both added in v4, see previous points) #1357
  • Only revert initialized bootstrappers #1385
  • Use globalCache in CachedTenantResolver 3984d64
  • Fix ViteBundler not affecting Vite static calls #1389
  • Features refactor: features are now always bootstrapped (in TenancyServiceProvider::boot(), rather than when Tenancy is resolved from the container), Feature::bootstrap() no longer takes a Tenancy instance, added Tenancy::bootstrapFeatures() for bootstrapping features enabled at runtime 4578c9e (fixes bug #949)
  • SQLite: delete -journal, -wal and -shm files along with the tenant database, handle a trailing directory separator in SQLiteDatabaseManager::$path 13a2209 a0a9b85, SQLiteDatabaseManager::makeConnectionConfig() now properly respects the static $path property #1459
  • Replace the TenantConfig feature with TenantConfigBootstrapper (bootstrappers have better error handling and safe reverting; the feature was effectively a bootstrapper listening to the TenancyBootstrapped/RevertedToCentralContext events), the feature is now deprecated b320f8f 36153a9
  • DatabaseConfig refactor: clarify the role of the tenant host connection, rename connectionDriverManager() to managerForDriver() a0a9b85
  • Delete expired or invalid impersonation tokens instead of only aborting with a 403 #1387
  • Create the tenant’s framework/cache directory in FilesystemTenancyBootstrapper (the CreateTenantStorage listener only runs when a tenant is created, but in multi-server setups the directory may need to be created whenever a tenant is used, so the listener is generally no longer needed), also use 0750 instead of 0777/0755 permissions for created storage directories #1410
  • Skip tenant database deletion when the tenant’s create_database internal attribute is false (it was likely created without a database; opt out using DeleteDatabase::$skipWhenCreateDatabaseIsFalse), add DeleteDatabase::$ignoreFailures for continuing the delete pipeline when deletion fails — DatabaseDeleted is now only fired when the database was actually deleted #1394
  • Fix DomainTenantResolver::isSubdomain() treating e.g. tenantsite.com as a subdomain of the central domain site.com (it now requires an actual dot-separated prefix) #1425
  • Validate parameters in tenant database manager statements — bindings are used where possible, otherwise the value is checked against an allowlist (validateParameter()/validatePassword()) — and only include charset/collation in the MySQL CREATE DATABASE statement when they aren’t null (null previously caused a QueryException) #1459
  • Show the underlying tenants:migrate output in tenants:migrate-fresh when it’s run verbosely (-v), it was previously always silenced #1464