intelfric/multi-tenant-role

Multi-tenant role and permission management for Laravel SaaS applications

v4.2.0 2025-07-17 08:32 UTC

This package is not auto-updated.

Last update: 2025-07-17 18:01:38 UTC


README

A Laravel package for multi-tenant role and permission management, perfect for SaaS applications where users can belong to multiple tenants with different roles in each.

Features

  • 🏢 Multi-tenant architecture - Users can belong to multiple tenants
  • 🎭 Tenant-scoped roles - Different roles per tenant for the same user
  • 🔐 Permission management - Fine-grained permissions within each tenant
  • 🛡️ Middleware protection - Route-level tenant permission/role checking
  • 🎨 Blade directives - Template-level authorization checks
  • Easy installation - One command setup
  • 🔧 Configurable - Customizable table names and user models
  • ⚛️ Inertia.js + React support - Modern frontend integration

Installation

1. Install the package via Composer:

composer require intelfric/multi-tenant-role

2. Publish and run the migrations:

php artisan vendor:publish --provider="Intelfric\MultiTenantRole\MultiTenantRoleServiceProvider" --tag="multi-tenant-role-migrations"
php artisan migrate

3. Add the HasTenantRoles trait to your User model:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Intelfric\MultiTenantRole\Traits\HasTenantRoles;

class User extends Authenticatable
{
    use HasTenantRoles;
    
    // ... rest of your User model
}

4. Register the service provider (if not auto-discovered):

Add the service provider to your config/app.php providers array:

'providers' => [
    // ...
    Intelfric\MultiTenantRole\MultiTenantRoleServiceProvider::class,
],

5. Register middleware aliases (Laravel 11):

For Laravel 11, add the middleware aliases to your bootstrap/app.php:

// bootstrap/app.php
$middleware->alias([
    'tenant_can' => \Intelfric\MultiTenantRole\Middleware\TenantCanMiddleware::class,
    'tenant_role' => \Intelfric\MultiTenantRole\Middleware\TenantRoleMiddleware::class,
]);

For Laravel 10 and earlier, add to your app/Http/Kernel.php:

// app/Http/Kernel.php
protected $middlewareAliases = [
    // ... other middleware aliases
    'tenant_can' => \Intelfric\MultiTenantRole\Middleware\TenantCanMiddleware::class,
    'tenant_role' => \Intelfric\MultiTenantRole\Middleware\TenantRoleMiddleware::class,
];

6. Clear configuration cache:

php artisan config:clear
php artisan route:clear
php artisan cache:clear

Troubleshooting

For detailed troubleshooting information, see the Troubleshooting Guide.

Quick Fixes

Middleware not recognized?

  1. Ensure middleware aliases are registered in bootstrap/app.php (Laravel 11) or app/Http/Kernel.php (Laravel 10 and earlier)
  2. Clear all caches:
php artisan optimize:clear

"Tenant not specified" error? Ensure your route includes the tenant parameter:

// ✅ Correct
Route::get('/tenants/{tenant}/posts', [PostController::class, 'index'])
    ->middleware(['auth', 'tenant_can:view-posts,tenant']);

// ❌ Wrong
Route::get('/posts', [PostController::class, 'index'])
    ->middleware(['auth', 'tenant_can:view-posts,tenant']);

Need more help?

Basic Usage

Creating Tenants, Roles, and Permissions

use Intelfric\MultiTenantRole\Models\Tenant;
use Intelfric\MultiTenantRole\Models\Role;
use Intelfric\MultiTenantRole\Models\Permission;

// Create a tenant
$tenant = Tenant::create([
    'name' => 'Acme Corporation',
    'slug' => 'acme-corp'
]);

// Create permissions
$editPermission = Permission::create(['name' => 'edit-posts']);
$deletePermission = Permission::create(['name' => 'delete-posts']);

// Create a role for the tenant
$editorRole = Role::createForTenant('editor', $tenant);

// Assign permissions to the role
$editorRole->givePermissionTo($editPermission);
$editorRole->givePermissionTo($deletePermission);

Assigning Users to Tenants

// Assign user to tenant with a role
$user->assignRoleToTenant($editorRole, $tenant);

// Or using the facade
use Intelfric\MultiTenantRole\Facades\MultiTenantRole;

MultiTenantRole::assignUserToTenant($user, $tenant, $editorRole);

Checking Permissions and Roles

