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.

For your AI agent

Copies a prompt. Paste it into the AI agent you already have open.

Read https://laravel-vuejs.space/laravel-vue-permissions.md
Spatie stores the permission. The controller still checks it. The Vue page only receives a boolean, so a hidden button is not the authorization.
Do not encode that rule as another Spatie string.
Show the diff. Do not commit.

Next: Breeze, Jetstream, and the Vue starter kit

On this page · 7 sections
  1. Install
  2. Share a boolean
  3. Where this stops
  4. Recap
  5. FAQ
  6. Next steps
  7. Sources

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. 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. The app around them is Inertia, a SPA, or Nuxt.

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.phpphp
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 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#

Is v-if enough to protect a Laravel route?

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

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.

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#

Sources#

Found this useful?

Ask questions, share what you built.

Join thousands of Laravel and Vue developers in our Facebook group, or get new posts in your Telegram feed. Sharing this article helps too.

Keep reading

Newsletter

Get the best of Laravel & Vue, once a month.

One short email with new guides, release notes worth reading and hand-picked community links. No spam, unsubscribe any time.