Contents
Authorization is where most apps cut corners. Role-based access gets bolted on late — usually after a client asks "can we give our manager access to invoices but not payroll?" — and the answer is a rat's nest of if ($user->role === 'admin') checks scattered across controllers, views, and API routes.
This works until it doesn't. Roles change. Requirements get more specific. And now you're hunting down every role === 'admin' check in a 50-file codebase to add || role === 'manager'.
Laravel Policies are the structured answer to this. They centralize authorization logic, they're testable, and they make it possible to build admin panels where your clients manage their own access — without calling you every time someone changes teams.
Gates vs. Policies: The Quick Version
Laravel has two authorization tools. Gates are simple closures, good for one-off checks that don't belong to a specific model:
Gate::define('view-reports', fn (User $user) => $user->hasPermission('reports'));
Policies are classes that group authorization logic around a model. If the question is "can this user do something to this thing," you want a Policy.
php artisan make:policy InvoicePolicy --model=Invoice
That generates a class with methods like view, create, update, delete — one for each action a user might take on an Invoice. Each method receives the authenticated user and the model instance, and returns a boolean.
Writing a Policy That Actually Reflects Your Business Rules
Here's what a real invoice policy might look like:
class InvoicePolicy
{
public function view(User $user, Invoice $invoice): bool
{
return $user->company_id === $invoice->company_id;
}
public function update(User $user, Invoice $invoice): bool
{
return $this->view($user, $invoice)
&& $user->hasRole('billing');
}
public function delete(User $user, Invoice $invoice): bool
{
return $user->hasRole('admin')
&& $invoice->status !== 'paid';
}
}
Plain English: anyone in the same company can view an invoice. Only billing-role users can edit it. Only admins can delete it, and only if it hasn't been paid. This logic lives in one file, not scattered across six controllers.
Register the policy in AuthServiceProvider — or in Laravel 11+, use the #[Policy] attribute — and Laravel auto-discovers it if you follow the naming convention.
Using Policies in Controllers and Views
Once registered, authorization is a single line in your controller:
public function update(Request $request, Invoice $invoice): Response
{
$this->authorize('update', $invoice);
// If the user can't update, Laravel throws a 403 automatically
}
In Blade templates, the same logic hides UI elements the user can't use:
@can('update', $invoice)
<button>Edit Invoice</button>
@endcan
Users never see buttons they can't act on. Your support queue shrinks because clients aren't clicking things and getting cryptic error messages.
Building Self-Service Permission Management
Here's where this pays off at scale. When permissions are stored in a database — a role-permission table that your policies reference — you can build an admin panel that lets your client's team lead assign roles without a developer involved.
A non-technical office manager can say "give Sarah access to invoices" and click a toggle. The policy checks $user->hasRole('billing') and that's now true for Sarah. No code change. No deployment. No support ticket to you.
This is the real business value of getting authorization right. It moves routine access management from your todo list to theirs. You build it once, and it keeps running without you.
Policies Are Testable by Design
Because policy logic is isolated in a class, testing it is straightforward:
it('prevents viewing invoices from other companies', function () {
$user = User::factory()->create(['company_id' => 1]);
$invoice = Invoice::factory()->create(['company_id' => 2]);
expect((new InvoicePolicy)->view($user, $invoice))->toBeFalse();
});
No HTTP requests. No database seeds beyond the factory. Just a unit test that runs in milliseconds and proves the business rule works.
That test suite becomes your audit trail. When a client asks "how do you know Sarah can't see Acme's invoices?" — you show them a passing test that proves exactly that.
The Bottom Line
Authorization feels like infrastructure — invisible when it works, catastrophic when it doesn't. Laravel Policies give you a home for authorization logic that's organized, testable, and extensible.
Get this right early and you're building a product your clients can grow into. Get it wrong and you're forever patching permission bugs in production.
Let's talk about building the right access model for your application.