# Laravel permissions on a Vue page

> Spatie stores the permission. The controller still checks it. The Vue page only receives a boolean, so a hidden button is not the authorization.

- URL: https://laravel-vuejs.space/laravel-vue-permissions
- Author: Anass Ez-zouaine (https://laravel-vuejs.space/author/anass-ez-zouaine)
- Published: 2026-10-10
- Topics: Laravel, VueJs, Permissions

You hid the Edit button with `v-if`, and the feature feels locked down. It isn’t: anyone can still send the request. Here is how to share a Spatie permission with a Vue page as a plain boolean, and keep the real check in the controller.

> **TL;DR**
>
> -   Add the `HasRoles` trait to the existing `User` model.
> -   Share `can.update` as a boolean, not the list of permission names.
> -   `v-if` is UX; `abort_unless(...can(...), 403)` in the controller is the authorization.
> -   Rules about a specific post belong in a policy.
>
> **What you'll learn**
>
> -   How to install spatie/laravel-permission on the starter kit
> -   How to share a permission as a boolean prop
> -   How to enforce it on the server and test it
>
> **Who it's for:** Laravel + Vue developers adding roles and permissions
>
> **Applies to:** Laravel 13 · spatie/laravel-permission · Inertia 3 · Vue 3 · Pest

A permission answers “is this user an editor?”. A policy answers “does this user own this post?”. This page is the first one. The owner check is [policies](https://laravel-vuejs.space/laravel-vue-policies). Hiding an Edit link is not either check. The controller has to refuse the request.

The pages those links sit on are the [CRUD guide](https://laravel-vuejs.space/laravel-inertia-vue-crud). The app around them is [Inertia, a SPA, or Nuxt](https://laravel-vuejs.space/laravel-vue-in-2026).

## Install

```bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
```

Add the trait to the user model. The starter kit’s `User` already exists. Do not replace the class.

```php
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}
```

`HasRoles` is the trait that provides `givePermissionTo`. `$user->can('posts.update')` is Laravel’s own `can`, which passes for that permission once the package’s gate hook is registered. Give the permission from a seeder or a one-off command, not from the Vue page.

```php
$user->givePermissionTo('posts.update');
```

## Share a boolean

Pass the result of `can()`, not the list of every permission name.

```php
return Inertia::render('posts/Index', [
    'can' => [
        'update' => $request->user()?->can('posts.update') ?? false,
    ],
    'posts' => Post::query()->latest('published_at')->paginate(10)->withQueryString(),
]);
```

```vue
<Link v-if="can.update" :href="`/posts/${post.id}/edit`">Edit</Link>
```

> **Gotcha: a hidden button is not authorization**
>
> `v-if` removes the link for people who cannot edit. Anyone can still POST the URL. The `update` method has to check the same ability.

```php
public function update(Request $request, Post $post): RedirectResponse
{
    abort_unless($request->user()?->can('posts.update'), 403);

    // the validation from the CRUD guide stays here
}
```

> **Test the endpoint, not the HTML**
>
> `tests/Feature/PostPermissionTest.php`
> ```php
> it('forbids updates without the permission', function () {
>     $post = Post::factory()->create();
>
>     $this->actingAs(User::factory()->create())
>         ->put("/posts/{$post->id}", ['title' => 'Changed'])
>         ->assertForbidden();
> });
> ```

A Pest test should log in a user without the permission and `assertForbidden()` on `PUT /posts/{id}`. A test that only checks the HTML for a missing link will pass while the endpoint stays open.

## Where this stops

> **Heads up: permission or policy?**
>
> `posts.update` is a global ability. It cannot express “only the author”. When the rule uses the post, write a [policy](https://laravel-vuejs.space/laravel-vue-policies) and pass `$request->user()->can('update', $post)` instead. Do not encode that rule as another Spatie string.

Roles are the same package: `assignRole('editor')`, then `givePermissionTo` on the role. The Vue page still receives a boolean. It does not need the role name.

## Recap

- Spatie stores the permission; `HasRoles` goes on the existing `User`.
- Share booleans, not names.
- Every protected controller method checks the same ability.
- Test the forbidden request.

## FAQ

**Q: Is v-if enough to protect a Laravel route?**

No. It only hides the link. The controller must check the permission and return 403.

**Q: Should I send all permissions to the Vue page?**

No. Send the booleans the page needs, like `can.update`. The page does not need role or permission names.

**Q: When should I use a policy instead of a Spatie permission?**

When the rule depends on the record, such as “only the author may edit this post”. Use `can('update', $post)` with a policy.

## Next steps

- [Laravel policies on a Vue page](https://laravel-vuejs.space/laravel-vue-policies)
- [CRUD with Inertia and Vue](https://laravel-vuejs.space/laravel-inertia-vue-crud)
- [Test a Laravel and Vue app](https://laravel-vuejs.space/test-laravel-vue)

## Sources

- [spatie/laravel-permission documentation](https://spatie.be/docs/laravel-permission)
- [Laravel 13: Authorization](https://laravel.com/docs/13.x/authorization)
- [Inertia v3: Authorization](https://inertiajs.com/docs/v3/security/authorization)
