Skip to main content
Laravel Permission uses five database tables to manage permissions and roles. This guide documents the complete schema structure and relationships.

Running Migrations

Publish and run the migrations:
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate

Table Overview

The package creates five interconnected tables:
TablePurpose
permissionsStores all permission definitions
rolesStores all role definitions
model_has_permissionsLinks models directly to permissions
model_has_rolesLinks models to roles
role_has_permissionsLinks roles to permissions

Table Schemas

permissions Table

Stores all permission definitions in your application.
CREATE TABLE permissions (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    guard_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    UNIQUE KEY (name, guard_name)
);
Columns:
ColumnTypeDescription
idbigint unsignedPrimary key
namevarchar(255)Permission name (e.g., ‘edit articles’)
guard_namevarchar(255)Guard name (e.g., ‘web’, ‘api’)
created_attimestampCreation timestamp
updated_attimestampLast update timestamp
Indexes:
  • Unique composite index on (name, guard_name)
If you encounter “1071 Specified key was too long” errors, reduce the length of name and guard_name columns or use the utf8mb4 charset with the utf8mb4_bin collation.

roles Table

Stores all role definitions in your application.
CREATE TABLE roles (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    team_id BIGINT UNSIGNED NULL,           -- only if teams enabled
    name VARCHAR(255) NOT NULL,
    guard_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    INDEX roles_team_foreign_key_index (team_id),  -- only if teams enabled
    UNIQUE KEY (team_id, name, guard_name)         -- with teams
    -- OR --
    UNIQUE KEY (name, guard_name)                  -- without teams
);
Columns:
ColumnTypeDescription
idbigint unsignedPrimary key
team_idbigint unsignedTeam ID (only with teams feature)
namevarchar(255)Role name (e.g., ‘admin’, ‘editor’)
guard_namevarchar(255)Guard name (e.g., ‘web’, ‘api’)
created_attimestampCreation timestamp
updated_attimestampLast update timestamp
Indexes:
  • Index on team_id (when teams enabled)
  • Unique composite index on (team_id, name, guard_name) with teams
  • Unique composite index on (name, guard_name) without teams

model_has_permissions Table

Pivot table linking models directly to permissions (bypassing roles).
CREATE TABLE model_has_permissions (
    permission_id BIGINT UNSIGNED NOT NULL,
    model_type VARCHAR(255) NOT NULL,
    model_id BIGINT UNSIGNED NOT NULL,
    team_id BIGINT UNSIGNED NULL,           -- only if teams enabled
    PRIMARY KEY (team_id, permission_id, model_id, model_type),  -- with teams
    -- OR --
    PRIMARY KEY (permission_id, model_id, model_type),           -- without teams
    INDEX model_has_permissions_model_id_model_type_index (model_id, model_type),
    INDEX model_has_permissions_team_foreign_key_index (team_id),  -- only if teams enabled
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);
Columns:
ColumnTypeDescription
permission_idbigint unsignedForeign key to permissions table
model_typevarchar(255)Polymorphic model class name
model_idbigint unsignedID of the model (e.g., user ID)
team_idbigint unsignedTeam ID (only with teams feature)
Indexes:
  • Composite index on (model_id, model_type) for efficient lookups
  • Index on team_id (when teams enabled)
  • Foreign key constraint on permission_id
Example Data:
permission_id | model_type      | model_id | team_id
5             | App\Models\User | 123      | 1
7             | App\Models\User | 123      | 1

model_has_roles Table