// Check if user has permission in tenant
if ($user->hasPermissionInTenant('edit-posts', $tenant)) {
    // User can edit posts in this tenant
}

// Check if user has role in tenant
if ($user->hasRoleInTenant('editor', $tenant)) {
    // User is an editor in this tenant
}

// Get user's role in tenant
$role = $user->getRoleInTenant($tenant);

// Get user's permissions in tenant
$permissions = $user->getPermissionsInTenant($tenant);

Middleware

Important: Make sure to register the middleware aliases in your bootstrap/app.php (Laravel 11) or app/Http/Kernel.php (Laravel 10 and earlier) as shown in the installation steps above.

Protect your routes with tenant-aware middleware:

// Require specific permission in tenant
Route::middleware(['auth', 'tenant_can:edit-posts,tenant'])->group(function () {
    Route::put('/tenants/{tenant}/posts/{post}', [PostController::class, 'update']);
});

// Require specific role in tenant
Route::middleware(['auth', 'tenant_role:editor,tenant'])->group(function () {
    Route::get('/tenants/{tenant}/dashboard', [DashboardController::class, 'index']);
});

The middleware expects the tenant to be passed as a route parameter. You can customize the parameter name:

// Use 'organization' instead of 'tenant'
Route::middleware(['tenant_can:edit-posts,organization'])->group(function () {
    Route::put('/orgs/{organization}/posts/{post}', [PostController::class, 'update']);
});

Flexible Tenant Resolution

The middleware automatically resolves tenants by ID, name, slug, or other attributes:

// Works with tenant ID
Route::get('/tenants/1/profile', [ProfileController::class, 'edit'])
    ->middleware('tenant_can:view-profile,tenant');

// Works with tenant slug
Route::get('/tenants/acme-corp/profile', [ProfileController::class, 'edit'])
    ->middleware('tenant_can:view-profile,tenant');

// Works with tenant name
Route::get('/tenants/Acme%20Corporation/profile', [ProfileController::class, 'edit'])
    ->middleware('tenant_can:view-profile,tenant');

// Works with URL-encoded names
Route::get('/tenants/Acme%20Corp/profile', [ProfileController::class, 'edit'])
    ->middleware('tenant_can:view-profile,tenant');

The middleware tries multiple resolution methods in this order:

  1. Route Model Binding (if tenant is already a model instance)
  2. Numeric ID (direct database lookup)
  3. Slug (exact match)
  4. Name (case-insensitive LIKE search)
  5. URL-decoded values (for encoded slugs/names)

Multiple Parameter Names

The middleware supports various parameter names for different use cases:

// Organization-based routes
Route::prefix('/organizations/{organization}')->group(function () {
    Route::get('/team', [TeamController::class, 'index'])
        ->middleware('tenant_can:manage-team,organization');
});

// Company-based routes
Route::prefix('/companies/{company}')->group(function () {
    Route::get('/projects', [ProjectController::class, 'index'])
        ->middleware('tenant_can:view-projects,company');
});

// Workspace-based routes
Route::prefix('/workspaces/{workspace}')->group(function () {
    Route::get('/tasks', [TaskController::class, 'index'])
        ->middleware('tenant_can:view-tasks,workspace');
});

Blade Directives

Use Blade directives for template-level authorization:

@tenantCan('edit-posts', $tenant)
    <a href="{{ route('posts.edit', [$tenant, $post]) }}">Edit Post</a>
@endTenantCan

@tenantRole('admin', $tenant)
    <a href="{{ route('tenant.settings', $tenant) }}">Tenant Settings</a>
@endTenantRole

Inertia.js + React Support

For modern frontend applications using Inertia.js with React, the package provides seamless integration with React components and hooks. See the Inertia React Guide for detailed setup and usage examples.

// React Permission Guard Component
<PermissionGuard permission="edit-posts">
    <button onClick={handleEdit}>Edit Post</button>
</PermissionGuard>

// React Permission Hook
const { hasPermission, hasRole } = usePermissions();

if (hasPermission('edit-posts')) {
    // User can edit posts
}

API Reference

User Methods (via HasTenantRoles trait)

// Tenant relationships
$user->tenants; // Get all tenants for user
$user->belongsToTenant($tenant); // Check if user belongs to tenant

// Role management
$user->assignRoleToTenant($role, $tenant);
$user->removeRoleFromTenant($role, $tenant);
$user->removeFromTenant($tenant);

