Docs LATEST

Tenancy (Multi-Tenant)

Enable and configure multi-tenant behavior.

Configuration

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:

  1. Explicit mapping — if the tenant id exists in map, that connection name or config override is used
  2. Pattern-based — otherwise, {tenant} in pattern is 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/)