Skip to content

Single-database tenancy

Out of the box, the package uses multi-database tenancy since most applications prefer that — it doesn’t require making many (or any) changes to business logic and can be added to an existing application.

However, multi-database tenancy is just a tiny part of the package, and since the package is highly modular you can get many benefits out of this package even with single-database tenancy.

To use single-database tenancy, make the following changes:

Remove any database-related logic from your tenant model:

app/Models/Tenant.php
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
}

Comment out or remove any database-related bootstrappers:

'bootstrappers' => [
// Bootstrappers\DatabaseTenancyBootstrapper::class,
// Bootstrappers\DatabaseSessionBootstrapper::class,
],

Comment out or remove any database-related jobs in the TenancyServiceProvider:

app/Providers/TenancyServiceProvider.php
Events\TenantCreated::class => [
JobPipeline::make([
Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class,
// Jobs\SeedDatabase::class,
// Jobs\CreateStorageSymlinks::class,
])->send(function (Events\TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(false),
],
// ...
Events\TenantDeleted::class => [
JobPipeline::make([
Jobs\DeleteDatabase::class,
])->send(function (Events\TenantDeleted $event) {
return $event->tenant;
})->shouldBeQueued(false),
],

In a single-database context, there are five types of models we’ll be talking about:

  1. Your Tenant model
  2. Primary models — models that directly belong to a tenant (through a belongsTo relationship)
  3. Secondary models — models that indirectly belong to a tenant (through a primary relationship)
    • Comment belongsTo Post belongsTo Tenant
  4. Tertiary models — models that indirectly belong to a tenant through a secondary relationship
    • Vote belongsTo Comment belongsTo Post belongsTo Tenant
  5. Global models — models that are not scoped to the current tenant.

To scope queries correctly, apply the Stancl\Tenancy\Database\Concerns\BelongsToTenant trait on primary models. That will ensure uses of your primary models are scoped to the current tenant, and uses of their children are scoped to the current tenant as long as those uses are through the primary model.

In other words:

Model definitions with only BelongsToTenant
class Post extends Model
{
use BelongsToTenant;
public function comments()
{
return $this->hasMany(Comment::class);
}
}
class Comment extends Model
{
public function post()
{
return $this->belongsTo(Post::class);
}
}
Using only BelongsToTenant
tenant()->posts(); // scoped using Eloquent, regardless of traits
Post::all(); // scoped using BelongsToTenant
$post->comments(); // scoped using Eloquent
Comment::all(); // NOT SCOPED

If you tried to use Comment::all() directly here, you’d still get posts of all tenants. To solve that, you also need to apply the BelongsToPrimaryModel trait (from the same namespace) on all secondary models.

Model definition with BelongsToPrimaryModel
class Comment extends Model
{
use BelongsToPrimaryModel;
public function getRelationshipToPrimaryModel(): string
{
return 'post';
}
public function post()
{
return $this->belongsTo(Post::class);
}
}

With that, you get this scoping:

Using BelongsToTenant + BelongsToPrimaryModel
tenant()->posts(); // scoped using Eloquent, regardless of traits
Post::all(); // scoped using BelongsToTenant
$post->comments(); // scoped using Eloquent
Comment::all(); // scoped using BelongsToPrimaryModel
$comment->votes(); // scoped using Eloquent
Vote::all(); // NOT SCOPED

We do not provide a solution for tertiary (and more indirect) relations out of the box. It is up to you to use some workaround or make sure to always access them through a parent model that is scoped.

Note that this only applies to “select many” queries. If you have a specific instance, Tenancy doesn’t enforce anything. These traits only affect what you may query using models.

The BelongsToTenant model uses the FillsCurrentTenant trait which automatically fills the tenant_id column with the current tenant ID if no ID was passed manually.

In other words, you do not need to pass the current tenant ID to Post::create() (from the example above).

To scope database indices, such as UNIQUE indices, you may need to change their definitions like this:

$table->unique('slug');
$table->unique(['tenant_id', 'slug']);

On secondary models you can reference the parent table in the first part of the index.

Laravel’s built-in validation rules use simple DB queries, not models. You need to scope them accordingly:

Rule::unique('posts', 'slug')->where('tenant_id', tenant('id'));

To avoid having to write all that out repeatedly, you can use the Stancl\Tenancy\Database\Concerns\HasScopedValidationRules trait to be able to use these helper functions:

// Current tenant
$tenant = tenant();
$rules = [
'id' => $tenant->exists('posts'),
'slug' => $tenant->unique('posts'),
];

It’s important to keep in mind that DB facade queries, or any similarly low level queries, will not be scoped. All these traits do is:

  1. Use Eloquent scopes to scope queries made using the respective models
  2. Use the FillsCurrentTenant trait to automatically fill the current tenant ID if a primary model is created without a tenant ID

To disable the tenant scope, simply add withoutTenancy() to your query. That method is a macro which expands to:

$builder->withoutGlobalScope(\Stancl\Tenancy\Database\TenantScope::class);

If you’d like to change the column name from tenant_id to e.g. team_id, you can do so by changing the tenancy.models.tenant_key_column config:

/**
* Name of the column used to relate models to tenants.
*
* This is used by the HasDomains trait, and models that use the BelongsToTenant trait (used in single-database tenancy).
*/
'tenant_key_column' => 'tenant_id',
'tenant_key_column' => 'team_id',

Note that, as the docblock suggests, this config key also affects the HasDomains trait, so you’d be expected to rename the domains.tenant_id database column to domains.team_id.