Spatie Permission Integration Guide β
Last updated: 2026-03-22
Table of Contents β
- Overview
- Why Use Spatie Permission?
- Installation
- Quick Start
- Permission Names
- Creating Roles
- Common Scenarios
- Integration Patterns
- Filament Shield
- Combining with Config
- Troubleshooting
- 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:
// 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 β
| Feature | Spatie Permission | Config-Based Auth |
|---|---|---|
| Setup Complexity | Medium | Low |
| Runtime Flexibility | High | Low |
| Role Management | Database | Code |
| Permission Changes | No deployment needed | Requires deployment |
| Ideal For | Multi-user apps | Simple apps |
| Performance | Extra DB queries | Fast (config cache) |
Installation β
Step 1: Install Spatie Permission β
composer require spatie/laravel-permissionStep 2: Publish Configuration & Migrations β
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 β
php artisan migrateThis creates:
permissionstablerolestablemodel_has_permissionstablemodel_has_rolestablerole_has_permissionstable
Step 4: Add HasRoles Trait to User Model β
// 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 β
Method 1: Use the Optional Seeder (Recommended) β
Run the provided seeder to create all standard permissions:
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:
php artisan vendor:publish --tag=filament-address-seedersThen edit database/seeders/AddressPermissionsSeeder.php and run it from your app:
php artisan db:seed --class="Database\Seeders\AddressPermissionsSeeder"This creates 7 permissions:
view-addressescreate-addressesupdate-addressesdelete-addressesverify-addressesimport-addressesexport-addresses
Then create roles and assign permissions (see Creating Roles below).
Method 2: Create Permissions Manually β
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
ADDRESS_AUTHORIZATION_ENABLED=trueOr in config/addresses.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:
// 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:
| Permission | CRUD Action | Bulk Operations | Description |
|---|---|---|---|
view-addresses | viewAny(), view() | - | View address list and details |
create-addresses | create(), replicate() | Recalculate Quality Scores (header + bulk) | Create new addresses; also gates quality score recalculation |
update-addresses | update(), reorder() | - | Edit existing addresses |
delete-addresses | delete(), 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-addressesrather thanupdate-addresses. This is because they operate at the class level with no specific record instance βupdaterequires a model instance and cannot be used for class-level checks. A viewer role (view-addressesonly) will not see these actions.
Permission Naming Convention β
Format: {action}-addresses
- Always plural:
addresses(notaddress) - Always lowercase
- Hyphen-separated (not underscore or camelCase)
Custom Permission Names β
You can't change permission names without copying and customizing the policy:
php artisan vendor:publish --tag=filament-address-policiesThen modify app/Policies/AddressPolicy.php to use your custom names.
Creating Roles β
Example 1: Admin Role (Full Access) β
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) β
$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) β
$viewer = Role::create(['name' => 'viewer']);
$viewer->givePermissionTo([
'view-addresses', // Only view permission
]);
$user->assignRole('viewer');Example 4: Data Manager Role (Import/Export) β
$dataManager = Role::create(['name' => 'data-manager']);
$dataManager->givePermissionTo([
'view-addresses',
'import-addresses',
'export-addresses',
]);
$user->assignRole('data-manager');Example 5: Multiple Roles per User β
// 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) β
// 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 β
// 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 β
// 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) β
// 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:
// 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:
// 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):
// 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 guardFilament 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:
| Source | Example permission name |
|---|---|
| Filament Shield (auto-generated) | view_any_address, create_address |
| Filament Address Pro | view-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:
// 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:
php artisan vendor:publish --tag=filament-address-policiesThen 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:
// 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 β
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:
# 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:
- Authorization not enabled in config
- Role doesn't have the required permission
- Cache issue
Solution:
// 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 cacheProblem: 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:
# 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:
# Reset permission cache
php artisan permission:cache-reset
# Or clear all caches
php artisan optimize:clearProblem: Multiple Guards Causing Issues β
Symptom: Permissions work in one guard but not another.
Solution:
// 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 β
// 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:
# Permission Structure
## Roles
### admin
- All permissions
### staff
- view-addresses
- create-addresses
- update-addresses
### viewer
- view-addresses3. Seed Roles in Production Setup β
// 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 β
// routes/web.php
Route::middleware(['auth', 'role:admin'])->group(function () {
// Admin-only routes
});5. Cache Permissions in Production β
# After deploying or changing permissions
php artisan permission:cache-resetThis caches permissions for better performance.
6. Test Permission Logic β
// 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:
// 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,
]);
});