// Role checking
$user->hasRoleInTenant($roleName, $tenant);
$user->hasAnyRoleInTenant(['admin', 'editor'], $tenant);
$user->hasAllRolesInTenant(['editor', 'publisher'], $tenant);
$user->getRoleInTenant($tenant);

// Permission checking
$user->hasPermissionInTenant($permissionName, $tenant);
$user->hasAnyPermissionInTenant(['edit', 'delete'], $tenant);
$user->hasAllPermissionsInTenant(['read', 'write'], $tenant);
$user->getPermissionsInTenant($tenant);

// Utility methods
$user->getAllTenantRoles(); // Get roles across all tenants
$user->getTenantsWithRole($roleName);
$user->getTenantsWithPermission($permissionName);

Tenant Methods

$tenant->users; // Get all users in tenant
$tenant->roles; // Get all roles for tenant

$tenant->addUser($user, $role); // Add user with optional role
$tenant->removeUser($user); // Remove user from tenant
$tenant->hasUser($user); // Check if user belongs to tenant
$tenant->getUserRole($user); // Get user's role in tenant

Role Methods

$role->tenant; // Get tenant that owns the role
$role->permissions; // Get all permissions for role

// Scopes
Role::forTenant($tenant)->get(); // Get roles for specific tenant
Role::global()->get(); // Get global roles (no tenant)

// Permission management
$role->givePermissionTo($permission);
$role->revokePermissionTo($permission);
$role->hasPermissionTo($permission);
$role->hasPermission($permissionName);
$role->syncPermissions($permissions);

// Factory method
Role::createForTenant($name, $tenant, $attributes);

Permission Methods

$permission->roles; // Get all roles that have this permission

// Factory methods
Permission::findByName($name, $guardName);
Permission::findOrCreate($name, $guardName);

// Scopes
Permission::forTenant($tenant)->get(); // Get permissions used in tenant

Facade Usage

use Intelfric\MultiTenantRole\Facades\MultiTenantRole;

// Create entities
$tenant = MultiTenantRole::createTenant('Company Name');
$role = MultiTenantRole::createRole('manager', $tenant);
$permission = MultiTenantRole::createPermission('manage-users');

// User management
MultiTenantRole::assignUserToTenant($user, $tenant, $role);
MultiTenantRole::userHasPermissionInTenant($user, 'manage-users', $tenant);
MultiTenantRole::userHasRoleInTenant($user, 'manager', $tenant);

// Utility methods
$tenants = MultiTenantRole::getUserTenants($user);
$role = MultiTenantRole::getUserRoleInTenant($user, $tenant);

Configuration

Publish the config file to customize the package:

php artisan vendor:publish --tag=multi-tenant-role-config

The config file allows you to customize:

  • User model class
  • Table names
  • Column names
  • Cache settings
  • Middleware aliases
// config/multi-tenant-role.php
return [
    'user_model' => App\Models\User::class,
    
    'table_names' => [
        'tenants' => 'tenants',
        'roles' => 'roles',
        'permissions' => 'permissions',
        'role_has_permissions' => 'role_has_permissions',
        'tenant_user' => 'tenant_user',
    ],
    
    // ... other configuration options
];

Database Schema

The package creates the following tables:

  • tenants - Stores tenant information
  • permissions - Stores available permissions
  • roles - Stores roles (with optional tenant_id for scoping)
  • role_has_permissions - Pivot table for role-permission relationships
  • tenant_user - Pivot table for user-tenant relationships with role assignment

Advanced Usage

Global vs Tenant Roles

You can create global roles (not tied to any tenant) by omitting the tenant:

// Global role (works across all tenants)
$globalAdmin = Role::create([
    'name' => 'super-admin',
    'tenant_id' => null
]);

// Tenant-specific role
$tenantAdmin = Role::createForTenant('admin', $tenant);

Custom User Model

If you're using a custom User model, update the config:

// config/multi-tenant-role.php
'user_model' => App\Models\CustomUser::class,

Route Model Binding

The middleware works seamlessly with route model binding:

// routes/web.php
Route::middleware(['tenant_can:edit-posts'])->group(function () {
    Route::put('/tenants/{tenant}/posts/{post}', [PostController::class, 'update']);
});

// In your controller
public function update(Tenant $tenant, Post $post)
{
    // The middleware has already verified the user has 'edit-posts' permission in this tenant
    // Your update logic here
}

Frontend Integration

Traditional Blade Templates

Use the provided Blade directives for server-side rendering:

