Tenancy (Multi-Tenant)
Enable and configure multi-tenant behavior.
VelvetCMS Core ships a built-in, optional multi-tenancy layer. When enabled, tenant context is resolved per request and used to scope content, views, storage, cache, configuration, and module discovery.
Enable tenancy #
Enable the feature in config/tenancy.php:
| Key | Env Variable | Description |
|---|---|---|
tenancy.enabled |
TENANCY_ENABLED |
Enable multi-tenancy |
tenancy.default |
TENANCY_DEFAULT |
Fallback tenant id |
tenancy.resolver |
TENANCY_RESOLVER |
Resolver type: host, path, or callback |
Resolver types #
Host resolver #
Maps hostnames or subdomains to tenant ids.
'host' => [
'map' => [
'example.com' => 'default',
'tenant1.example.com' => 'tenant1',
],
'strip_www' => true,
'wildcard_subdomains' => false,
'root_domains' => [
'example.com',
],
],
If wildcard_subdomains is enabled and the request host is foo.example.com, the tenant id becomes foo.
Path resolver #
Uses a URL path segment as the tenant id:
'path' => [
'segment' => 1,
'map' => [
'acme' => 'tenant-acme',
],
],
With the configuration above, a request to /acme/blog resolves tenant tenant-acme and routes are matched against /blog.
Callback resolver #
Provide a custom resolver class that implements TenantResolverInterface.
'callback' => App\Tenancy\MyResolver::class,
Your resolver receives the Request and the tenancy config and must return a TenantContext or null.
Tenant-aware paths #
When tenancy is enabled, these paths are scoped automatically:
| Scope | Path |
|---|---|
| Content | user/tenants/<tenant>/content |
| Views | user/tenants/<tenant>/views (when view.path starts with user/) |
| Storage | storage/tenants/<tenant> |
Customize the base roots in tenancy.paths.user_root and tenancy.paths.storage_root.
Tenant-local modules #
Module discovery supports tenant-scoped paths via config/modules.php:
'tenant_paths' => [
base_path('user/tenants/{tenant}/modules/*'),
],
When tenancy is enabled and a tenant is resolved, these paths are scanned for module.json manifests. Use {tenant} as a placeholder for the tenant id.
Tenant config overrides #
Tenant overrides live in:
user/tenants/<tenant>/config/
They are merged on top of config/ and user/config/.
Isolation #
Cache isolation #
Cache keys are prefixed with the tenant id and file cache uses tenant storage, preventing cross-tenant collisions.
Database isolation #
VelvetCMS Core supports database-per-tenant mode for true data isolation. Enable it in the database section of config/tenancy.php:
'database' => [
'enabled' => env('TENANCY_DB_ENABLED', false),
'pattern' => env('TENANCY_DB_PATTERN', 'velvet_{tenant}'),
'map' => [
// 'tenant-a' => 'mysql_tenant_a', // Use a named connection
// 'tenant-b' => ['database' => 'custom_db'], // Override specific values
],
],
When enabled, VelvetCMS Core automatically switches the database connection for the resolved tenant:
- Explicit mapping — if the tenant id exists in
map, that connection name or config override is used - Pattern-based — otherwise,
{tenant}inpatternis replaced with the tenant id (e.g.velvet_acme)
If you don't need database-per-tenant, you can still isolate data by overriding DB config per tenant:
user/tenants/<tenant>/config/db.php
Session isolation #
For path-based tenancy, cookies are scoped to the tenant path automatically when session.path is not set.
CLI behavior #
In CLI mode, tenancy uses the default tenant unless a tenant id is set:
| Variable | Priority |
|---|---|
TENANCY_TENANT |
Preferred |
TENANT |
Fallback |
Example:
TENANCY_TENANT=acme ./velvet cache:clear
URLs and assets #
When using path-based tenancy, route generation and asset() automatically include the tenant prefix.
Use tenant_url() to build custom URLs with the tenant prefix.
Helpers #
| Helper | Description |
|---|---|
tenant() |
Returns the current TenantContext |
tenant_id() |
Returns the current tenant id |
tenant_enabled() |
Returns true if tenancy is enabled |
tenant_prefix() |
Returns the URL prefix (path mode) |
tenant_url($path) |
Prefixes a path with the tenant segment |
tenant_user_path($path) |
Resolves tenant user directory |
tenant_storage_path($path) |
Resolves tenant storage directory |
view_path($path) |
Resolves the tenant view directory (when view.path is under user/) |