Skip to content

Configuration

Our package is configured in three ways:

  1. The configuration file (config/tenancy.php)
  2. Static properties on various classes
  3. Event listeners: covered separately on the Event system page.

The configuration file is thoroughly annotated and comes with good defaults for multi-database applications.

Individual pages of this documentation cover the main parts of the configuration as well as common changes.

You can find an online version of the configuration file here.

Many classes in our package are configurable via static properties. These are used for more niche use cases that would clutter up our config, but should still be configurable.

We want the package to be as configurable as possible, and setting public static properties is much more ergonomic than having to extend classes and replace container bindings.

There are too many static properties to fully document them all here, however an auto-generated (though possibly outdated) index is included below. It only includes the properties and their docblocks, so if you need more context than the name of the property or the docblock, it is recommended to simply take a look at the class to see how the property is being used.

That’s the best way to discover these static properties anyway. Whenever you’re using a some class from the package directly and wish to customize something, there’s a good chance there’s a static property for configuring what you need. Most classes are not too complicated on their own, so source diving shouldn’t be difficult.

As of the time of writing, there are 45 configurable static properties in the codebase. Some are documented on specific pages of the documentation.

To change static properties, use this syntax in any service provider (it’s recommended to use the TenancyServiceProvider to keep all tenancy-related logic in one place):

