
September 07, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Authorization bugs do not throw stack traces. They let the wrong user view a file, cancel a booking, or approve an expense until someone notices weeks later. If you searched for laravel 13 authorization gates policies documentation, you need the official patterns mapped to real controller and API code—not scattered if ($user->role === 'admin') checks. Laravel 13 on PHP 8.3+ ships a first-class authorization layer: Gates for standalone abilities, Policies for model-bound rules, and one Gate facade that resolves both. Whether you build a client portal with document sharing, a booking app, or a multi-vendor marketplace, this guide follows the Laravel development patterns I use on production apps—register once, enforce everywhere, test in CI.
AppServiceProvider, auto-discover policies by naming convention, call $this->authorize() in controllers, and use @can in Blade for UI only—keeping every check testable and consistent.What is the difference between Laravel Policies and Gates?
Laravel offers two tools that share one engine. Gates answer a single question: can this user perform ability X? They fit actions with no single model—admin dashboards, report exports, feature toggles.
Policies are classes grouped around one Eloquent model. A DocumentPolicy defines view, update, and delete. Laravel maps authorize('update', $document) to DocumentPolicy@update when naming conventions match.
| Criteria | Gate | Policy |
|---|---|---|
| Best for | Global abilities, feature flags, cross-model checks | CRUD and model-scoped rules |
| Registration | Gate::define() in AppServiceProvider | Auto-discovery or manual map in service provider |
| Controller | $this->authorize('export-reports') | $this->authorize('update', $post) |
| Blade | @can('export-reports') | @can('update', $post) |
| Testing | Gate::allows('ability') | $user->can('update', $post) |
| Structure | Closure or invokable class | Class with methods per ability |
Mature apps use both. A legal-tech portal might keep DocumentPolicy for file rows while a Gate named access-billing-module gates a subscription screen. The modern Laravel architecture rule applies: authorization lives in dedicated classes, not controllers or Blade.
The official Laravel 13 authorization documentation describes Gates and Policies as two registration styles for the same underlying system. That detail matters when you debug why a check returns false.
How do you create and register a Laravel Policy in Laravel 13?
Laravel 13 continues policy auto-discovery. Name your policy after the model—App\Models\Invoice maps to App\Policies\InvoicePolicy—and the framework finds it without manual wiring. For custom paths, map models in AppServiceProvider using Gate::policy().
Generate a policy with Artisan
php artisan make:policy DocumentPolicy --model=Document
php artisan make:policy OrderPolicy --model=Order Artisan scaffolds standard methods: viewAny, view, create, update, delete, restore, and forceDelete. You do not need every method. Undefined abilities return false unless before() overrides them.
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 before every ability check. Return true to allow, false to deny, or null to fall through. Super-admin bypass belongs here—not copied into six methods.
Register Gates for standalone abilities
Since Laravel 11, gate definitions live in app/Providers/AppServiceProvider.php inside boot(). The skeleton no longer ships a separate AuthServiceProvider by default.
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. It is easier to unit-test and inject dependencies through the container:
php artisan make:class Policies/ExportFinancialReportGate
Gate::define('export-financial-report', ExportFinancialReportGate::class); Pair policies with solid schema design. Foreign keys like firm_id and pivot tables for case assignments keep policy methods readable. See database migration best practices for indexing columns your policies filter on.
Manual policy mapping when auto-discovery fails
Non-standard model or policy namespaces need an explicit map. Add this to AppServiceProvider::boot():
Gate::policy(\App\Domain\Billing\Models\Invoice::class, \App\Policies\Billing\InvoicePolicy::class); Run php artisan policy:show Invoice when available in your toolchain to confirm Laravel resolves the correct class. After upgrades from Laravel 12, verify discovery still matches your folder layout—Laravel 12 remains supported through February 2027 on PHP 8.2+, but Laravel 13 requires PHP 8.3 or higher.
How do you authorize actions in controllers, Blade, and APIs?
Once policies and gates exist, Laravel gives you several enforcement points. Use route middleware for coarse access, 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 AuthorizationException, which Laravel converts to a 403 response. Never rely on hiding a button as your only protection. 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. That pattern fits 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 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 removes boilerplate on RESTful Laravel APIs.
Inline checks without throwing
When you need a boolean instead of an exception, use Gate::allows() or $user->can():
if ($request->user()->can('update', $document)) {
/* show edit form */
} Use authorize() for mutating routes. Use can() for conditional UI or branching logic that should not abort the request.
When should you use Spatie Permission instead of native Policies?
Laravel's built-in authorization handles abilities well. It does not ship role 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. It provides
$user->hasRole(),$user->can('permission-name'), and Blade@roledirectives. - Policies and Gates encode business logic that depends on model state—ownership, firm membership, order status, subscription tier.
A common mistake is encoding business rules into permission names like edit-document-case-123. Permissions should stay 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 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 sits in my regular toolkit alongside packages in essential Laravel plugins.
Native authorization alone suffices for single-role apps, prototypes, or internal tools with two fixed user types. Once stakeholders ask for a role that 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.
Performance considerations
Policies run on nearly every authenticated request. Avoid heavy queries inside view methods when listing collections. 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. Spatie caches role assignments, which cuts repeated permission lookups during one request cycle.
Guest users and nullable User type hints
Policies can accept guests. Type-hint ?User $user and return false for abilities guests must never perform. For public reads, return true from view when a document is marked public. Route middleware still controls whether guests reach the controller at all.
Custom 403 responses
Denied requests return 403 with a generic message by default. Handle AuthorizationException in bootstrap/app.php or your exception handler. Return a friendly redirect for web routes and clean JSON errors for API consumers—patterns covered in the Laravel 13 error handling docs.
Key Takeaways
- Use Policies for model-bound rules and Gates for standalone abilities like dashboard access or feature flags.
- Define gates in
AppServiceProvideron Laravel 13; rely on auto-discovery for conventionally named policies. - Always call
authorize()server-side—Blade@cancontrols UI only and is not a security boundary. - Combine Spatie Permission for role storage with Policies for business logic; keep permissions coarse and policies specific.
- Register
authorizeResource()on REST controllers to cut repetitive authorization boilerplate. - Write feature and unit tests for every policy method that protects sensitive data, and run them in CI on every deploy.
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 return true from view when content is public. Pair this with route middleware: auth stops guests on private routes, while policies fine-tune access within 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. Pass authorization results to views as props ('canUpdate' => auth()->user()->can('update', $document)) for button visibility. 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. That is secure-by-default. If before() returns null, Laravel falls through to the named method. If that method does not exist, the result is still false. Define abilities you intend to allow explicitly.
Should authorization logic live in Eloquent models?
Keep models focused on data and relationships. Small helpers like $user->belongsToFirm($firmId) are fine—policies can call them. Avoid $document->userCanEdit($user) on the model itself. Policies should be the single source of truth for access decisions.
Build authorization correctly from day one
Laravel 13 authorization gates and policies documentation 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 prevents the 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, 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 for examples of firm-scoped access patterns in production.
Need help designing authorization for a 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.

