A user should be able to edit their own post but not someone else's; an editor should be able to delete comments while a regular member cannot. Laravel authorization lets you collect exactly these kinds of rules in one place, without polluting your business logic. In this guide we'll walk through when to use a Policy versus a Gate, how to build model-based access control, and how to apply it across your controllers, Blade and API layers.
Authentication and authorization are not the same
Let's separate the two ideas first. Authentication answers "who are you?"; it's handled by the login screen, the password and the session. Authorization answers "are you allowed to do this?". A user may be logged in and still have no right to view another user's invoice. Laravel offers two main tools for authorization: Gate and Policy.
When to use a Gate, when to use a Policy
Both use the same engine, but their use cases differ:
- Gate — ideal for simple, broad permissions that aren't tied to a specific model. For example "access the admin panel" or "change site settings". Gates are closure-based and usually defined in
AppServiceProvider. - Policy — gathers permissions that revolve around a single Eloquent model (view, create, update, delete) into one class. It's the right choice for the CRUD authorization of models such as
Post,InvoiceorProject.
A practical rule: if the permission is tied to a model row, use a Policy; if not, use a Gate.
Defining a simple permission with a Gate
You can define a rule that isn't tied to a model directly as a Gate. The first parameter of the closure is always the authenticated user:
use Illuminate\Support\Facades\Gate;
use App\Models\User;
public function boot(): void
{
Gate::define('access-admin', function (User $user) {
return $user->is_admin;
});
}
To check it, you use Gate::allows() or Gate::denies():
if (Gate::allows('access-admin')) {
// show the panel content
}
Creating a Policy for model-based access control
The real power lies in Policies. Generating one for a model is a single command:
php artisan make:policy PostPolicy --model=Post
The --model flag scaffolds the standard methods such as view, create, update and delete. You then fill them in according to your business rules:
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
From Laravel 11 onwards, policies are auto-discovered as long as you follow the naming convention (App\Models\Post → App\Policies\PostPolicy); no extra registration is required. If you need a different mapping, you can bind it manually with Gate::policy(Post::class, PostPolicy::class).
Using a Policy in a controller
Inside a controller, the cleanest approach is the authorize method. If the rule isn't satisfied, Laravel automatically throws a 403 response:
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}
If you want to map all CRUD methods in a single line, you can use authorizeResource in the controller's constructor; it automatically binds resource controller methods to policy methods:
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
Applying it in Blade and the API layer
Authorization shouldn't stay only in the controller. To hide a button from a user who lacks permission in Blade, use the @can directive:
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
To protect routes in bulk for API and form requests, the can middleware does the job:
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
You can also check through a user object: $user->can('update', $post). This is handy for sending a "can edit?" flag to the frontend in a JSON response.
The before hook and common mistakes
If you want administrators to be able to do everything, add a before method to the policy. It runs before all other checks and, if it returns true, skips the rest:
public function before(User $user, string $ability): ?bool
{
return $user->is_admin ? true : null;
}
Returning null here is critical: if you return false, the other methods never run and you block everyone who isn't an admin. Other common mistakes: forgetting to make the parameter nullable as ?User $user for guest (unauthenticated) users, and confusing authorize with can — the former throws an exception while the latter returns a boolean.
Frequently Asked Questions
Should I use a Policy or a Gate?
If the permission is tied to a specific Eloquent model row (for example "edit this post"), use a Policy. For general permissions that aren't tied to a model (for example "access the panel"), a Gate stays simpler.
Do I need to register policies manually?
For Laravel 11+, no; they are auto-discovered as long as you follow the naming convention. You only need Gate::policy() when you require a different model-to-policy mapping.
Can I return a custom message instead of a plain 403?
Yes. By returning Illuminate\Auth\Access\Response::deny('message') from a policy method you can provide custom error text and a status code, and use Response::allow() instead of true.
Getting authorization right from the start keeps your project safe as it grows. To set up clean, maintainable access control in your Laravel project, get in touch with me.