src/Providers/TenancyServiceProvider.php
public function boot()
{
ClassName::$propertyName = 'value';
// ...
}
src/Actions/RemoveStorageSymlinksAction.php
/**
* Should the directories created for nested symlinks be removed along with the symlink.
*
* Before enabling this, make sure you understand the removeLink() method
* and the high stakes of recursively removing parent directories (even if the logic should be sound).
*
* @see CreateStorageSymlinksAction
*/
public static bool $removeNestedDirectories = false;
src/Bootstrappers/CacheTagsBootstrapper.php
public static string $cacheManagerWithTags = \Stancl\Tenancy\Overrides\CacheManager::class;
src/Bootstrappers/BroadcastChannelPrefixBootstrapper.php
/**
* Closures overriding broadcasters with custom broadcasters that prefix the channel names with the tenant keys.
*
* The key is the broadcaster's name, and the value is a closure that should prefix the broadcaster's channels.
* $broadcasterOverrides['custom'] = fn () => ...; // Custom override closure
*
* For more info, see the default override methods in this class (pusher() and ably()).
*/
public static array $broadcasterOverrides = [];
src/Bootstrappers/DatabaseTenancyBootstrapper.php
/**
* When true, throw an exception if a tenant gets connected to
* another tenant's database or to the central database.
*
* This case should never come up in well-configured apps where
* users cannot set or edit tenant IDs or database names, so this
* option is disabled by default.
*
* However, applications dealing with extremely sensitive data may
* choose to enable this runtime check to prevent a bug or misconfiguration
* from creating an exploit that would let an attacker access another
* tenant's data or data from the central database.
*
* One way such a scenario might come up is if an application allows
* broad tenant attribute updates on a page for updating some fields
* on the tenant, without restricting that action to only a limited
* set of fields that are safe to edit. An attacker might be able to add
* something like ['tenancy_db_name' => '...'] to the request which could
* lead to this internal attribute being updated on an existing tenant.
*
* It's possible that enabling this setting will negate the performance
* benefits of cached tenant lookup.
*/
public static bool $harden = false;
src/Bootstrappers/DatabaseCacheBootstrapper.php
/**
* Cache stores to scope.
*
* If null, all cache stores that use the database driver will be scoped.
* If an array, only the specified stores will be scoped. These all must use the database driver.
*/
public static array|null $stores = null;
/**
* Should scoped stores be adjusted on the global cache manager to use the central connection.
*
* You may want to set this to false if you don't use the built-in global cache and instead provide
* a list of stores to scope (static::$stores), with your own global store excluded that you then
* use manually. But in such a scenario you likely wouldn't be using global cache at all which means
* the callbacks for adjusting it wouldn't be executed in the first place.
*/
public static bool $adjustGlobalCacheManager = true;
src/Bootstrappers/BroadcastingConfigBootstrapper.php
/**
* Tenant properties to be mapped to config (similarly to the TenantConfigBootstrapper).
*
* For example:
* [
* 'config.key.name' => 'tenant_property',
* ]
*
* $tenant->tenant_property will be mapped to config('config.key.name') when tenancy is initialized.
*/
public static array $credentialsMap = [];
public static array $mapPresets = [/* ... */];
src/Bootstrappers/MailConfigBootstrapper.php
/**
* Tenant properties to be mapped to config (similarly to the TenantConfigBootstrapper).
*
* For example:
* [
* 'config.key.username' => 'tenant_property',
* 'config.key.password' => 'nested.tenant_property',
* ]
*/
public static array $credentialsMap = [];
public static string|null $mailer = null;
public static array $mapPresets = [/* ... */];
src/Bootstrappers/Integrations/FortifyRouteBootstrapper.php
/**
* Fortify redirects that should be used in tenant context.
*
* Syntax: ['redirect_name' => 'tenant_route_name']
*/
public static array $fortifyRedirectMap = [];
/**
* Should the tenant parameter be passed to fortify routes in the tenant context.
*
* This should be enabled with path/query string identification and disabled with domain identification.
*
* You may also disable this when using path/query string identification if passing the tenant parameter
* is handled in another way (TenancyUrlGenerator::$passTenantParameter for both,
* UrlGeneratorBootstrapper:$addTenantParameterToDefaults for path identification).
*/
public static bool $passTenantParameter = false;
/**
* Tenant route that serves as Fortify's home (e.g. a tenant dashboard route).
* This route will always receive the tenant parameter.
*/
public static string|null $fortifyHome = 'tenant.dashboard';
/**
* Follow the query_parameter config instead of the tenant_parameter_name (path identification) config.
*
* This only has an effect when:
* - $passTenantParameter is enabled, and
* - the tenant_parameter_name config for the path resolver differs from the query_parameter config for the request data resolver.
*
* In such a case, instead of adding ['tenant' => '...'] to the route parameters (or whatever your tenant_parameter_name is if not 'tenant'),
* the query_parameter will be passed instead, e.g. ['team' => '...'] if your query_parameter config is 'team'.
*
* This is enabled by default because typically you will not need $passTenantParameter with path identification.
* UrlGeneratorBootstrapper::$addTenantParameterToDefaults is recommended instead when using path identification.
*
* On the other hand, when using request data identification (specifically query string) you WILL need to
* pass the parameter therefore you would use $passTenantParameter.
*/
public static bool $passQueryParameter = true;
src/Bootstrappers/RootUrlBootstrapper.php
/**
* A closure that accepts the tenant and the original root URL and returns the new root URL.
* When null, the root URL is not altered in any way.
*
* We recommend setting this property in the TenancyServiceProvider's overrideUrlInTenantContext() method.
*/
public static Closure|null $rootUrlOverride = null;
/**
* Overriding the root url may cause issues in *some* tests, so you can disable
* the behavior by setting this property to false.
*/
public static bool $rootUrlOverrideInTests = true;
src/Bootstrappers/CacheTenancyBootstrapper.php
/** @var Closure(Tenant, string): string */
public static Closure|null $prefixGenerator = null;
src/Bootstrappers/TenantConfigBootstrapper.php
/** @var array<string, string|array> */
public static array $storageToConfigMap = [/* ... */];
src/Bootstrappers/UrlGeneratorBootstrapper.php
/**
* Should the tenant route parameter get added to TenancyUrlGenerator::defaults().
*
* This is recommended when using path identification since defaults() generally has better support in integrations,
* namely Ziggy, compared to TenancyUrlGenerator::$passTenantParameterToRoutes.
*
* With query string identification, this has no effect since URL::defaults() only works for route paramaters.
*/
public static bool $addTenantParameterToDefaults = true;
src/Bootstrappers/LogChannelBootstrapper.php
/**
* Logging channels whose path is built using storage_path() (e.g. Laravel's 'single' and 'daily').
*
* Channels included here will be configured to use tenant-specific storage paths.
*
* Overrides in the $channelOverrides property take precedence over
* $storagePathChannels when a channel is included in both.
*/
public static array $storagePathChannels = ['single', 'daily'];
/**
* Custom channel configuration overrides.
*
* Channels included here will be configured using the provided override.
* The overrides take precedence over the $storagePathChannels behavior
* when both approaches are used for the same channel.
*
* You can either map tenant attributes to channel config keys using an array,
* or provide a closure that returns the full channel config array.
*
* Examples:
* - Array mapping: ['slack' => ['url' => 'webhookUrl']]
* - this maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is null, the override is ignored)
* - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])]
* - this manually merges ['url' => $tenant->slackUrl] into the channel's config
* - null is not ignored, the closure controls the override fully
*
* So the channel overrides can be arrays and closures that return arrays.
*/
public static array $channelOverrides = [];
src/Commands/CreateUserWithRLSPolicies.php
/**
* Force, rather than just enable, the created RLS policies.
*
* By default, table owners bypass RLS policies. When this is enabled,
* they also need the BYPASSRLS permission. If your setup lets you create
* a user with BYPASSRLS, you may prefer leaving this on for additional
* safety. Otherwise, if you can't use BYPASSRLS, you can set this to false
* and depend on the behavior of table owners bypassing RLS automatically.
*
* This setting generally doesn't affect behavior at all with "default"
* setups, however if you have a more custom setup, with additional users
* involved (e.g. central connection user not being the same user that
* creates tables, or the created "RLS user" creating some tables) you
* should take care with how you configure this.
*
* Tables with existing policies are only updated when their policies get
* recreated, so after changing this, run `tenants:rls --force`.
*/
public static bool $forceRls = true;
/**
* Disable RLS on tables that have RLS enabled but no policies, and that
* shouldn't have an RLS policy based on the current RLS policy manager.
*
* A table with RLS enabled and no policies denies all rows to the RLS user.
* This happens e.g. when a table stops being scoped and its policy has been
* dropped in a migration using dropRLSPolicies(), or as a zombie policy.
*
* Set this to false if you have tables that intentionally have RLS enabled
* without any policies (to block the RLS user from accessing them).
*/
public static bool $disableRlsOnTablesWithoutPolicies = true;
src/Controllers/TenantAssetController.php
/**
* Used for adding custom headers to the response.
*
* @var (Closure(Request): array)|array
*/
public static Closure|array $headers = [];
/**
* Additional middleware to be used on the route to this controller.
*
* @var array<string>
*/
public static array $middleware = [];
/**
* Disk the assets are served from.
*
* When null, the assets are served from app/public inside the tenant's storage directory.
*
* The disk has to be local, since the assets are read from the filesystem. Disks using the
* 'scoped' driver are supported as long as the disk they're based on uses the 'local' driver.
*
* The disk also has to be listed in tenancy.filesystem.disks -- for scoped disks, it's the
* disk they're based on that has to be listed there (since a scoped disk inherits its root).
*/
public static string|null $publicDisk = null;
src/Database/Concerns/HasPending.php
public static string $pendingSinceCast = 'timestamp';
src/Database/Concerns/ValidatesDatabaseParameters.php
/**
* Characters allowed in parameters.
*
* Used as the default allowlist in validateParameter(), which validates non-password
* parameters such as database names or usernames.
*
* Since non-password parameters don't need to use as many special characters, we use
* a stricter allowlist here.
*/
public static string $allowedParameterCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
/**
* Characters allowed in database user passwords.
*
* The allowlist for passwords is less strict than for other parameters
* because it's more common to use more special characters in passwords.
*/
public static string $allowedPasswordCharacters = ' !#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~';
src/Database/Models/ImpersonationToken.php
/** You can set this property to customize the table name */
public static string $tableName = 'tenant_user_impersonation_tokens';
src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
/** @var string[] */
public static array $grants = [/* ... */];
src/Database/TenantDatabaseManagers/PermissionControlledMicrosoftSQLServerDatabaseManager.php
/** @var string[] */
public static array $grants = [/* ... */];
src/Database/DatabaseConfig.php
/**
* Database username generator (can be set by the developer.).
*
* @var Closure(Model&Tenant, self): string
*/
public static Closure $usernameGenerator;
/**
* Database password generator (can be set by the developer.).
*
* @var Closure(Model&Tenant, self): string
*/
public static Closure $passwordGenerator;
/**
* Database name generator (can be set by the developer.).
*
* @var Closure(Model&Tenant, self): string
*/
public static Closure $databaseNameGenerator;
src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php
/**
* SQLite database directory path.
*
* Defaults to database_path().
*/
public static string|null $path = null;
/**
* Characters allowed in database names.
*
* Includes dots to support file extensions (e.g. '.sqlite').
*/
public static string $allowedDatabaseNameCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.';
src/Features/TenantConfig.php
/** @var array<string, string|array> */
public static array $storageToConfigMap = [/* ... */];
src/Jobs/CreateDatabase.php
public static bool $ignoreExisting = false;
src/Features/DisallowSqliteAttach.php
public static string|false|null $extensionPath = null;
src/Jobs/MigrateDatabase.php
/**
* Should pending tenants be included while migrating,
* regardless of the tenancy.pending.include_in_queries config value.
*
* If false, pending tenants will be specifically excluded.
*
* If null, default to tenancy.pending.include_in_queries config.
*/
public static ?bool $includePending = true;
src/Jobs/DeleteDatabase.php
/** Skip database deletion if the create_database internal attribute is false. */
public static bool $skipWhenCreateDatabaseIsFalse = true;
/** Ignore exceptions thrown during database deletion and continue execution. */
public static bool $ignoreFailures = false;
src/Jobs/SeedDatabase.php
/**
* Should pending tenants be included while seeding,
* regardless of the tenancy.pending.include_in_queries config value.
*
* If false, pending tenants will be specifically excluded.
*
* If null, default to tenancy.pending.include_in_queries config.
*/
public static ?bool $includePending = true;
src/Features/UserImpersonation.php
/** The lifespan of impersonation tokens (in seconds). */
public static int $ttl = 60;
src/Listeners/QueueableListener.php
public static bool $shouldQueue = false;
src/Middleware/IdentificationMiddleware.php
public static ?Closure $onFail = null;
/**
* Should Tenancy be ended before returning the response.
*
* This can be useful in tests if it's unergonomic that the test stays in the
* tenant context after making a request to a tenant route.
*/
public static bool $terminateTenancy = false;
src/Middleware/InitializeTenancyByDomain.php
public static ?Closure $onFail = null;
src/Middleware/InitializeTenancyByPath.php
public static ?Closure $onFail = null;
src/Middleware/ScopeSessions.php
public static string $tenantIdKey = '_tenant_id';
/** @var Closure(Request): mixed */
public static Closure|null $onFail = null;
src/Middleware/PreventAccessFromUnwantedDomains.php
/**
* Set this property if you want to customize the on-fail behavior.
*/
public static ?Closure $abortRequest;
src/Middleware/InitializeTenancyBySubdomain.php
/**
* The index of the subdomain fragment in the hostname
* split by `.`. 0 for first fragment, 1 if you prefix
* your subdomain fragments with `www`.
*
* @var int
*/
public static $subdomainIndex = 0;
public static ?Closure $onFail = null;
src/Middleware/InitializeTenancyByRequestData.php
public static ?Closure $onFail = null;
public static bool $requireCookieEncryption = false;
src/Resolvers/RequestDataTenantResolver.php
public static bool $shouldCache = false;
public static int $cacheTTL = 3600; // seconds
public static string|null $cacheStore = null; // default
src/ResourceSyncing/Listeners/CreateTenantResource.php
public static bool $shouldQueue = false;
src/Resolvers/DomainTenantResolver.php
/** The model representing the domain that the tenant was identified on. */
public static Domain|null $currentDomain = null;
src/ResourceSyncing/Listeners/DeleteResourceInTenant.php
public static bool $shouldQueue = false;
src/ResourceSyncing/Listeners/DeleteAllTenantMappings.php
public static bool $shouldQueue = false;
/**
* Pivot tables to clean up after a tenant is deleted, in the
* ['table_name' => 'tenant_key_column'] format.
*
* Since we cannot automatically detect which pivot tables
* are being used, they have to be specified here manually.
*
* The default value follows the polymorphic table used by default.
*/
public static array $pivotTables = ['tenant_resources' => 'tenant_id'];
src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php
public static bool $shouldQueue = false;
src/ResourceSyncing/Listeners/DeleteResourceMapping.php
public static bool $shouldQueue = false;
src/RLS/PolicyManagers/TraitRLSManager.php
/** @var Closure(): array<Model> */
public static Closure|null $modelDiscoveryOverride = null;
/**
* Directories in which the manager will discover your models.
* Subdirectories of the specified directories are also scanned.
*
* For example, specifying 'app/Models' will discover all models in the 'app/Models' directory and all of its subdirectories.
* Specifying 'app/Models/*' will discover all models in the subdirectories of 'app/Models' (+ their subdirectories),
* but not the models present directly in the 'app/Models' directory.
*/
public static array $modelDirectories = ['app/Models'];
/**
* Scope queries of all tenant models only using RLS (without the global scopes).
*
* This only disables the global scopes. If the manager discovers
* a given model and it's not excluded, RLS is always applied
* regardless of whether the global scopes are applied.
*
* To use RLS-only scoping only for some models, you can keep this disabled and
* make the models of your choice implement the RLSModel interface.
*/
public static bool $implicitRLS = false;
/**
* Models excluded from discovery. These don't get RLS policies,
* so the single-database traits always apply their global scopes
* on them, even with implicit RLS or when they implement RLSModel.
*
* @var array<class-string<Model>>
*/
public static array $excludedModels = [];
src/ResourceSyncing/Listeners/RestoreResourcesInTenants.php
public static bool $shouldQueue = false;
src/Overrides/TenancyUrlGenerator.php
/**
* Parameter which works as a flag for bypassing the behavior modification of the affected methods.
*
* For example, in tenant context:
* Route::get('/', ...)->name('home');
* // query string identification
* Route::get('/tenant', ...)->middleware(InitializeTenancyByRequestData::class)->name('tenant.home');
* - route('home') => app.test/tenant?tenant=tenantKey
* - route('home', [$bypassParameter => true]) => app.test/
* - route('tenant.home', [$bypassParameter => true]) => app.test/tenant -- no query string added
*
* Note: UrlGeneratorBootstrapper::$addTenantParameterToDefaults is not affected by this, though
* it doesn't matter since it doesn't pass any extra parameters when not needed.
*
* @see Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper
*/
public static string $bypassParameter = 'central';
/**
* Should route names passed to the affected methods
* get prefixed with the tenant route name prefix.
*
* This is useful when using e.g. path identification with third-party packages
* where you don't have control over all route() calls or don't want to change
* too many files. Often this will be when using route cloning.
*/
public static bool $prefixRouteNames = false;
/**
* Should the tenant parameter be passed to the affected methods.
*
* This is useful with path or query parameter identification. The former can be handled
* more elegantly using UrlGeneratorBootstrapper::$addTenantParameterToDefaults.
*
* @see Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper
*/
public static bool $passTenantParameterToRoutes = false;
/**
* Route name overrides.
*
* Note: This behavior can be bypassed using $bypassParameter just like
* $prefixRouteNames and $passTenantParameterToRoutes.
*
* Example from a Jetstream integration:
* [
* 'profile.show' => 'tenant.profile.show',
* 'two-factor.login' => 'tenant.two-factor.login',
* ]
*
* In the tenant context:
* - `route('profile.show')` will return a URL as if you called `route('tenant.profile.show')`.
* - `route('profile.show', ['central' => true])` will return a URL as if you called `route('profile.show')`.
*/
public static array $overrides = [];
/**
* Follow the query_parameter config instead of the tenant_parameter_name (path identification) config.
*
* This only has an effect when:
* - $passTenantParameterToRoutes is enabled, and
* - the tenant_parameter_name config for the path resolver differs from the query_parameter config for the request data resolver.
*
* In such a case, instead of adding ['tenant' => '...'] to the route parameters (or whatever your tenant_parameter_name is if not 'tenant'),
* the query_parameter will be passed instead, e.g. ['team' => '...'] if your query_parameter config is 'team'.
*
* This is enabled by default because typically you will not need $passTenantParameterToRoutes with path identification.
* UrlGeneratorBootstrapper::$addTenantParameterToDefaults is recommended instead when using path identification.
*
* On the other hand, when using request data identification (specifically query string) you WILL need to pass the parameter
* directly to route() calls, therefore you would use $passTenantParameterToRoutes to avoid having to do that manually.
*/
public static bool $passQueryParameter = true;
src/UniqueIdentifierGenerators/RandomHexGenerator.php
public static int $bytes = 6;
src/UniqueIdentifierGenerators/RandomIntGenerator.php
public static int $min = 0;
public static int $max = PHP_INT_MAX;
src/UniqueIdentifierGenerators/RandomStringGenerator.php
public static int $length = 8;
src/ResourceSyncing/Listeners/UpdateOrCreateSyncedResource.php
public static bool $shouldQueue = false;
/**
* This static property allows you to scope the "get model query"
* that's responsible for finding the resources that should get synced (in the getModel() method).
*
* For example, to include soft deleted records while syncing (excluded by default), you can use this closure:
*
* UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
* if ($query->hasMacro('withTrashed')) {
* $query->withTrashed();
* }
* };
*/
public static Closure|null $scopeGetModelQuery = null;
src/TenancyServiceProvider.php
public static Closure|null $configure = null;
public static bool $registerForgetTenantParameterListener = true;
public static bool $migrateFreshOverride = true;
src/RLS/PolicyManagers/TableRLSManager.php
/**
* When true, all valid constraints are considered while generating paths for RLS policies,
* unless explicitly marked with a 'no-rls' comment.
*
* When false, only columns explicitly marked with 'rls' or 'rls table.column' comments are considered.
*/
public static bool $scopeByDefault = true;
src/Tenancy.php
/**
* List of relations to eager load when fetching a tenant via tenancy()->find().
*/
public static array $findWith = [];