laravel-tenancy-support maintained by misaf
Laravel Tenancy Support
Shared tenancy contracts and Eloquent helpers for tenant-aware packages.
Requirements
- PHP 8.2+
- Laravel application runtime (for container + Eloquent usage)
Installation
composer require misaf/laravel-tenancy-support
Contracts
TenantResolver
namespace Misaf\TenancySupport\Contracts;
interface TenantResolver
{
public function getTenantId(): int|string|null;
}
TeamResolver
namespace Misaf\TenancySupport\Contracts;
interface TeamResolver
{
public function getTeamId(): int|string|null;
}
TenantAccessResolver (optional)
Use this when your resolver can authorize elevated, all-tenant access (for example, super admin).
namespace Misaf\TenancySupport\Contracts;
interface TenantAccessResolver
{
public function canAccessAllTenants(): bool;
}
TeamAccessResolver (optional)
Use this when your resolver can authorize elevated, all-team access.
namespace Misaf\TenancySupport\Contracts;
interface TeamAccessResolver
{
public function canAccessAllTeams(): bool;
}
Resolver Example
namespace App\Tenancy;
use Misaf\TenancySupport\Contracts\TeamAccessResolver;
use Misaf\TenancySupport\Contracts\TeamResolver;
use Misaf\TenancySupport\Contracts\TenantAccessResolver;
use Misaf\TenancySupport\Contracts\TenantResolver;
final class RequestTenantResolver implements TenantResolver, TenantAccessResolver, TeamResolver, TeamAccessResolver
{
public function getTenantId(): int|string|null
{
return auth()->user()?->tenant_id;
}
public function getTeamId(): int|string|null
{
return auth()->id();
}
public function canAccessAllTenants(): bool
{
return (bool) auth()->user()?->is_super_admin;
}
public function canAccessAllTeams(): bool
{
return (bool) auth()->user()?->is_super_admin;
}
}
If TeamResolver is not bound, team scope falls back to auth()->id().
Bind it in your service provider:
use App\Tenancy\RequestTenantResolver;
use Misaf\TenancySupport\Contracts\TeamResolver;
use Misaf\TenancySupport\Contracts\TenantResolver;
public function register(): void
{
$this->app->scoped(TenantResolver::class, RequestTenantResolver::class);
$this->app->scoped(TeamResolver::class, RequestTenantResolver::class);
}
Model Usage
use Illuminate\Database\Eloquent\Model;
use Misaf\TenancySupport\Concerns\BelongsToTenant;
final class Invoice extends Model
{
use BelongsToTenant;
}
Team scope usage:
use Illuminate\Database\Eloquent\Model;
use Misaf\TenancySupport\Concerns\BelongsToTeam;
final class Task extends Model
{
use BelongsToTeam;
}
Behavior:
- Normal queries are tenant-scoped by
tenant_id. - If tenant is not resolved, reads fail-closed (no rows).
- On create,
tenant_idis auto-filled from current tenant when available. - Team-scoped models apply the same behavior using
team_id.
Super Admin: Explicit All-Tenant Queries
Elevated access is opt-in per operation:
use App\Models\Invoice;
use Misaf\TenancySupport\Support\CurrentTenant;
$allInvoices = CurrentTenant::withAllTenants(
fn () => Invoice::query()->latest()->get()
);
withAllTenants(...) throws when current context is not authorized by TenantAccessResolver::canAccessAllTenants().
Team equivalent:
use App\Models\Task;
use Misaf\TenancySupport\Support\CurrentTeam;
$allTeamTasks = CurrentTeam::withAllTeams(
fn () => Task::query()->latest()->get()
);
License
MIT. See LICENSE.