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
HasRolestrait to the existingUsermodel. - Share
can.updateas a boolean, not the list of permission names. v-ifis UX;abort_unless(...can(...), 403)in the controller is the authorization.- Rules about a specific post belong in a policy.
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#
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrateAdd the trait to the user model. The starter kit’s User already exists. Do not replace the class.
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.
$user->givePermissionTo('posts.update');Share a boolean#
Pass the result of can(), not the list of every permission name.
return Inertia::render('posts/Index', [
'can' => [
'update' => $request->user()?->can('posts.update') ?? false,
],
'posts' => Post::query()->latest('published_at')->paginate(10)->withQueryString(),
]);<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.
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
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;
HasRolesgoes on the existingUser. - 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.



