Laravel policies on a Vue page

A policy decides whether this user can update this post. The Vue page receives that boolean. The controller still calls authorize.

For your AI agent

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

Read https://laravel-vuejs.space/laravel-vue-policies.md
A policy decides whether this user can update this post. The Vue page receives that boolean. The controller still calls authorize.
Do not take userid from the request body.
Do not ship every user's id to the browser and decide there.
Do not check the role in the Vue page.
Show the diff. Do not commit.

Next: Test a Laravel and Vue app

On this page · 8 sections
  1. The column
  2. The policy
  3. A test
  4. Where this stops
  5. Recap
  6. FAQ
  7. Next steps
  8. Sources

“Only the author can edit this post” sounds like one v-if. It is three pieces that have to agree: an owner column, a policy, and a controller that calls it. Here they are, plus the boolean that keeps the Vue link honest.

TL;DR

  • Add user_id, set it from $request->user(), never from the request body.
  • PostPolicy::update returns $user->id === $post->user_id; Laravel discovers it by name.
  • The controller calls $this->authorize('update', $post); the page gets canUpdate.
  • Test with assertForbidden() on the endpoint.

What you'll learn

  • How to add an owner column safely
  • How to write and call a policy
  • How to pass the same answer to the Vue page and test it

Who it's for: Laravel + Vue developers adding per-record authorization

Applies to: Laravel 13 · Inertia 3 · Vue 3 · Pest

The posts in the setup guide have no owner. A policy needs one. This page adds user_id, then allows update only for that user. A permission such as “editor” is a different question and stays in permissions. The screen is still the CRUD guide. The app is Inertia, a SPA, or Nuxt.

The column#

database/migrations/xxxx_add_user_id_to_posts_table.phpphp
Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
});
app/Models/Post.phpphp
public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}

Gotcha: user_id never comes from the form

Add user_id to $fillable, and set it in store from the logged-in user. Do not take user_id from the request body. The form would then let anyone assign the post to someone else.

app/Http/Controllers/PostController.phpphp
Post::create([
    ...$data,
    'user_id' => $request->user()->id,
    'slug' => str($data['title'])->slug(),
    'published_at' => now(),
]);

The policy#

Laravel discovers PostPolicy for Post when the names match. You do not register it by hand.

bash
php artisan make:policy PostPolicy --model=Post
app/Policies/PostPolicy.phpphp
public function update(User $user, Post $post): bool
{
    return $user->id === $post->user_id;
}

Gotcha: an empty method returns false

update and destroy can share that line. viewAny can return true while the index is still a list of every post. Write the method you mean. An empty policy method that falls through to false hides every button and then looks like a Vue bug.

Call it from the controller. authorize throws 403 before the update runs.

php
public function update(Request $request, Post $post): RedirectResponse
{
    $this->authorize('update', $post);

    // validation from the CRUD guide
}

Pass the same answer to the page. The link and the endpoint then agree.

The page guesses, then The policy decides:

resources/js/pages/posts/Form.vuevue
<Link v-if="post.user_id === $page.props.auth.user.id" :href="`/posts/${post.id}/edit`">Edit</Link>
resources/js/pages/posts/Form.vuevue
<Link v-if="canUpdate" :href="`/posts/${post.id}/edit`">Edit</Link>
php
return Inertia::render('posts/Form', [
    'post' => $post->only(['id', 'title', 'excerpt']),
    'canUpdate' => $request->user()?->can('update', $post) ?? false,
]);
vue
<Link v-if="canUpdate" :href="`/posts/${post.id}/edit`">Edit</Link>

Tip: per-row booleans on the index

On the index, that boolean is per row. Compute it in the query’s map or share a list of ids the user can edit. Do not ship every user’s id to the browser and decide there.

A test#

Prove the endpoint is closed

tests/Feature/PostPolicyTest.phpphp
it('forbids a user who does not own the post', function () {
    $owner = User::factory()->create();
    $other = User::factory()->create();
    $post = Post::factory()->for($owner)->create();

    $this->actingAs($other)
        ->put("/posts/{$post->id}", [
            'title' => 'Taken',
            'excerpt' => 'Not yours.',
        ])
        ->assertForbidden();
});

The factory needs user_id once the column exists. assertForbidden is the assertion that matters. A test that only checks the missing link does not close the endpoint.

Where this stops#

Spatie permissions and this policy can both exist. An editor permission can allow update even when the user is not the owner, inside the policy, with $user->can('posts.update'). Put that combination in the policy so there is still one method the controller calls. Do not check the role in the Vue page.

Recap#

  • Owner column from the logged-in user, never from input.
  • One policy method per ability; the controller calls it.
  • The page receives the policy’s answer as a boolean.
  • Combine permissions inside the policy, not in Vue.

FAQ#

How do I hide the Edit button for users who do not own a post in Inertia?

Compute $request->user()?->can('update', $post) in the controller, pass it as canUpdate, and use v-if. The controller still calls authorize.

Do I need to register a Laravel policy?

No. Laravel discovers App\\Policies\\PostPolicy for App\\Models\\Post when the names match.

Can an editor role override the owner check?

Yes. Inside the policy, return true when $user->can('posts.update') or the user owns the post, so the controller still calls one method.

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.