Configuration Reference¶
All configuration lives under the tenancy: key in config/packages/tenancy.yaml. Every key has a sensible default — you only need to specify the keys you want to change.
Config Keys¶
tenancy.driver¶
| Type | Default |
|---|---|
string |
database_per_tenant |
Selects the tenant isolation strategy. Two values are supported:
database_per_tenant— Each tenant gets its own database. The bundle routes through aDoctrine\DBAL\Driver\Middlewareon thetenantconnection; the middleware merges the active tenant'sgetConnectionConfig()atconnect()time. Requiresdoctrine/dbalanddatabase.enabled: true. See Database-per-Tenant Driver for the full pipeline.shared_db— All tenants share one database. Queries are automatically scoped via thetenancy_awareDoctrine SQL filter on entities marked#[TenantAware]. Requiresdoctrine/orm.
tenancy.strict_mode¶
| Type | Default |
|---|---|
bool |
true |
When true (the default), querying a #[TenantAware] entity without an active tenant context throws TenantMissingException. This prevents cross-tenant data leaks.
When false, queries without an active tenant silently return all rows from all tenants.
See Strict Mode for the full security rationale and how to disable.
tenancy.landlord_connection¶
| Type | Default |
|---|---|
string |
default |
The DBAL connection name used for the landlord (central) database — the database that stores the tenancy_tenants table. When database.enabled: true, the bundle rewires DoctrineTenantProvider to use the entity manager bound to this connection.
tenancy.tenant_entity_class¶
| Type | Default |
|---|---|
string |
Tenancy\Bundle\Entity\Tenant |
The fully-qualified class name of your Tenant entity. Must implement Tenancy\Bundle\TenantInterface:
interface TenantInterface
{
public function getSlug(): string;
public function getDomain(): ?string;
public function getConnectionConfig(): array;
public function getName(): string;
public function isActive(): bool;
}
Override this if you extend Tenancy\Bundle\Entity\Tenant or provide your own implementation.
tenancy.cache_prefix_separator¶
| Type | Default |
|---|---|
string |
. |
The separator inserted between the tenant slug and the cache key when the cache bootstrapper namespaces the cache pool. For example, with the default separator and slug acme, a cache key user.123 becomes acme.user.123.
tenancy.database.enabled¶
| Type | Default |
|---|---|
bool |
false |
Set to true to activate the database-per-tenant driver. This:
- Registers
DatabaseSwitchBootstrapperas a bootstrapper (calls$connection->close()on boot/clear) - Registers
TenantDriverMiddlewarewith thedoctrine.middlewaretag scoped toconnection: tenant; the landlord connection is never tagged - Rewires
DoctrineTenantProviderto use thelandlordentity manager - Registers the
tenancy:migratecommand (ifdoctrine/migrationsis installed)
Must NOT be combined with driver: shared_db — see Validation Rules.
tenancy.resolvers¶
| Type | Default |
|---|---|
string[] |
['host', 'header', 'query_param', 'console'] |
The list of active resolver aliases, in priority order (highest priority first). Built-in resolver aliases:
| Alias | Class | Priority |
|---|---|---|
host |
HostResolver |
30 |
origin |
OriginHeaderResolver |
25 |
header |
HeaderResolver |
20 |
query_param |
QueryParamResolver |
10 |
console |
ConsoleResolver |
N/A (ConsoleCommandEvent) |
To disable a resolver, remove its alias from the list. Custom resolvers are registered automatically via DI autoconfiguration (see Custom Resolver).
tenancy.host.app_domain¶
| Type | Default |
|---|---|
string\|null |
null |
The base domain used by HostResolver for subdomain extraction. When set to example.com, a request to acme.example.com resolves to tenant slug acme.
When null (the default), HostResolver always returns null and passes control to the next resolver in the chain.
For multi-segment subdomains (e.g. api.acme.example.com), the last segment before the app_domain suffix is used as the slug (acme).
tenancy.origin.allow_list¶
| Type | Default |
|---|---|
array |
[] (empty — resolver disabled) |
The list of allow-listed origins for OriginHeaderResolver (priority 25). Each entry is either a map form { origin: ..., slug: ... } pinning a specific origin to a specific tenant slug, or a wildcard-shorthand string where the leftmost label of the matched host becomes the slug at runtime.
# config/packages/tenancy.yaml
tenancy:
resolvers: ['host', 'header', 'origin']
origin:
allow_list:
# Explicit map form: pin a specific origin to a specific tenant slug.
- { origin: 'https://acme.app.example.com', slug: 'acme' }
- { origin: 'https://beta.app.example.com', slug: 'beta-customer' }
# Wildcard shorthand: leftmost label becomes the slug at runtime.
# `https://*.app.example.com` matches `https://anything.app.example.com`
# and resolves to tenant slug = `anything`.
- 'https://*.app.example.com'
Each entry must be an absolute URL with scheme http or https, no path/query/fragment, and at most one * in the leftmost label. Invalid configurations fail at container compile time with a descriptive error — there is no way to ship a misconfigured allow-list to runtime. For the full Trust Model (what Origin guarantees from a browser context, where the trust ends, and the recommended pairing with real authentication), see Origin Header Resolver.
Per-tenant mailer config¶
Each tenant can carry its own SMTP transport, From header, and Reply-To header via three nullable columns on the Tenant entity. The bundle reads these via the getMailerDsn(), getMailerFrom(), and getMailerReplyTo() methods on TenantInterface. Returning null from any getter falls back to the landlord's default Mailer config.
| Column | Type | Purpose |
|---|---|---|
mailerDsn |
string\|null |
SMTP transport DSN (e.g. smtp://user:****@smtp.example.com:587) |
mailerFrom |
string\|null |
From: header on outgoing emails |
mailerReplyTo |
string\|null |
Reply-To: header (optional) |
The bundle ships Tenancy\Bundle\Mailer\TenantMailerConfigTrait as a one-line shortcut — use TenantMailerConfigTrait; inside a custom Tenant entity supplies all three columns, their getters, and their setters:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; // optional — only honored if doctrine/orm is installed
use Tenancy\Bundle\Entity\AbstractTenant;
use Tenancy\Bundle\Mailer\TenantMailerConfigTrait;
#[ORM\Entity]
#[ORM\Table(name: 'tenancy_tenants')]
class AppTenant extends AbstractTenant
{
use TenantMailerConfigTrait;
}
The bootstrapper works under both synchronous Mailer dispatch and Messenger-routed async dispatch via an X-Transport: tenant_<slug> stamp. A message dequeued for a deleted tenant throws (rather than silently dropping) — this contract is enforced at container compile time. For the full X-Transport strategy and async failure-mode warning, see the Mailer Bootstrapper guide.
Validation Rules¶
The bundle enforces one compile-time constraint:
shared_db + database.enabled = error
Setting driver: shared_db and database.enabled: true simultaneously is rejected at container compile time:
tenancy.driver: shared_db cannot be combined with tenancy.database.enabled: true.
Choose one isolation strategy.
These are mutually exclusive — database_per_tenant uses a per-tenant DBAL connection, while shared_db uses a SQL filter on a shared connection. You cannot run both at once.
Full Example¶
// config/packages/tenancy.php
return static function (\Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator $container): void {
$container->extension('tenancy', [
'driver' => 'database_per_tenant',
'strict_mode' => true,
'landlord_connection' => 'default',
'tenant_entity_class' => 'Tenancy\\Bundle\\Entity\\Tenant',
'cache_prefix_separator' => '.',
'database' => [
'enabled' => false,
],
'resolvers' => ['host', 'header', 'query_param', 'console'],
'host' => [
'app_domain' => null,
],
]);
};
Minimal Examples¶
Scenario 1: Database-per-Tenant (Subdomain SaaS)¶
Scenario 2: Shared-DB (API-first)¶
Scenario 3: API-only with Header Resolver Only¶
strict_mode: false
Disabling strict mode means that queries without an active tenant return all rows from all tenants. Use this only when cross-tenant queries are intentional (e.g. internal admin tooling). See Strict Mode.