Roles & Permissions
Manage authorization using roles and permissions.
Inertia Start uses spatie/laravel-permission to manage roles and permissions. The package integrates with Laravel's authorization system, so all standard Laravel features (gates, policies, can(), @can()) work as expected.
Built-In Roles
Out of the box, Inertia Start ships with two roles:
| Role | Description |
|---|---|
| Super Admin | Full application access. Bypasses all authorization gates automatically. |
| Admin | Access to the admin area. Subject to normal authorization checks. |
Both roles grant access to the admin panel. The distinction is that Super Admins bypass every gate check in the application (except team-scoped gates on the teams branch), making them true "god mode" administrators.
How Admin Access Works
The is_admin Attribute
The User model exposes a computed is_admin boolean attribute that returns true if the user holds either the Super Admin or Admin role:
protected function isAdmin(): Attribute
{
return Attribute::get(
fn () => $this->hasRole(['Super Admin', 'Admin'])
);
}The IsAdmin Middleware
Admin routes are protected by the IsAdmin middleware, registered under the admin alias:
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && $request->user()->is_admin) {
return $next($request);
}
throw new AuthorizationException;
}All routes in routes/admin.php use this middleware:
Route::middleware(['auth', 'verified', 'admin'])->prefix('admin')->group(function () {
Route::get('dashboard', [DashboardController::class, 'index'])->name('admin.dashboard');
// ...
});The Super Admin Gate Bypass
AppServiceProvider registers a Gate::before callback that short-circuits every gate check for Super Admins:
Gate::before(function ($user, $ability) {
return $user->hasRole('Super Admin') ? true : null;
});Returning true grants the ability unconditionally. Returning null falls through to the next gate check, so regular users still go through normal authorization.
Frontend Access
The HandleInertiaRequests middleware shares auth.isAdmin as an Inertia prop, derived from $user->is_admin. Vue components use this value to conditionally render admin UI elements (e.g., navigation links to the admin panel). All actual access control happens server-side.
Initial Super Admin Setup
The first Super Admin is created through the setup wizard at /setup. The SuperAdminSetupController creates the user, creates the Super Admin role if it doesn't exist, and assigns it:
$role = Role::firstOrCreate(['name' => 'Super Admin']);
$user->assignRole($role);Creating Additional Roles and Permissions
Creating Roles
Use Role::create() or Role::firstOrCreate() (typically in a seeder or a one-time Artisan command):
use Spatie\Permission\Models\Role;
Role::firstOrCreate(['name' => 'Editor']);
Role::firstOrCreate(['name' => 'Moderator']);Creating Permissions
use Spatie\Permission\Models\Permission;
Permission::firstOrCreate(['name' => 'edit posts']);
Permission::firstOrCreate(['name' => 'delete posts']);
Permission::firstOrCreate(['name' => 'publish posts']);Assigning Permissions to Roles
$editor = Role::findByName('Editor');
$editor->givePermissionTo(['edit posts', 'publish posts']);
$moderator = Role::findByName('Moderator');
$moderator->givePermissionTo('delete posts');You can also sync permissions (replaces the existing set):
$editor->syncPermissions(['edit posts', 'publish posts']);Assigning Roles to Users
$user->assignRole('Editor');
$user->assignRole(['Editor', 'Moderator']); // multiple rolesChecking Roles and Permissions
The User model uses the HasRoles trait, which provides these methods:
// Roles
$user->hasRole('Editor');
$user->hasAnyRole(['Editor', 'Moderator']);
$user->hasAllRoles(['Editor', 'Moderator']);
// Permissions (direct or inherited from a role)
$user->can('edit posts');
$user->hasPermissionTo('edit posts');
$user->hasAnyPermission(['edit posts', 'delete posts']);Seeding Roles and Permissions
Create a dedicated seeder to keep your role/permission setup reproducible:
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RolesAndPermissionsSeeder extends Seeder
{
public function run(): void
{
app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
Permission::firstOrCreate(['name' => 'edit posts']);
Permission::firstOrCreate(['name' => 'delete posts']);
Permission::firstOrCreate(['name' => 'publish posts']);
Role::firstOrCreate(['name' => 'Editor'])
->givePermissionTo(['edit posts', 'publish posts']);
Role::firstOrCreate(['name' => 'Moderator'])
->givePermissionTo('delete posts');
}
}Run it with:
php artisan db:seed --class=RolesAndPermissionsSeederPermission Cache
Spatie caches permissions for performance. When you modify roles or permissions programmatically, call app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions() at the start of your seeder or command to avoid stale cache issues.
Using Laravel Authorization
Gates
Gates are closures that answer authorization questions. Define them in AppServiceProvider (or any service provider):
use Illuminate\Support\Facades\Gate;
Gate::define('publish-post', function (User $user, Post $post) {
return $user->hasPermissionTo('publish posts');
});Check a gate anywhere in the app:
// In a controller
Gate::authorize('publish-post', $post); // throws AuthorizationException if denied
$user->can('publish-post', $post); // returns bool
// In Blade (works in Inertia too via controller)
// @can('publish-post', $post)Policies
Policies group authorization logic for a model into a dedicated class. Create one with Artisan:
php artisan make:policy PostPolicy --model=PostDefine methods for each action:
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id
|| $user->hasPermissionTo('edit posts');
}
public function delete(User $user, Post $post): bool
{
return $user->hasPermissionTo('delete posts');
}
}Laravel auto-discovers policies in app/Policies/ that match the naming convention (PostPolicy for Post). You can also register them explicitly in a service provider:
Gate::policy(Post::class, PostPolicy::class);Use the policy in a controller:
// Throws AuthorizationException if denied
$this->authorize('update', $post);
// Returns bool
$request->user()->can('update', $post);Form Request Authorization
Authorization can also live in Form Requests via the authorize() method:
class UpdatePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update', $this->route('post'));
}
}Middleware-Based Authorization
Protect individual routes using the can middleware:
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::delete('/posts/{post}', [PostController::class, 'destroy'])
->middleware('can:delete,post');Team Permissions
Team-scoped permissions are only available on the teams branch. See the Teams page for details.
On the teams branch, every role and permission record is scoped to a specific team. The SetTeamPermissions middleware sets the active team context on each request so that can() checks resolve against the correct team's permissions. See the Teams page for the full documentation.
Further Reading
- spatie/laravel-permission documentation: Full reference for roles, permissions, teams, caching, and Blade directives.
- Laravel Authorization documentation: Gates, policies, controller helpers, Blade directives, and middleware.