Skip to content

Pending tenants

The process of creating tenants can be slow in some applications: a database may need to be created, migrated (potentially with hundreds of migrations), perhaps even seeded, or some external services may need to be called to provision some resources for the tenants (creating S3 buckets, issuing API keys, the list goes on).

To solve that problem, we have traditionally offered queued tenant creation. The problem with queued onboarding is that you then need to adjust your tenant onboarding flow since immediately upon signing up, the tenant’s database might not exist, so you need to handle a “preparing” state.

Pending tenants, introduced in version 4, are a new solution to this problem. Instead of using a queued onboarding process, you can simply maintain a pool of pre-created tenants. These tenants are created using a scheduled job and pruned during deployments. In the unlikely case the pool goes empty, the tenant creation process simply becomes synchronous — usually meaning just a bit slow.

To use pending tenants, add the HasPending trait to 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\Concerns\HasPending;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains, HasPending;
}

This trait will add some Eloquent scopes and helper methods to the model.

A tenant is marked as pending if its pending_since attribute is not null. Note that thanks to the virtualcolumn feature, you do not need a dedicated column for this attribute.

To create a pending tenant, you may use the createPending() method. This will add a tenant to the “pool” of pending tenants. To pull a tenant from the pool, you can use the pullPending($attributes = []) method. If the pool is empty, that method will implicitly call create(), therefore the passed $attributes should include values for all non-nullable columns. The pullPendingFromPool($firstOrCreate = false, $attributes = []) method is called by pullPending() and can be used instead of that higher-level method for more control over whether a tenant should be created if the pool is empty or if null should be returned.

Or in code form:

// Empty pool
Tenant::createPending(['foo' => 'bar']);
// Pool size = 1
// Pulled tenant will have both ['foo' => 'bar']
// and ['company' => 'acme'] attributes
$tenant = Tenant::pullPending(['company' => 'acme']);
// Pool size = 0
// Pulled tenant will only have ['company' => 'acme']
$tenant = Tenant::pullPending(['company' => 'acme']);
// The pool is empty, so a tenant was created instead of pulling
// Pool size = 0
$tenant = Tenant::pullPendingFromPool(false, ['company' => 'acme']);
$tenant === null;

The paragraph above mentioned that these calls should include all non-nullable columns because if the pool is empty and the methods end up calling just Tenant::create(), the passed attributes need to include everything necessary for the creation of a tenant record in the database. (You should also include any information your own logic needs, for instance if certain attributes are always expected to be present on the tenant in a TenantCreated job pipeline.)

Similarly, the createPending() method should receive all attributes necessary for the creation of the tenant (for instance if you have a non-nullable slug column, you can set it to a dummy random string until that value is overwritten during a pull). This isn’t always possible though, since as we’ll talk about in a moment, you will typically be creating pending tenants using CLI commands that do not accept any attributes. Therefore, to provide these default values for non-nullable columns, you can use the getPendingAttributes() method:

public function getPendingAttributes(array $attributes = []): array
{
return ['slug' => Str::random(8)];
}

The $attributes argument is only informative. The array returned from this method will be merged with $attributes, so the main use for $attributes is to return different pending attributes dynamically based on the attributes being passed to createPending() (which as mentioned above is generally not the case since you will be using a CLI command).

To create pending tenants, typically in a scheduled job, you can use the tenants:pending-create command:

Terminal window
tenants:pending-create
--count= The number of pending tenants to maintain

Notice the word maintain — the command doesn’t create new tenants if the pool is of the desired size already.

If the --count argument is not provided, the tenancy.pending.count config (or TENANCY_PENDING_COUNT environment variable) is used.

To prune old pending tenants, typically during deployments, you can use the tenants:pending-clear command:

Terminal window
tenants:pending-clear
--older-than-days= Deletes all pending tenants older than the amount of days
--older-than-hours= Deletes all pending tenants older than the amount of hours

The config key tenancy.pending.include_in_queries specifies whether pending tenants should be included in tenant model queries by default. In other words, whether Tenant::all() should include pending tenants.

You may prefer disabling this config to avoid having to manually exclude (more on that below) tenants on pages such as a central admin panel. Keep in mind that doing this will exclude pending tenants from commands such as tenants:migrate. To work around that, you can either clear pending tenants (as suggested above) during a deployment — which is when your migrations would be running — or you can use the --with-pending option when calling tenants:migrate. Many commands that interact with multiple tenants support this option, so make sure to run --help on those commands.

To manually include or exclude pending tenants, you can use the following query builder methods:

  • onlyPending() to only query pending tenants
  • withPending() to always include pending tenants, regardless of the config discussed above
  • withoutPending() to always exclude pending tenants, again regardless of the config discussed above

By default, the pending_since column is cast to timestamp. If you’d prefer using date, perhaps with some specific format, you can set:

app/Models/Tenant.php
public static string $pendingSinceCast = 'date';