Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Policies and Gates: Complete Authorization Guide

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.

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.

Gates vs Policies DecisionAuthorization check needed?Tied to a model?Post, Order, InvoiceStandalone ability?Dashboard, export, featureUse a Policyupdate, delete, viewAnyUse a GateGate::define closureBoth resolve via Gate facade under the hood
Laravel Policies and Gates decision tree — model-bound rules belong in Policies; global abilities fit Gates
CriteriaGatePolicy
Best forGlobal abilities, feature flags, cross-model checksCRUD and model-scoped rules
RegistrationGate::define() in service providerAuto-discovered or manual map in AuthServiceProvider
Controller usage$this->authorize('export-reports')$this->authorize('update', $post)
Blade@can('export-reports')@can('update', $post)
TestingGate::allows('ability')$user->can('update', $post)
StructureSingle closure or invokable classClass 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);
Authorization Request FlowHTTP RequestRoute + middlewareControllerauthorize()Gate FacadeResolves abilityPolicy Methodview, updatebefore() hookSuper-admin bypassAllow → proceed | Deny → 403AuthorizationException
How Laravel Policies and Gates resolve an authorization check from controller to policy method

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.

Authorization Enforcement LayersWeb Application1. auth middleware2. can: middleware3. controller authorize()4. @can in Blade (UI only)REST API1. auth:sanctum2. tokenCan() scope3. Policy via authorize()4. Rate limiting middlewareShared: Policy + Gate business rulesSame DocumentPolicy for web and APINever duplicate logic in controllers
Laravel Policies and Gates apply across web and API layers—UI directives never replace server-side checks

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 @role directives.
  • 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.

Authorization GotchasAnti-patternRole checks only in BladeHidden button ≠ securityFixauthorize() in controllerPolicy on every mutating routeAnti-patternN+1 queries inside policiesassignedCases() per rowFixEager load relationshipsCache role checks in requestAnti-patternPolicy logic in middlewareHard to test, no model contextFixPolicy class per modelbefore() for admin bypass
Production mistakes to avoid when implementing Laravel Policies and Gates

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 @can directives 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

Gates handle global abilities like view-admin-dashboard. Policies bind CRUD rules to Eloquent models. Use both in mature apps.

Run php artisan make:policy DocumentPolicy --model=Document. Laravel 13 auto-discovers policies when App\Models\Document maps to App\Policies\DocumentPolicy. Artisan scaffolds viewAny, view, create, update, delete, restore, and forceDelete. You only implement methods you need; undefined abilities return false unless before() overrides them. Pair policies with solid database design—foreign keys like firm_id and assignment pivot tables keep policy logic readable instead of nested query spaghetti.

before() runs ahead of every ability check on that policy. Return true to allow immediately, false to deny, or null to fall through to the specific method such as view or update. On client portals I have built, super-admin bypass belongs here—not copied into six separate methods. This keeps authorization DRY and makes the intent obvious to the next developer reviewing access rules.

In Laravel 11 and 13, gate definitions typically live in app/Providers/AppServiceProvider.php inside boot() using Gate::define(). Pass a closure for simple checks or an invokable class like ExportFinancialReportGate::class for complex logic that needs dependency injection and unit tests. Gates suit standalone abilities—admin dashboard access, report exports, subscription-gated features—not tied to one Eloquent row.

The AuthorizesRequests trait exposes authorize(). Call $this->authorize('update', $document) and Laravel resolves DocumentPolicy@update. Failed checks throw Illuminate\Auth\Access\AuthorizationException, returning HTTP 403. For REST controllers following conventions, $this->authorizeResource(Document::class, 'document') in the constructor maps index to viewAny, show to view, store to create, update to update, and destroy to delete automatically.

No. @can, @cannot, and @canany only control UI visibility—a user can POST directly to your endpoint regardless of what the template rendered. Always enforce rules server-side via authorize(), Form Request authorize(), or route middleware. Blade checks are presentation-only. Hiding an Edit button without a controller check is an authorization bug waiting to happen, not a security control.

Laravel's built-in layer handles abilities but does not ship role tables, admin UIs, or permission cache invalidation. Use Spatie Laravel Permission when you have more than two user types or stakeholders ask for granular roles like invoice-only access. Keep permissions coarse and stable—documents.update, cases.manage—and put ownership, firm membership, and model state checks inside Policies. Spatie registers permissions as Gates, so $user->can('documents.update') works everywhere.

Write policy unit tests calling methods directly with User and model factories—expect true when the owner updates, false when another firm's user views. Feature tests with actingAs() should assert 403 on denied routes and redirect guests to login. Fake gates with Gate::define() for isolated controller tests. Run authorization tests in GitLab CI on every merge request. A regression that opens document access to the wrong firm is a data breach, not a cosmetic bug.

Token abilities like documents:read and documents:write define what the API token itself may do. Policies enforce business rules—firm membership, ownership, case assignment. Both layers must pass: check tokenCan('documents:write'), then authorize('update', $document). Token abilities gate API scope; policies gate model-specific logic. Document this dual requirement clearly in your API best practices and OpenAPI specs so mobile or third-party integrators understand the split.

Attach middleware like can:delete,document or can:view-admin-dashboard on routes. Middleware runs before the controller when the entire route requires one ability, keeping routing tables declarative. Use coarse route middleware for role-like gates, then finer controller or Form Request checks for action-specific rules. Pick the outermost sensible layer—middleware for whole routes, authorize() when logic depends on the specific model instance being accessed.

Policies run on nearly every authenticated request. Avoid heavy queries inside view() when listing collections—authorize viewAny on index pages and scope Eloquent queries in the controller by firm_id instead. Index columns your policies filter on, especially on PostgreSQL-backed apps where composite indexes help as data grows. Redis caching of role assignments through Spatie reduces repeated permission lookups during a single request cycle.

Yes. Implement authorize() returning $this->user()->can('update', $this->route('document')). Combining validation and authorization means invalid and unauthorized requests fail early with consistent responses. I use this pattern on traditional controllers and Livewire components alike. It keeps controllers thin and ensures no update action runs without both valid input and a passing policy check on the resolved model.

Failed authorize() checks throw AuthorizationException, which Laravel converts to HTTP 403 Forbidden.

By default, denied requests return 403 with a generic message. Override AuthorizationException handling in your exception handler for friendly web redirects while keeping JSON errors clean for API consumers. Laravel 11+ supports custom messages via the exception's withMessages pattern. Match the response format to the client—HTML users get readable feedback; API consumers get structured error payloads without leaking internal policy details.

Yes. App\Models\Invoice maps to App\Policies\InvoicePolicy automatically unless you register non-standard paths manually.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: