Skip to content

Spatie Permission Integration Guide ​

Last updated: 2026-03-22

Table of Contents ​

  1. Overview
  2. Why Use Spatie Permission?
  3. Installation
  4. Quick Start
  5. Permission Names
  6. Creating Roles
  7. Common Scenarios
  8. Integration Patterns
  9. Filament Shield
  10. Combining with Config
  11. Troubleshooting
  12. Best Practices

Overview ​

Filament Address Pro automatically detects and integrates with Spatie Laravel Permission when installed. No configuration needed - it just works! πŸŽ‰

How It Works ​

The AddressPolicy checks if your User model has the hasPermissionTo() method (provided by Spatie's HasRoles trait). If found, it uses Spatie permissions. If not, it falls back to config-based authorization.

Detection Logic:

php
// In AddressPolicy
if (method_exists($user, 'hasPermissionTo')) {
    return $user->hasPermissionTo('view-addresses');
}

// Fallback to config
return config('addresses.authorization.view_any', true);

Zero Configuration ​

No setup required in config/addresses.php - the integration is automatic! Just install Spatie, create permissions, and assign them to users.


Why Use Spatie Permission? ​

When to Use Spatie ​

βœ… Use Spatie Permission when:

  • You have 3+ roles with different permissions
  • You need dynamic permission management (assign/revoke at runtime)
  • You're building a multi-user application with complex access control
  • You want a database-driven permission system
  • You need permission checking in multiple places (controllers, Livewire, etc.)

When NOT to Use Spatie ​

❌ Stick with config-based auth when:

  • You have a simple 2-role system (admin/staff)
  • Permissions are static and rarely change
  • You want everything in code/config files
  • You have a single-user or small team application

Comparison ​

FeatureSpatie PermissionConfig-Based Auth
Setup ComplexityMediumLow
Runtime FlexibilityHighLow
Role ManagementDatabaseCode
Permission ChangesNo deployment neededRequires deployment
Ideal ForMulti-user appsSimple apps
PerformanceExtra DB queriesFast (config cache)

Installation ​

Step 1: Install Spatie Permission ​

bash
composer require spatie/laravel-permission

Step 2: Publish Configuration & Migrations ​

bash
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

This creates:

  • config/permission.php - Spatie configuration
  • Migration files for permissions and roles tables

Step 3: Run Migrations ​

bash
php artisan migrate

This creates:

  • permissions table
  • roles table
  • model_has_permissions table
  • model_has_roles table
  • role_has_permissions table

Step 4: Add HasRoles Trait to User Model ​

php
// app/Models/User.php
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;  // ← Add this trait

    // ... rest of your model
}

That's it! The package will now automatically use Spatie Permission. πŸŽ‰


Quick Start ​

Run the provided seeder to create all standard permissions:

bash
php artisan db:seed --class="Viewflex\FilamentAddress\Database\Seeders\AddressPermissionsSeeder"

The seeder also contains commented-out examples for creating roles and assigning permissions. To customize it (uncomment and edit the examples), publish it first:

bash
php artisan vendor:publish --tag=filament-address-seeders

Then edit database/seeders/AddressPermissionsSeeder.php and run it from your app:

bash
php artisan db:seed --class="Database\Seeders\AddressPermissionsSeeder"

This creates 7 permissions:

  • view-addresses
  • create-addresses
  • update-addresses
  • delete-addresses
  • verify-addresses
  • import-addresses
  • export-addresses

Then create roles and assign permissions (see Creating Roles below).

Method 2: Create Permissions Manually ​

php
use Spatie\Permission\Models\Permission;

Permission::create(['name' => 'view-addresses']);
Permission::create(['name' => 'create-addresses']);
Permission::create(['name' => 'update-addresses']);
Permission::create(['name' => 'delete-addresses']);
Permission::create(['name' => 'verify-addresses']);
Permission::create(['name' => 'import-addresses']);
Permission::create(['name' => 'export-addresses']);

Enable Authorization ​

env
# .env
ADDRESS_AUTHORIZATION_ENABLED=true

Or in config/addresses.php:

php
'authorization' => [
    'enabled' => true,
],

Register Entity Types (required for standalone AddressResource) ​

If you are using the standalone Addresses resource (not just AddressesRelationManager), you must tell it which models can own addresses:

php
// config/addresses.php
'import' => [
    'allowed_entity_types' => [
        'App\Models\User',
        'App\Models\Customer',
    ],
],

Without this, the create form will show an empty "Belongs To" dropdown.


Permission Names ​

Standard Permissions ​

The package uses these standardized permission names:

PermissionCRUD ActionBulk OperationsDescription
view-addressesviewAny(), view()-View address list and details
create-addressescreate(), replicate()Recalculate Quality Scores (header + bulk)Create new addresses; also gates quality score recalculation
update-addressesupdate(), reorder()-Edit existing addresses
delete-addressesdelete(), restore(), forceDelete()Delete Selection (bulk)Delete addresses
verify-addresses-bulkVerify()Bulk verification operations
import-addresses-import()Import addresses from files
export-addresses-export()Export addresses to files

Note: The "Recalculate Quality Scores" actions (both the header button and the bulk table action) are gated behind create-addresses rather than update-addresses. This is because they operate at the class level with no specific record instance β€” update requires a model instance and cannot be used for class-level checks. A viewer role (view-addresses only) will not see these actions.

Permission Naming Convention ​

Format: {action}-addresses

  • Always plural: addresses (not address)
  • Always lowercase
  • Hyphen-separated (not underscore or camelCase)

Custom Permission Names ​

You can't change permission names without copying and customizing the policy:

bash
php artisan vendor:publish --tag=filament-address-policies

Then modify app/Policies/AddressPolicy.php to use your custom names.


Creating Roles ​

Example 1: Admin Role (Full Access) ​

php
use Spatie\Permission\Models\Role;

$admin = Role::create(['name' => 'admin']);

$admin->givePermissionTo([
    'view-addresses',
    'create-addresses',
    'update-addresses',
    'delete-addresses',
    'verify-addresses',
    'import-addresses',
    'export-addresses',
]);

// Assign to user
$user->assignRole('admin');

Example 2: Staff Role (Limited Access) ​

php
$staff = Role::create(['name' => 'staff']);

$staff->givePermissionTo([
    'view-addresses',
    'create-addresses',
    'update-addresses',  // Can edit, but not delete
]);

$user->assignRole('staff');

Example 3: Viewer Role (Read-Only) ​

php
$viewer = Role::create(['name' => 'viewer']);

$viewer->givePermissionTo([
    'view-addresses',  // Only view permission
]);

$user->assignRole('viewer');

Example 4: Data Manager Role (Import/Export) ​

php
$dataManager = Role::create(['name' => 'data-manager']);

$dataManager->givePermissionTo([
    'view-addresses',
    'import-addresses',
    'export-addresses',
]);

$user->assignRole('data-manager');

Example 5: Multiple Roles per User ​

php
// User can have multiple roles
$user->assignRole(['staff', 'data-manager']);

// User will have combined permissions from both roles
$user->hasPermissionTo('view-addresses');    // true (from staff)
$user->hasPermissionTo('import-addresses');  // true (from data-manager)

Common Scenarios ​

Scenario 1: Small Team (3 Users) ​

php
// Owner (full access)
$owner = User::find(1);
$owner->assignRole('admin');

// Employee 1 (can view and create)
$employee1 = User::find(2);
$employee1->assignRole('staff');

// Employee 2 (read-only)
$employee2 = User::find(3);
$employee2->assignRole('viewer');

Scenario 2: Department-Based Access ​

php
// Sales department
$salesRole = Role::create(['name' => 'sales']);
$salesRole->givePermissionTo(['view-addresses', 'create-addresses', 'update-addresses']);

// Finance department (export only)
$financeRole = Role::create(['name' => 'finance']);
$financeRole->givePermissionTo(['view-addresses', 'export-addresses']);

// IT department (import/export)
$itRole = Role::create(['name' => 'it']);
$itRole->givePermissionTo(['view-addresses', 'import-addresses', 'export-addresses']);

// Assign users to departments
$salesPerson->assignRole('sales');
$accountant->assignRole('finance');
$dataAnalyst->assignRole('it');

Scenario 3: Hierarchical Permissions ​

php
// Super Admin (all permissions)
$superAdmin = Role::create(['name' => 'super-admin']);
$superAdmin->givePermissionTo(Permission::all());

// Manager (most permissions)
$manager = Role::create(['name' => 'manager']);
$manager->givePermissionTo([
    'view-addresses',
    'create-addresses',
    'update-addresses',
    'verify-addresses',
    'export-addresses',
]);

// Staff (basic permissions)
$staff = Role::create(['name' => 'staff']);
$staff->givePermissionTo([
    'view-addresses',
    'create-addresses',
]);

Scenario 4: Direct Permission Assignment (No Roles) ​

php
// Grant permission directly to a user
$user->givePermissionTo('view-addresses');
$user->givePermissionTo('create-addresses');

// Revoke permission
$user->revokePermissionTo('create-addresses');

// Check permission
if ($user->hasPermissionTo('view-addresses')) {
    // User can view addresses
}

Integration Patterns ​

Pattern 1: Using Existing Spatie Setup ​

If you already use Spatie Permission in your app, the package will automatically integrate with your existing roles and permissions.

Just add the address permissions to your existing roles:

php
// Add to existing admin role
$adminRole = Role::findByName('admin');
$adminRole->givePermissionTo([
    'view-addresses',
    'create-addresses',
    // ... etc
]);

Pattern 2: Separate Address Roles ​

Create dedicated roles just for address management:

php
// Address-specific roles
Role::create(['name' => 'address-admin']);
Role::create(['name' => 'address-editor']);
Role::create(['name' => 'address-viewer']);

// Users can have both app roles and address roles
$user->assignRole(['staff', 'address-editor']);

Pattern 3: Guard-Specific Permissions ​

If you use multiple guards (e.g., web and admin):

php
// Create permissions for specific guard
Permission::create([
    'name' => 'view-addresses',
    'guard_name' => 'admin',  // Specify guard
]);

// When checking permissions, Spatie automatically uses the correct guard
// based on the authenticated user's guard

Filament Shield ​

Filament Shield (bezhansalleh/filament-shield) is a popular companion plugin that auto-generates Filament resource permissions and stores them in Spatie's tables. It is widely used alongside Spatie Permission in Filament applications.

How Shield Works ​

Shield scans the resources, pages, and widgets registered in your Filament panel and automatically creates Spatie permissions for each one. It also provides a UI for managing roles and permissions directly in the admin panel.

Naming Convention Difference ​

Shield and this package use different permission naming conventions:

SourceExample permission name
Filament Shield (auto-generated)view_any_address, create_address
Filament Address Proview-addresses, create-addresses

Shield uses underscores and singular resource names. This package uses hyphens and a plural noun (addresses). The two sets of permissions are separate entries in Spatie's permissions table and do not conflict.

Using Shield and This Package Together ​

If you already have Shield installed, you have two options:

Option A: Manage our permissions separately (recommended)

Create the 7 package permissions manually (or via the seeder) and assign them to your existing Shield-managed roles:

php
// Add package permissions to your existing admin role
$adminRole = Role::findByName('admin');
$adminRole->givePermissionTo([
    'view-addresses',
    'create-addresses',
    'update-addresses',
    'delete-addresses',
    'verify-addresses',
    'import-addresses',
    'export-addresses',
]);

Option B: Use Shield's naming convention

Copy the AddressPolicy and change the permission names to match what Shield would generate:

bash
php artisan vendor:publish --tag=filament-address-policies

Then update each hasPermissionTo() call in app/Policies/AddressPolicy.php to use Shield's naming format (e.g. view_any_address instead of view-addresses).

Shield Does Not Auto-Generate Our Permissions ​

Shield generates permissions for resources registered in your application's panel. Whether it generates permissions for AddressResource depends on how the package's resources are registered in your panel. Even if it does, the generated names will follow Shield's convention, not ours, so the policy's hasPermissionTo() checks will not match automatically.