@tenantCan('edit-posts', $tenant)
    <a href="{{ route('posts.edit', [$tenant, $post]) }}">Edit Post</a>
@endTenantCan

Inertia.js + React

For modern SPAs, use the React components and hooks:

import PermissionGuard from '@/Components/PermissionGuard';
import usePermissions from '@/Hooks/usePermissions';

function ProjectCard({ project, tenant }) {
    const { hasPermission } = usePermissions();
    
    return (
        <div>
            <h3>{project.name}</h3>
            <PermissionGuard permission="edit-projects">
                <button>Edit</button>
            </PermissionGuard>
        </div>
    );
}

See the complete Inertia React Guide for detailed examples.

Documentation

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Security

If you discover any security related issues, please email the maintainers instead of using the issue tracker.

License

The MIT License (MIT). Please see License File for more information.

Credits

Support

If you find this package useful, please consider starring the repository. For support questions, please use the GitHub issues.

Owner Roles and Branch Access

This package implements a smart role system where owner/administrator roles can access all branches within their tenant without needing branch-specific role assignments, while other roles are branch-specific.

Owner Role Concept

Owner roles (like 'owner', 'admin', 'administrator') automatically have access to all branches within their tenant. This means:

  • ✅ Owner can access any branch without explicit branch assignment
  • ✅ Other roles (manager, staff, etc.) need specific branch assignments
  • ✅ Owner permissions are inherited from tenant-level role
  • ✅ Branch-specific roles only apply to non-owner users

Role Hierarchy

The package supports three levels of roles:

  1. Global Roles (tenant_id = null, branch_id = null)

    • Work across all tenants and branches
    • Example: Super admin, system administrator
  2. Tenant-Level Roles (tenant_id = X, branch_id = null)

    • Work across all branches within a specific tenant
    • Example: Owner, admin, tenant administrator
    • Owner roles automatically have access to all branches
  3. Branch-Level Roles (tenant_id = X, branch_id = Y)

    • Work only within a specific branch
    • Example: Branch manager, branch staff, cashier

Database Structure

  • branches table: Stores branch information (name, slug, address, etc.)
  • branch_user table: Pivot table for user-branch-role relationships
  • roles table: Includes nullable branch_id column for branch-specific roles

Models

  • Branch model: Represents a branch and its relationships
  • Role model: Supports branch-level roles via branch_id
  • Tenant model: Has many branches and owner role detection

Configuration

Configure which roles are considered owner roles:

// config/multi-tenant-role.php
'owner_roles' => [
    'owner',
    'admin', 
    'administrator',
    'super-admin',
    'super_admin',
],

Example Usage

// Create a tenant
$tenant = Tenant::create([
    'name' => 'Acme Corporation',
    'slug' => 'acme-corp'
]);

// Create branches
$mainBranch = $tenant->createBranch([
    'name' => 'Main Office',
    'slug' => 'main-office',
]);

$darBranch = $tenant->createBranch([
    'name' => 'Dar es Salaam Branch',
    'slug' => 'dar-branch',
]);

// Create roles
$ownerRole = Role::createOwnerRole($tenant); // Tenant-level owner role
$adminRole = Role::createAdminRole($tenant); // Tenant-level admin role
$managerRole = Role::createForBranch('manager', $mainBranch); // Branch-specific role
$staffRole = Role::createForBranch('staff', $darBranch); // Branch-specific role

// Assign users
$owner = User::find(1);
$manager = User::find(2);
$staff = User::find(3);

// Owner gets tenant-level role (access to all branches)
$owner->assignRoleToTenant($ownerRole, $tenant);

// Manager gets branch-specific role
$manager->assignRoleToBranch($managerRole, $mainBranch);

// Staff gets branch-specific role
$staff->assignRoleToBranch($staffRole, $darBranch);

// Check permissions
if ($owner->hasOwnerRoleInTenant($tenant)) {
    // Owner can access any branch
    echo "Owner can access all branches";
}

if ($owner->hasPermissionInTenantOrBranch('view-reports', $tenant, $mainBranch)) {
    // Owner can view reports in any branch
    echo "Owner can view reports in main branch";
}

if ($manager->hasPermissionInBranch('view-reports', $mainBranch)) {
    // Manager can only view reports in their assigned branch
    echo "Manager can view reports in main branch";
}

if ($staff->hasPermissionInBranch('view-reports', $darBranch)) {
    // Staff can only view reports in their assigned branch
    echo "Staff can view reports in Dar branch";
}

Enhanced Permission Checking

The package provides enhanced methods that automatically handle owner role logic:

// Enhanced permission check - automatically handles owner roles
$user->hasPermissionInTenantOrBranch('edit-users', $tenant, $branch);

// Enhanced role check - automatically handles owner roles  
$user->hasRoleInTenantOrBranch('manager', $tenant, $branch);

// Check if user has owner role
$user->hasOwnerRoleInTenant($tenant);

// Get all branches where user has a specific role
$user->getBranchesWithRole('manager');

// Get all branches where user has a specific permission
$user->getBranchesWithPermission('edit-users');

// Get all branches user can access (including all tenant branches for owners)
$user->getAccessibleBranches($tenant);

// Check if user can access a specific branch
$user->canAccessBranch($branch);

Inertia.js Data Structure

When using Inertia.js, the package provides comprehensive data sharing:

{
  "user": { "id": 1, "name": "Owner User", "tenants": [...], "branches": [...] },
  "tenant": { "id": 1, "name": "Acme Corp", "slug": "acme" },
  "branch": { "id": 1, "name": "Main Office", "slug": "main" },
  "userRole": "owner",
  "userPermissions": ["view-users", "edit-users"],
  "userTenants": [
    {
      "id": 1,
      "name": "Acme Corp",
      "slug": "acme", 
      "role": "owner",
      "accessType": "direct"
    }
  ],
  "userBranches": [
    {
      "id": 1,
      "name": "Main Office",
      "slug": "main",
      "address": "123 Main St",
      "is_active": true,
      "accessType": "owner",
      "role": null
    }
  ],
  "hasOwnerRole": true,
  "canAccessAllBranches": true
}

Key Features:

  • Owner users automatically see all tenant branches in userBranches
  • Branch-only users see only their assigned branches
  • Mixed access supported for users with both tenant and branch roles
  • Access type indication shows how user accesses each resource

Complete Setup Example

Here's a complete example of setting up a multi-branch organization:

// 1. Create tenant
$tenant = Tenant::create([
    'name' => 'Acme Corporation',
    'slug' => 'acme-corp'
]);

// 2. Create branches
$mainBranch = $tenant->createBranch([
    'name' => 'Main Office',
    'slug' => 'main-office',
    'address' => '123 Main St, City'
]);

$darBranch = $tenant->createBranch([
    'name' => 'Dar es Salaam Branch',
    'slug' => 'dar-branch',
    'address' => '456 Ocean Rd, Dar es Salaam'
]);

// 3. Create permissions
$viewReports = Permission::create(['name' => 'view-reports']);
$editUsers = Permission::create(['name' => 'edit-users']);
$manageBranch = Permission::create(['name' => 'manage-branch']);

// 4. Create roles with different scopes
$ownerRole = Role::createOwnerRole($tenant);
$ownerRole->givePermissionTo($viewReports);
$ownerRole->givePermissionTo($editUsers);
$ownerRole->givePermissionTo($manageBranch);

$managerRole = Role::createForBranch('manager', $mainBranch);
$managerRole->givePermissionTo($viewReports);
$managerRole->givePermissionTo($manageBranch);

$staffRole = Role::createForBranch('staff', $darBranch);
$staffRole->givePermissionTo($viewReports);

// 5. Assign users
$owner = User::find(1);
$manager = User::find(2);
$staff = User::find(3);

$owner->assignRoleToTenant($ownerRole, $tenant);
$manager->assignRoleToBranch($managerRole, $mainBranch);
$staff->assignRoleToBranch($staffRole, $darBranch);

// 6. Test permissions
// Owner can access all branches
assert($owner->hasPermissionInTenantOrBranch('view-reports', $tenant, $mainBranch) === true);
assert($owner->hasPermissionInTenantOrBranch('view-reports', $tenant, $darBranch) === true);

// Manager can only access their branch
assert($manager->hasPermissionInBranch('view-reports', $mainBranch) === true);
assert($manager->hasPermissionInBranch('view-reports', $darBranch) === false);

// Staff can only access their branch
assert($staff->hasPermissionInBranch('view-reports', $darBranch) === true);
assert($staff->hasPermissionInBranch('view-reports', $mainBranch) === false);

Migration

  • The package provides migrations for branches, branch_user, and updates the roles table to include branch_id.

Middleware

  • You can extend the middleware to check for branch-level permissions as needed.