
September 07, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Authorization bugs are quiet killers. A user who can view another client's documents, cancel someone else's booking, or approve their own expense report will not trigger an error log—they will simply do the wrong thing until someone notices. Laravel Policies and Gates: Complete Authorization Guide is what you need when role checks scattered across controllers stop scaling. Laravel ships a first-class authorization layer that keeps business rules in one place, testable and readable. Whether you are building a client portal with document sharing, a booking system, or a multi-vendor marketplace, policies and gates are how you enforce who can do what—without duplicating if ($user->role === 'admin') in fifteen files. This guide walks through Laravel 13 on PHP 8.3+, from your first policy to production patterns I use on real applications.
view-admin-dashboard, while Policies bind CRUD-style rules to Eloquent models. Register both in AuthServiceProvider, call $this->authorize() in controllers, and use @can in Blade—keeping every permission check consistent and testable.What is the difference between Laravel Policies and Gates?
Laravel offers two complementary tools. Gates are anonymous closures or invokable classes that answer a single question: "Can this user perform ability X?" They suit actions that are not tied to a specific model—accessing an admin dashboard, exporting a report, or impersonating a user.
Policies are classes grouped around one Eloquent model. A PostPolicy might define view, create, update, and delete methods. Laravel maps them automatically when you follow naming conventions, and controller helpers like authorize('update', $post) resolve to PostPolicy@update.
| Criteria | Gate | Policy |
|---|---|---|
| Best for | Global abilities, feature flags, cross-model checks | CRUD and model-scoped rules |
| Registration | Gate::define() in service provider | Auto-discovered or manual map in AuthServiceProvider |
| Controller usage | $this->authorize('export-reports') | $this->authorize('update', $post) |
| Blade | @can('export-reports') | @can('update', $post) |
| Testing | Gate::allows('ability') | $user->can('update', $post) |
| Structure | Single closure or invokable class | Class with methods matching abilities |
In practice, mature Laravel applications use both. A legal-tech portal might keep DocumentPolicy for file access while a Gate named access-billing-module controls a subscription feature unrelated to any single document row. The modern Laravel architecture principle applies here: authorization logic belongs in dedicated classes, not controllers or Blade templates.
How do you create and register a Laravel Policy?
Laravel 13 continues policy auto-discovery. Name your policy after the model—App\Models\Invoice maps to App\Policies\InvoicePolicy—and Laravel finds it without manual registration. For non-standard paths, map models explicitly in app/Providers/AppServiceProvider.php or the dedicated auth provider.
Generate a policy with Artisan
php artisan make:policy DocumentPolicy --model=Document
php artisan make:policy OrderPolicy --model=Order Artisan scaffolds methods Laravel expects: viewAny, view, create, update, delete, restore, and forceDelete. You do not need every method—undefined abilities return false by default unless you override before().
A production-ready policy example
On client portals I have built, document access depends on ownership, firm membership, and case assignment. Here is a simplified pattern:
<?php
namespace App\Policies;
use App\Models\Document;
use App\Models\User;
class DocumentPolicy
{
public function before(User $user, string $ability): ?bool
{
if ($user->hasRole('super-admin')) {
return true;
}
return null;
}
public function viewAny(User $user): bool
{
return $user->firm_id !== null;
}
public function view(User $user, Document $document): bool
{
return $user->firm_id === $document->firm_id
&& ($user->id === $document->owner_id
|| $user->assignedCases()->where('id', $document->case_id)->exists());
}
public function update(User $user, Document $document): bool
{
return $user->id === $document->owner_id
|| $user->canManageCase($document->case_id);
}
public function delete(User $user, Document $document): bool
{
return $user->id === $document->owner_id;
}
} The before() hook runs ahead of every ability check. Return true to allow, false to deny, or null to fall through to the specific method. Super-admin bypass belongs here—not copied into six methods.
Register a Gate for standalone abilities
In Laravel 11+, gate definitions typically live in app/Providers/AppServiceProvider.php inside the boot() method, or in a dedicated provider. The pattern is unchanged:
use Illuminate\Support\Facades\Gate;
use App\Models\User;
public function boot(): void
{
Gate::define('view-admin-dashboard', function (User $user) {
return $user->hasRole('admin') || $user->hasRole('manager');
});
Gate::define('export-financial-report', function (User $user) {
return $user->hasPermission('reports.export')
&& $user->subscription->isActive();
});
} For complex gate logic, use an invokable class instead of a closure—easier to unit-test and inject dependencies through the container:
php artisan make:class Policies/ExportFinancialReportGate
Gate::define('export-financial-report', ExportFinancialReportGate::class); Pair policy creation with solid database design. Foreign keys like firm_id and pivot tables for case assignments make policy methods readable instead of nested query spaghetti. See database migration best practices for indexing columns your policies filter on.
How do you authorize actions in controllers, Blade, and APIs?
Once policies and gates exist, Laravel gives you several enforcement points. Pick the outermost layer that makes sense—route middleware for coarse roles, controller authorization for action-specific checks, and Blade directives purely for UI visibility.
Controller authorization
The AuthorizesRequests trait on your base controller exposes authorize():
public function update(UpdateDocumentRequest $request, Document $document)
{
$this->authorize('update', $document);
$document->update($request->validated());
return redirect()->route('documents.show', $document);
} Failed checks throw Illuminate\Auth\Access\AuthorizationException, which Laravel converts to a 403 response. Never rely on hiding a button as your only protection—always authorize server-side. Form Requests can authorize too:
public function authorize(): bool
{
return $this->user()->can('update', $this->route('document'));
} I often combine Form Request validation with authorization so invalid and unauthorized requests fail early with consistent responses. That pattern fits well with Livewire components and traditional controllers alike.
Route middleware with can:
Route::delete('/documents/{document}', [DocumentController::class, 'destroy'])
->middleware('can:delete,document');
Route::get('/admin/dashboard', AdminDashboardController::class)
->middleware('can:view-admin-dashboard'); Middleware runs before the controller. Use it when the entire route requires one ability and you want declarative routing tables.
Blade directives
@can('update', $document)
<a href="{{ route('documents.edit', $document) }}" class="btn btn-primary">Edit</a>
@endcan
@cannot('delete', $document)
<span class="text-muted">You cannot delete this file</span>
@endcannot
@canany(['update', 'delete'], $document)
<div class="dropdown">...</div>
@endcanany Blade checks are presentation-only. A user can POST directly to your endpoint regardless of what the template rendered.
API and Sanctum token abilities
For REST APIs, the same policy methods apply. With Laravel Sanctum, token abilities add a second axis—what the token itself may do:
$token = $user->createToken('mobile-app', ['documents:read', 'documents:write']);
if ($request->user()->tokenCan('documents:write')) {
$this->authorize('update', $document);
} Token abilities gate API scope; policies gate business rules. Both must pass. Document this clearly in your API best practices and OpenAPI specs. For Passport-based OAuth apps, scopes play a similar role.
Resource controllers and authorizeResource
For standard REST controllers, register all resource policies at once:
public function __construct()
{
$this->authorizeResource(Document::class, 'document');
} This maps index to viewAny, show to view, store to create, and so on. It saves boilerplate when your controller follows REST conventions—common on RESTful Laravel APIs.
When should you use Spatie Permission instead of native Policies?
Laravel's built-in authorization handles abilities well. It does not ship role and permission database tables, admin UIs to assign them, or cache invalidation when roles change. That is where Spatie Laravel Permission enters—and I use it on most production apps with more than two user types.
The split is straightforward:
- Spatie Permission stores roles and permissions in the database, provides
$user->hasRole()and$user->can('permission-name'), and integrates with Blade via@roledirectives. - Policies and Gates encode business logic that depends on model state—ownership, firm membership, order status, subscription tier.
A common mistake is putting business rules into permission names like edit-document-case-123. Permissions should be coarse and stable: documents.update, cases.manage. Policies evaluate whether this specific document qualifies.
public function update(User $user, Document $document): bool
{
if (! $user->can('documents.update')) {
return false;
}
return $user->firm_id === $document->firm_id;
} Spatie also registers permissions as Gates automatically, so $user->can('documents.update') works everywhere. For apps with dozens of roles—vendor, staff, client, auditor—the package saves weeks compared to rolling your own pivot tables. It appears in my regular toolkit alongside packages covered in essential Laravel plugins.
When native authorization alone suffices: single-role apps, prototypes, or internal tools with two fixed user types. Once stakeholders ask "can we add a role that only sees invoices but not contracts?", reach for Spatie early rather than refactoring later.
For Symfony-heavy teams comparing patterns, Symfony Voters solve a similar problem; Laravel Policies are the direct equivalent with less ceremony.
How do you test Laravel authorization rules?
Untested authorization is assumed broken. Laravel's testing helpers make policy coverage fast to write and cheap to run in CI.
Policy unit tests
use App\Models\Document;
use App\Models\User;
use App\Policies\DocumentPolicy;
test('owner can update their document', function () {
$user = User::factory()->create();
$document = Document::factory()->for($user, 'owner')->create();
expect((new DocumentPolicy)->update($user, $document))->toBeTrue();
});
test('user from another firm cannot view document', function () {
$user = User::factory()->create(['firm_id' => 1]);
$document = Document::factory()->create(['firm_id' => 2]);
expect((new DocumentPolicy)->view($user, $document))->toBeFalse();
}); Feature tests with actingAs
test('guest cannot delete document', function () {
$document = Document::factory()->create();
$this->delete(route('documents.destroy', $document))
->assertRedirect(route('login'));
});
test('authorized user receives 403 when policy denies', function () {
$user = User::factory()->create();
$document = Document::factory()->create(['firm_id' => 999]);
$this->actingAs($user)
->get(route('documents.show', $document))
->assertForbidden();
}); Gate fakes for isolated tests
use Illuminate\Support\Facades\Gate;
test('dashboard loads when gate allows', function () {
Gate::define('view-admin-dashboard', fn () => true);
$this->actingAs(User::factory()->create())
->get('/admin/dashboard')
->assertOk();
}); Run authorization tests in your GitLab CI pipeline on every merge request. A regression that opens document access to the wrong firm is a data breach, not a cosmetic bug. I treat policy tests with the same priority as payment gateway tests on eCommerce projects like Nepal Gift Card.
Performance considerations
Policies run on nearly every authenticated request. Avoid heavy queries inside view methods when listing collections—instead, use viewAny for index pages and scope Eloquent queries in the controller:
public function index()
{
$this->authorize('viewAny', Document::class);
$documents = Document::query()
->where('firm_id', auth()->user()->firm_id)
->paginate(20);
return view('documents.index', compact('documents'));
} On PostgreSQL-backed apps, firm-scoped indexes keep these queries fast as data grows—see PostgreSQL for Laravel developers for composite index patterns. Redis caching of role assignments (Spatie handles this) reduces repeated permission lookups during a single request cycle.
Custom policy responses
By default, denied requests return 403 with a generic message. Override AuthorizationException handling or use Gate::authorize with custom messages in Laravel 11+ via the exception's withMessages pattern, or return a friendly redirect in your exception handler for web routes while keeping JSON errors clean for API consumers.
Official reference: the Laravel 13 authorization documentation covers policy discovery, guest user handling, and inline authorization. Cross-check method signatures there when upgrading from Laravel 12—PHP 8.3 is required for Laravel 13, while Laravel 12 remains supported through February 2027 on PHP 8.2+.
Key Takeaways
- Use Policies for model-bound rules (view, update, delete) and Gates for standalone abilities like dashboard access or feature flags.
- Always call
authorize()server-side—Blade@candirectives control UI only and are not security boundaries. - Combine Spatie Permission for role storage with Policies for business logic; keep permissions coarse and policies specific.
- Register
authorizeResource()on REST controllers to eliminate repetitive authorization boilerplate. - Write feature and unit tests for every policy method that protects sensitive data—run them in CI on every deploy.
- Eager-load relationships and scope queries in controllers; do not run N+1 queries inside policy methods on index pages.
People Also Ask
Can guests use Laravel Policies?
Yes. Type-hint ?User $user in policy methods to handle unauthenticated visitors. Return false for abilities guests should never perform, or implement public read access by returning true from view when the document is marked public. Pair this with route middleware: auth middleware stops guests entirely on private routes, while policies fine-tune access within authenticated or mixed contexts.
How do Laravel Policies work with Livewire and Inertia?
Call $this->authorize() inside Livewire component actions the same way you would in a controller—Livewire components are PHP classes with full access to authorization helpers. Pass authorization results to Inertia or Livewire views as props ('canUpdate' => auth()->user()->can('update', $document)) for button visibility, but never skip the server-side check when the action executes.
What happens when no policy method exists for an ability?
Laravel returns false—access denied. This is secure-by-default. If you define before() and it returns null, Laravel falls through to the named method; if that method does not exist, the result is still false. Explicitly define abilities you intend to allow, and treat missing methods as intentional denial.
Should authorization logic live in Eloquent models?
Keep models focused on data and relationships. Small helper methods like $user->belongsToFirm($firmId) are fine—policies can call them. Avoid $document->userCanEdit($user) on the model itself; that scatters authorization across layers and makes testing harder. Policies are the single source of truth for access decisions.
Build authorization correctly from day one
Laravel Policies and Gates: Complete Authorization Guide boils down to one discipline: every mutating action passes through a named, tested ability before it touches the database. Gates handle global features; policies protect your models; Spatie Permission manages roles when the matrix outgrows two user types. On legal-tech portals, booking platforms, and enterprise Laravel applications, that structure has prevented the kind of quiet data leaks that erode client trust overnight.
If you are auditing an existing app or starting a portal that needs role-based document access, payment approvals, or multi-tenant isolation, solid authorization architecture saves expensive refactors later. Use the JSON formatter to inspect API error payloads during auth testing, review related work on Court Marriage In Nepal and other legal-tech portals, and read more on Laravel development in the blog archive.
Need help designing authorization for a production Laravel 13 application? API development and custom software projects are where I implement these patterns for clients in Nepal and worldwide. Learn more about my work, or contact us to discuss your project requirements.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

