inertia start
Features

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:

RoleDescription
Super AdminFull application access. Bypasses all authorization gates automatically.
AdminAccess 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:

app/Models/User.php
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:

app/Http/Middleware/IsAdmin.php
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:

routes/admin.php
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:

app/Providers/AppServiceProvider.php
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 roles

Checking 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:

database/seeders/RolesAndPermissionsSeeder.php
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=RolesAndPermissionsSeeder

Permission 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=Post

Define methods for each action:

app/Policies/PostPolicy.php
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

On this page