Bottom line: If you use Shield, choose Option A (manage our permissions separately alongside Shield's permissions) unless you want to customize the policy.


Combining with Config ​

You can mix Spatie permissions with config-based authorization for fine-grained control.

Override Spatie with Config ​

Publish the policy and customize:

php
// app/Policies/AddressPolicy.php
public function update(User $user, Address $address): bool
{
    // Check Spatie permission first
    if (method_exists($user, 'hasPermissionTo')) {
        $hasPermission = $user->hasPermissionTo('update-addresses');

        // Additional check: only edit own addresses
        return $hasPermission && ($address->created_by === $user->id || $user->hasRole('admin'));
    }

    // Fallback to config
    return config('addresses.authorization.update', true);
}

Conditional Logic ​

php
public function bulkVerify(User $user): bool
{
    if (method_exists($user, 'hasPermissionTo')) {
        // Has permission, but also check quota
        $hasPermission = $user->hasPermissionTo('verify-addresses');
        $hasQuota = $user->api_quota_remaining > 0;

        return $hasPermission && $hasQuota;
    }

    return config('addresses.authorization.bulk_verify', true);
}

Troubleshooting ​

Problem: Permission Not Found ​

Error: Spatie\Permission\Exceptions\PermissionDoesNotExist

Cause: Permission hasn't been created.

Solution:

bash
# Run the seeder
php artisan db:seed --class="Viewflex\FilamentAddress\Database\Seeders\AddressPermissionsSeeder"

# OR create manually
php artisan tinker
>>> Permission::create(['name' => 'view-addresses']);

Problem: User Still Can't Access After Assigning Role ​

Possible Causes:

  1. Authorization not enabled in config
  2. Role doesn't have the required permission
  3. Cache issue

Solution:

php
// Check if authorization is enabled
config('addresses.authorization.enabled');  // Should be true

// Check user's roles and permissions
$user->roles;
$user->permissions;
$user->hasPermissionTo('view-addresses');

// Clear cache
php artisan cache:clear
php artisan config:clear
php artisan permission:cache-reset  // Spatie-specific cache

Problem: HasRoles Trait Not Found ​

Error: Trait 'Spatie\Permission\Traits\HasRoles' not found

Cause: Spatie Permission not installed or User model doesn't use the trait.

Solution:

bash
# Install Spatie
composer require spatie/laravel-permission

# Add trait to User model
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}

Problem: Permissions Not Working in Production ​

Cause: Permission cache not refreshed after changes.

Solution:

bash
# Reset permission cache
php artisan permission:cache-reset

# Or clear all caches
php artisan optimize:clear

Problem: Multiple Guards Causing Issues ​

Symptom: Permissions work in one guard but not another.

Solution:

php
// Create permissions for both guards
Permission::create(['name' => 'view-addresses', 'guard_name' => 'web']);
Permission::create(['name' => 'view-addresses', 'guard_name' => 'admin']);

// Assign to role with correct guard
$webAdmin = Role::create(['name' => 'admin', 'guard_name' => 'web']);
$adminAdmin = Role::create(['name' => 'admin', 'guard_name' => 'admin']);

Best Practices ​

1. Use Descriptive Role Names ​

php
// Good
Role::create(['name' => 'address-data-manager']);
Role::create(['name' => 'address-viewer']);

// Avoid
Role::create(['name' => 'role1']);
Role::create(['name' => 'user']);

2. Document Your Permission Structure ​

Create a PERMISSIONS.md file documenting your roles and permissions:

markdown
# Permission Structure

## Roles

### admin
- All permissions

### staff
- view-addresses
- create-addresses
- update-addresses

### viewer
- view-addresses

3. Seed Roles in Production Setup ​

php
// database/seeders/ProductionRolesSeeder.php
public function run()
{
    // Create roles only if they don't exist
    $admin = Role::firstOrCreate(['name' => 'admin']);
    $staff = Role::firstOrCreate(['name' => 'staff']);

    // Assign permissions
    $admin->syncPermissions(Permission::all());
    $staff->syncPermissions(['view-addresses', 'create-addresses']);
}

4. Use Role Middleware in Routes ​

php
// routes/web.php
Route::middleware(['auth', 'role:admin'])->group(function () {
    // Admin-only routes
});

5. Cache Permissions in Production ​

bash
# After deploying or changing permissions
php artisan permission:cache-reset

This caches permissions for better performance.

6. Test Permission Logic ​

php
// tests/Feature/AddressAuthorizationTest.php
test('staff cannot delete addresses', function () {
    $staff = User::factory()->create();
    $staff->assignRole('staff');

    $address = Address::factory()->create();

    $this->actingAs($staff);

    expect(Gate::denies('delete', $address))->toBeTrue();
});

7. Audit Permission Changes ​

Log when permissions are granted/revoked:

php
// In a model observer or event listener
Permission::created(function ($permission) {
    Log::info('Permission created', ['name' => $permission->name]);
});

Role::pivotAttached(function ($model, $relationName, $pivotIds) {
    Log::info('Role attached to user', [
        'user_id' => $model->id,
        'role' => $relationName,
    ]);
});

Additional Resources ​

Released under a commercial license.