Pivot table linking models to roles.
CREATE TABLE model_has_roles (
    role_id BIGINT UNSIGNED NOT NULL,
    model_type VARCHAR(255) NOT NULL,
    model_id BIGINT UNSIGNED NOT NULL,
    team_id BIGINT UNSIGNED NULL,           -- only if teams enabled
    PRIMARY KEY (team_id, role_id, model_id, model_type),  -- with teams
    -- OR --
    PRIMARY KEY (role_id, model_id, model_type),           -- without teams
    INDEX model_has_roles_model_id_model_type_index (model_id, model_type),
    INDEX model_has_roles_team_foreign_key_index (team_id),  -- only if teams enabled
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
Columns:
ColumnTypeDescription
role_idbigint unsignedForeign key to roles table
model_typevarchar(255)Polymorphic model class name
model_idbigint unsignedID of the model (e.g., user ID)
team_idbigint unsignedTeam ID (only with teams feature)
Indexes:
  • Composite index on (model_id, model_type) for efficient lookups
  • Index on team_id (when teams enabled)
  • Foreign key constraint on role_id
Example Data:
role_id | model_type      | model_id | team_id
2       | App\Models\User | 123      | 1
3       | App\Models\User | 456      | 2

role_has_permissions Table

Pivot table linking roles to permissions.
CREATE TABLE role_has_permissions (
    permission_id BIGINT UNSIGNED NOT NULL,
    role_id BIGINT UNSIGNED NOT NULL,
    PRIMARY KEY (permission_id, role_id),
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
Columns:
ColumnTypeDescription
permission_idbigint unsignedForeign key to permissions table
role_idbigint unsignedForeign key to roles table
Indexes:
  • Composite primary key on (permission_id, role_id)
  • Foreign key constraints on both columns
Example Data:
permission_id | role_id
1             | 2
3             | 2
4             | 2
1             | 3

Relationship Diagram

┌─────────────┐         ┌──────────────┐         ┌───────┐
│   Models    │────────►│ model_has_   │────────►│ Roles │
│  (e.g. User)│         │    roles     │         └───┬───┘
└──────┬──────┘         └──────────────┘             │
       │                                             │
       │                ┌──────────────┐             │
       └───────────────►│ model_has_   │             │
                        │ permissions  │             │
                        └──────┬───────┘             │
                               │                     │
                               │    ┌────────────┐   │
                               └───►│Permissions │◄──┘
                                    │            │
                                    └────────────┘



                                    ┌────┴────────┐
                                    │role_has_    │
                                    │permissions  │
                                    └─────────────┘

Customizing Table Names

You can customize table names in config/permission.php before running migrations:
'table_names' => [
    'roles' => 'user_roles',
    'permissions' => 'app_permissions',
    'model_has_permissions' => 'user_permissions',
    'model_has_roles' => 'user_role_assignments',
    'role_has_permissions' => 'role_permission_assignments',
],
Customize table names BEFORE running migrations. Changing them after migration requires manual database changes.

Customizing Column Names

You can also customize key column names:
'column_names' => [
    'role_pivot_key' => 'custom_role_id',
    'permission_pivot_key' => 'custom_permission_id',
    'model_morph_key' => 'model_uuid',  // useful for UUID primary keys
    'team_foreign_key' => 'organization_id',
],

Adding Teams Support

If you need to add teams support to an existing installation:
1

Enable teams in config

Set 'teams' => true in config/permission.php
2

Generate migration

Run the setup command:
php artisan permission:setup-teams
3

Review the migration

Check the generated migration in database/migrations/
4

Run migration

php artisan migrate
5

Clear cache

php artisan permission:cache-reset
php artisan config:clear
The teams migration adds team_id columns to:
  • roles
  • model_has_permissions
  • model_has_roles

Database Best Practices

1

Use indexes

The default migration includes all necessary indexes. Don’t remove them as they’re critical for performance.
2

Foreign key constraints

Keep the CASCADE DELETE constraints to automatically clean up related records.
3

Backup before schema changes

Always backup your database before modifying the permission tables structure.
4

Test in staging

Test any schema modifications in a staging environment first.

Querying Tables Directly

While you should typically use the package’s models, you can query tables directly if needed:
use Illuminate\Support\Facades\DB;

// Get all permissions for a user
$permissions = DB::table('model_has_permissions')
    ->join('permissions', 'permissions.id', '=', 'model_has_permissions.permission_id')
    ->where('model_type', 'App\\Models\\User')
    ->where('model_id', $userId)
    ->get();

// Get all users with a specific role
$users = DB::table('model_has_roles')
    ->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
    ->where('roles.name', 'admin')
    ->where('model_type', 'App\\Models\\User')
    ->pluck('model_id');
Direct database queries bypass the permission cache. Use the package’s models and methods for better performance.

Build docs developers (and LLMs) love