13.x of the official Laravel documentation and to the official implementation and tests of the latest Framework release v13.42.0, checked on November 10, 2026. For authorization basics, see the authorization guide. For how providers are loaded, see package auto-discovery.
Don’t depend on the consuming app’s User class
As an example, addupdate authorization to the package’s Acme\Reports\Models\Report. The acme_reports table is assumed to have an owner_id column that stores the owner’s authentication identifier. How to ship migrations is covered in package migrations.
src/Models/Report.php is an ordinary Eloquent model.
App\Models\User, it depends on the consuming app’s namespace and model structure. If all you need is the user’s authentication identifier, use Laravel’s Authenticatable contract.
Put the following in src/Policies/ReportPolicy.php.
null identifiers first, then casts both values to strings to absorb the type difference between an integer from the database and a string. In this example only the owner may update. In apps that handle several user types or guards, also check whether the identifier alone uniquely identifies the subject. If it doesn’t, replace the policy with your own that includes user-type or tenant boundaries.
Authenticatable is a contract for things like the authentication identifier. It does not provide isAdministrator() or methods for fetching roles. If you need package-specific permission data, define a separate contract and have the consuming app inject an implementation.Register the mapping in a service provider
Inboot() of src/ReportsServiceProvider.php, register only the models the package owns.
Gate::policy() stores the policy class name keyed by the model’s class name. The registration itself does not build the policy, query the database or check any user. The Gate builds the policy with the container’s make() when it is needed, for example by Gate::getPolicyFor(). That means you can inject dependencies into the policy’s constructor.
The placement of Report and ReportPolicy also fits standard auto-discovery, but here the mapping is registered explicitly to make clear what the package provides. Registration alone does not apply authorization to HTTP endpoints. Call the following in a controller or similar before the update runs.
AuthorizationException is thrown, which normal HTTP handling turns into a 403 response. The policy method arguments differ between update, which receives a model, and abilities such as create, which receive the model’s class name. Follow the calling conventions in the authorization guide.
Read the policy resolution order
Gate::getPolicyFor() converts an object target to its class name and searches in the following order. This is the order for choosing a policy, not the order in which all authorization hooks run.
So while an explicit registration exists for the same
Report class, changing #[UsePolicy] or naming conventions alone does not swap the policy. On the other hand, if the consuming app uses a model that extends Report, a policy found via the child class’s own attribute or a guess comes before the explicit registration on the parent. Check with the model class you actually use, too.
Override explicitly in the consuming app
Callingpolicy() again for the same model replaces the mapping with the later registration. Simply putting it in another provider’s boot() depends on load order. One approach is to register an application-booted callback from the consuming app’s AppServiceProvider::boot().
ApplicationReportPolicy is a policy the consuming app implements. When the package registers in a normal boot(), this callback runs afterwards. It is not a mechanism that unconditionally wins over everything, including re-registrations in other booted callbacks or providers loaded later. In this example, the package’s registering provider is not a deferred provider.
Alternatively, you can leave the mapping unchanged and swap what ReportPolicy::class resolves to in the container. resolvePolicy() asks the container to resolve the policy every time, but whether you actually get a new instance depends on that policy’s binding. Avoid designs that read user or request data in the constructor and keep it in a singleton policy.
Keep hooks that affect all authorization separate
In a normal Gate check, the globalGate::before() runs first. If it returns anything other than null, the policy check does not run.
The diagram shows only the path where a policy is found and its matching method can be called. It omits the global after hooks that run afterwards and the fallback to Gate definitions when the policy has no matching method.
Don’t register a global Gate::before() that allows every ability just to implement admin handling inside your package. It would also allow authorization for other packages and the consuming app. A before() inside the policy is scoped to that policy, but it is not called when the policy has no method for the ability. When you want the following method to decide, return null instead of false.
Also, the update() in this example requires a non-nullable Authenticatable, so the policy method does not run for guests. Unless something like a global hook overrides the result, the request is denied. To allow guests, for example for public viewing, provide a nullable user argument and an explicit check separately.
Test registration and decisions separately
This assumes the Testbench base class introduced in package testing, loadingReportsServiceProvider::class in getPackageProviders(). The following is an example tests/Feature/ReportPolicyTest.php. It uses Laravel’s generic authenticatable model for users, so you can check decisions without saving to the database.
forUser() does not rewrite the user on the shared Gate. It creates a separate Gate, passing along the mappings, abilities and callbacks as they are at that moment. As a result, a per-user Gate that already exists does not see later policy registration changes on the shared Gate. Finish registration at boot, and don’t store per-user Gates in long-lived services.
In the consuming app you also need checks with the real guard and user model, and HTTP tests against the update endpoint. The tests above alone do not verify that authorization is applied to the route or how the owner identifier is stored in the database.
Contracts to keep when updating
- Have the results for the owner, non-owners, guests and records with no owner stayed the same?
- Do the published ability names match the policy method arguments?
- Does the consuming app’s policy replacement still work?
- If you support child models, is the policy actually selected for the child class correct?
- Are real update requests allowed and denied as expected, including the app’s global hooks?
Primary sources
- Official Laravel documentation: Authorization
- Official Laravel documentation: Package development
- Framework v13.42.0: Gate registration, resolution and forUser
- Framework v13.42.0: AuthServiceProvider, which registers the Gate as a singleton
- Framework v13.42.0: Application boot and booted
- Framework v13.42.0: Official tests for the authorization Gate