A powerful, multi-tenant capable permission handling package for Laravel with role-based access control, wildcard permissions, high-performance caching, and complete domain/subdomain-based tenancy support.
ubxty/multi-tenant-laravel-permissions is a Laravel package for a powerful, multi-tenant capable permission handling package for laravel with role-based access control, wildcard permissions, high-performance caching, and complete domain/subdomain-based tenancy support..
It currently has 0 GitHub stars and 53 downloads on Packagist (latest version 2.0.0).
Install it with composer require ubxty/multi-tenant-laravel-permissions.
Discover more Laravel packages by ubxty
or browse all Laravel packages to compare alternatives.
Last updated
A powerful, multi-tenant capable permission handling package for Laravel with role-based access control, wildcard permissions, high-performance caching, and complete domain/subdomain-based tenancy support.
Built by Ubxty
posts.*@role, @hasrole, @hasanyrole, @hasallroles, @unlessroletenant.yourapp.comTenantScoped traittenant_idtenant(), tenant_id(), has_tenant(), run_for_tenant()Tenant::get(), Tenant::run(), etc.Install the package via Composer:
composer require ubxty/multi-tenant-laravel-permissions
Publish the configuration and migrations:
php artisan vendor:publish --provider="Ubxty\MultiTenantLaravelPermissions\MultiTenantPermissionsServiceProvider"
Run the migrations:
php artisan migrate
Get multi-tenancy working in your app in 5 steps:
// config/multi-tenant-permissions.php
'tenancy' => [
'tenant_model' => App\Models\Company::class,
'tenant_foreign_key' => 'company_id',
'subdomain_column' => 'subdomain',
],
// app/Models/Company.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Ubxty\MultiTenantLaravelPermissions\Tenancy\Contracts\Tenant;
use Ubxty\MultiTenantLaravelPermissions\Tenancy\Traits\IsTenant;
class Company extends Model implements Tenant
{
use IsTenant;
protected $fillable = ['name', 'subdomain', 'email'];
}
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(prepend: [
\Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromHost::class,
]);
})
// app/Models/Project.php
use Ubxty\MultiTenantLaravelPermissions\Tenancy\Traits\TenantScoped;
class Project extends Model
{
use TenantScoped;
}
$tenant = tenant(); // Get current tenant model
$id = tenant_id(); // Get current tenant ID
if (has_tenant()) { ... } // Check if tenant is set
// Run code for a specific tenant
run_for_tenant($tenant, function ($tenant) {
return Project::all(); // Scoped to $tenant
});
π Full Tenancy Documentation β
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Ubxty\MultiTenantLaravelPermissions\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
use App\Models\Role;
use App\Models\Permission;
// Create permissions
$permission = Permission::create(['name' => 'edit articles']);
// Create roles (with optional tenant scoping)
$role = Role::create([
'name' => 'writer',
'title' => 'Content Writer',
'tenant_id' => tenant_id(), // Optional: scope to current tenant
]);
// Assign permission to role
$role->givePermissionTo($permission);
// Assign role to user
$user->assignRole('writer');
The tenancy section in your config file controls all tenant behavior:
// config/multi-tenant-permissions.php
'tenancy' => [
/*
|--------------------------------------------------------------------------
| Tenant Model
|--------------------------------------------------------------------------
| The Eloquent model that represents a tenant (Company, Organization, etc.)
*/
'tenant_model' => App\Models\Company::class,
/*
|--------------------------------------------------------------------------
| Foreign Key
|--------------------------------------------------------------------------
| The foreign key column used in tenant-scoped models
| e.g., 'tenant_id', 'company_id', 'organization_id'
*/
'tenant_foreign_key' => 'company_id',
/*
|--------------------------------------------------------------------------
| Identification Columns
|--------------------------------------------------------------------------
| Columns used to identify tenants from URLs
*/
'subdomain_column' => 'subdomain', // For subdomain.yourapp.com
'domain_column' => 'domain', // For full domain identification
'identifier_column' => 'subdomain', // Primary identifier
/*
|--------------------------------------------------------------------------
| Domain Settings
|--------------------------------------------------------------------------
*/
'domain_identification' => false, // Enable full domain mode (tenant.com)
'central_domains' => [ // Domains that don't resolve a tenant
'yourapp.com',
'www.yourapp.com',
'localhost',
],
'ignored_subdomains' => ['www', 'api', 'app', 'admin'],
/*
|--------------------------------------------------------------------------
| Path Exclusions
|--------------------------------------------------------------------------
| URL paths that skip tenant resolution (OAuth callbacks, webhooks)
*/
'excluded_paths' => [
'oauth/callback/*',
'webhooks/*',
'api/health',
],
/*
|--------------------------------------------------------------------------
| Security Settings
|--------------------------------------------------------------------------
*/
'abort_without_tenant' => false, // 404 if no tenant on non-central domain
'no_tenant_message' => 'Organization not found',
'validate_user_tenant' => true, // Ensure user belongs to resolved tenant
'tenant_mismatch_action' => 'logout', // 'logout', 'abort', 'redirect'
'hide_data_without_tenant' => true, // Return empty results if no tenant
/*
|--------------------------------------------------------------------------
| Super Admin
|--------------------------------------------------------------------------
*/
'super_admin_role' => 'super_admin', // Role that bypasses tenant restrictions
/*
|--------------------------------------------------------------------------
| URL Configuration
|--------------------------------------------------------------------------
| Auto-configure app URL based on tenant subdomain
*/
'configure_urls' => false,
'url_scheme' => 'https',
'base_domain' => env('APP_BASE_DOMAIN', 'yourapp.com'),
/*
|--------------------------------------------------------------------------
| Debug
|--------------------------------------------------------------------------
*/
'debug_logging' => env('TENANCY_DEBUG', false),
],
| Middleware | Alias | Description | Use Case |
|------------|-------|-------------|----------|
| SetTenantFromHost | tenant.host | Resolves tenant from domain/subdomain | Web routes |
| SetTenantFromUser | tenant.user | Resolves tenant from authenticated user | API routes |
| EnsureTenant | tenant.ensure | Aborts 403 if no tenant resolved | Protected routes |
| SkipTenantResolution | tenant.skip | Bypasses tenant resolution | OAuth, webhooks |
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// Web: Resolve from subdomain
$middleware->web(prepend: [
\Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromHost::class,
]);
// API: Resolve from authenticated user
$middleware->api(append: [
\Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromUser::class,
]);
// Register aliases for route-level usage
$middleware->alias([
'tenant.host' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromHost::class,
'tenant.user' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromUser::class,
'tenant.ensure' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\EnsureTenant::class,
'tenant.skip' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SkipTenantResolution::class,
]);
});
protected $middlewareGroups = [
'web' => [
\Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromHost::class,
// ... other middleware
],
'api' => [
\Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromUser::class,
// ... other middleware
],
];
protected $middlewareAliases = [
'tenant.host' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromHost::class,
'tenant.user' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SetTenantFromUser::class,
'tenant.ensure' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\EnsureTenant::class,
'tenant.skip' => \Ubxty\MultiTenantLaravelPermissions\Tenancy\Middleware\SkipTenantResolution::class,
];
// Require tenant context
Route::middleware(['tenant.ensure'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::resource('projects', ProjectController::class);
});
// Skip tenant resolution (OAuth, webhooks)
Route::middleware(['tenant.skip'])->group(function () {
Route::get('/oauth/callback/{provider}', [OAuthController::class, 'callback']);
Route::post('/webhooks/stripe', [StripeWebhookController::class, 'handle']);
});
// Mix: require auth + tenant
Route::middleware(['auth', 'tenant.ensure'])->group(function () {
Route::get('/profile', [ProfileController::class, 'show']);
});
Add to any model that should be filtered by tenant:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Ubxty\MultiTenantLaravelPermissions\Tenancy\Traits\TenantScoped;
class Project extends Model
{
use TenantScoped;
protected $fillable = ['name', 'description', 'company_id'];
}
What it does automatically:
Project::all() only returns projects for current tenantProject::create([...]) auto-sets company_id// Get all records across all tenants (admin panels, reports)
$allProjects = Project::withoutTenantScope()->get();
// Query for a specific tenant
$projects = Project::forTenant($tenantId)->get();
// Check if model belongs to current tenant
if ($project->belongsToCurrentTenant()) {
// ...
}
class Project extends Model
{
use TenantScoped;
// Override if different from config
public function getTenantForeignKeyName(): string
{
return 'organization_id';
}
}
Available globally after package installation:
// Get current tenant model (or null)
$tenant = tenant();
// Get current tenant ID (or null)
$id = tenant_id();
// Check if tenant is set
if (has_tenant()) {
// In tenant context
}
// Backward compatible alias (for migration from CompanyContext)
$company = company();
// Run code for a specific tenant
$result = run_for_tenant($tenant, function ($tenant) {
// All queries here are scoped to $tenant
return Project::count();
});
// Access TenantContext directly
$context = tenancy();
use Ubxty\MultiTenantLaravelPermissions\Facades\Tenant;
// Get current tenant
$tenant = Tenant::get();
// Get tenant ID
$id = Tenant::id();
// Check if tenant is set
if (Tenant::check()) { ... }
// Run for tenant
Tenant::run($tenant, function ($tenant) {
return Project::all();
});
// Register booting callback
Tenant::booting(function ($tenant) {
Log::info("Booting tenant: {$tenant->name}");
});
// Register ending callback
Tenant::ending(function ($tenant) {
Log::info("Ending tenant: {$tenant->name}");
});
Run code within a specific tenant's context:
// Using helper
$projects = run_for_tenant($tenant, function ($tenant) {
// All TenantScoped models query this tenant
return Project::with('tasks')->get();
});
// Using facade
$count = Tenant::run($tenant, fn($t) => Project::count());
// Using TenantContext directly
use Ubxty\MultiTenantLaravelPermissions\Tenancy\Services\TenantContext;
app(TenantContext::class)->run($tenant, function ($tenant) {
// ...
});
Note: Context switching is synchronous. The original tenant is restored after the callback.
use App\Models\Role;
use App\Models\Permission;
// Create permissions
Permission::create(['name' => 'view posts']);
Permission::create(['name' => 'create posts']);
Permission::create(['name' => 'edit posts']);
Permission::create(['name' => 'delete posts']);
// Create a role (optionally scoped to tenant)
$role = Role::create([
'name' => 'editor',
'title' => 'Content Editor',
'tenant_id' => tenant_id(), // null for global roles
]);
// Assign permissions to role
$role->givePermissionTo('view posts', 'create posts', 'edit posts');
// Or sync permissions
$role->syncPermissions(['view posts', 'create posts']);
// Remove a permission
$role->revokePermissionTo('delete posts');
$user = User::find(1);
// Assign a single role
$user->assignRole('editor');
// Assign multiple roles
$user->assignRole('editor', 'writer');
// Sync roles (replaces all current roles)
$user->syncRoles(['editor', 'admin']);
// Remove a role
$user->removeRole('writer');
// Check specific permission
if ($user->hasPermissionTo('edit posts')) { ... }
// Check via role
if ($user->hasRole('admin')) { ... }
// Check any role
if ($user->hasAnyRole(['admin', 'editor'])) { ... }
// Check all roles
if ($user->hasAllRoles(['editor', 'writer'])) { ... }
// Using Laravel's can() - works with Gate
if ($user->can('edit posts')) { ... }
Check permissions for a specific tenant:
// Check if user has role for a specific tenant
if ($user->hasRoleForTenant('admin', $tenantId)) { ... }
// Assign a role for a specific tenant
$user->assignRoleForTenant('manager', $tenantId);
// Get all roles for a tenant
$roles = $user->getRolesForTenant($tenantId);
// Sync roles for a tenant
$user->syncRolesForTenant(['editor', 'writer'], $tenantId);
// Check permission for a tenant
if ($user->hasPermissionForTenant('edit posts', $tenantId)) { ... }
Extend the package models for custom logic:
// App\Models\Role.php
namespace App\Models;
use Ubxty\MultiTenantLaravelPermissions\Models\Role as BaseRole;
class Role extends BaseRole
{
// Your custom logic
}
Update your config to use custom models:
// config/multi-tenant-permissions.php
'models' => [
'permission' => App\Models\Permission::class,
'role' => App\Models\Role::class,
],
Grant access to a group of permissions with wildcards:
// Create specific permissions
Permission::create(['name' => 'posts.create']);
Permission::create(['name' => 'posts.edit']);
Permission::create(['name' => 'posts.delete']);
Permission::create(['name' => 'posts.publish']);
// Create wildcard permission
$wildcard = Permission::create(['name' => 'posts.*']);
// Assign wildcard - grants access to ALL posts.* permissions
$role->givePermissionTo($wildcard);
// Now this role has access to all posts permissions
$user->can('posts.create'); // true
$user->can('posts.edit'); // true
$user->can('posts.delete'); // true
$user->can('posts.publish'); // true
// Multi-level wildcards
Permission::create(['name' => 'admin.*']); // All admin permissions
Permission::create(['name' => 'admin.users.*']); // All admin user permissions
Attach JSON settings to permissions when assigning to roles:
$role = Role::findByName('manager');
// Grant permission with specific scope settings
$role->givePermissionToWithSettings('view_projects', [
'scope' => 'team_only',
'max_count' => 10,
'regions' => ['us', 'eu'],
]);
// Update settings
$role->updatePermissionSettings('view_projects', [
'scope' => 'global',
'max_count' => 100,
]);
// Retrieve settings
$settings = $role->getPermissionSettings('view_projects');
// ['scope' => 'global', 'max_count' => 100, 'regions' => ['us', 'eu']]
// Use in application logic
if ($user->hasPermissionTo('view_projects')) {
$settings = $user->getPermissionSettings('view_projects');
if ($settings['scope'] === 'team_only') {
$projects = Project::where('team_id', $user->team_id)->get();
}
}
This package implements a dual-layer caching system for optimal performance:
Add cache columns to your users table:
Schema::table('users', function (Blueprint $table) {
$table->json('permissions_cache')->nullable();
$table->json('roles_cache')->nullable();
});
| Scenario | Without Cache | With Cache | |----------|--------------|------------| | Dashboard with 20 permission checks | 20+ DB queries | 1 DB query | | Kanban board with 50 items | 50+ DB queries | 1 DB query | | API endpoint with multiple checks | N DB queries | 0 DB queries* |
*After first request loads cache into memory.
// config/multi-tenant-permissions.php
'cache' => [
'expiration_time' => 60 * 24, // minutes
'key' => 'multi_tenant_permissions.cache',
'store' => 'default',
],
Cache is automatically cleared when:
Manual cache clear:
php artisan permission:cache-reset
// Single role
Route::get('/admin', [AdminController::class, 'index'])
->middleware('role:admin');
// Multiple roles (any)
Route::get('/manage', [ManageController::class, 'index'])
->middleware('role:admin|manager');
// Single permission
Route::post('/posts', [PostController::class, 'store'])
->middleware('permission:create posts');
// Multiple permissions (any)
Route::put('/posts/{id}', [PostController::class, 'update'])
->middleware('permission:edit posts|manage posts');
// Role OR permission
Route::delete('/posts/{id}', [PostController::class, 'destroy'])
->middleware('role_or_permission:admin|delete posts');
Laravel 11+ (bootstrap/app.php):
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \Ubxty\MultiTenantLaravelPermissions\Middleware\RoleMiddleware::class,
'permission' => \Ubxty\MultiTenantLaravelPermissions\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Ubxty\MultiTenantLaravelPermissions\Middleware\RoleOrPermissionMiddleware::class,
]);
})
Laravel 10 (app/Http/Kernel.php):
protected $middlewareAliases = [
'role' => \Ubxty\MultiTenantLaravelPermissions\Middleware\RoleMiddleware::class,
'permission' => \Ubxty\MultiTenantLaravelPermissions\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Ubxty\MultiTenantLaravelPermissions\Middleware\RoleOrPermissionMiddleware::class,
];
{{-- Check single role --}}
@role('admin')
<a href="/admin">Admin Panel</a>
@endrole
{{-- Check any role --}}
@hasanyrole('admin|editor')
<button>Edit Content</button>
@endhasanyrole
{{-- Check all roles --}}
@hasallroles('editor|writer')
<p>You can both write and edit!</p>
@endhasallroles
{{-- Negation --}}
@unlessrole('admin')
<p>You need admin access to see more options.</p>
@endunlessrole
{{-- Laravel's native @can works too --}}
@can('edit posts')
<button>Edit Post</button>
@endcan
@canany(['edit posts', 'delete posts'])
<div class="post-actions">...</div>
@endcanany
# Clear permission cache for all users
php artisan permission:cache-reset
# Show current permission cache status
php artisan permission:show
If you're migrating from a custom tenancy implementation (like CompanyContext), here's how:
// app/Providers/AppServiceProvider.php
public function register()
{
// Alias the package TenantContext as your old class
$this->app->alias(
\Ubxty\MultiTenantLaravelPermissions\Tenancy\Services\TenantContext::class,
\App\Services\Tenancy\CompanyContext::class
);
}
Replace imports:
use App\Services\Tenancy\CompanyContext;
β
use Ubxty\MultiTenantLaravelPermissions\Tenancy\Services\TenantContext;
Replace method calls:
// Old
CompanyContext::get();
CompanyContext::set($company);
// New (facade)
Tenant::get();
Tenant::set($company);
// Or helpers
tenant();
The package includes a company() helper for backward compatibility:
// These are equivalent:
$company = company();
$company = tenant();
The full configuration file allows customization of:
See the published config/multi-tenant-permissions.php for all options.
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security related issues, please email [email protected] or open an issue on GitHub.
The MIT License (MIT). Please see License File for more information.